diff --git a/config.php b/config.php index 8daf20a..b116180 100644 --- a/config.php +++ b/config.php @@ -12,11 +12,9 @@ require_once __DIR__ . '/private/nexttoken.php'; require_once __DIR__ . '/private/usersettings.php'; require_once __DIR__ . '/private/base64config.php'; require_once __DIR__ . '/private/racketzip.php'; +require_once __DIR__ . '/private/viewdata.php'; -ini_set('display_errors', '1'); -ini_set('display_startup_errors', '1'); -ini_set('log_errors', '1'); -error_reporting(E_ALL); +require_once __DIR__ . '/config/reporting.php'; $DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite'; @@ -36,11 +34,11 @@ function h($s) return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } -function t($key, $fallback = null) +function t($key, $fallback = null, $values = array()) { global $languageStore, $language; - return $languageStore->translate($key, $language, $fallback); + return $languageStore->translateFormat($key, $language, $values, $fallback); } function post_value($name, $default = '') @@ -84,51 +82,18 @@ function token_status_class($token) : 'token-expired'; } +function code_value($value) +{ + return '' . h($value) . ''; +} + $language = resolve_user_language( $userSettings, $currentUser->id(), $languageStore->supportedLanguages() ); -$languageStore->seedDefaults(array( - 'app.title' => array('en' => 'Racket sandbox', 'nl' => 'Racket sandbox'), - 'app.manage_prompts' => array('en' => 'Manage prompts', 'nl' => 'Prompts beheren'), - 'app.user_management' => array('en' => 'User management', 'nl' => 'Gebruikersbeheer'), - 'app.configuration' => array('en' => 'Configuration', 'nl' => 'Configuratie'), - 'app.logout' => array('en' => 'Logout', 'nl' => 'Uitloggen'), - 'app.language' => array('en' => 'Language', 'nl' => 'Taal'), - 'app.logged_in_as' => array('en' => 'Logged in as:', 'nl' => 'Ingelogd als:'), - 'app.admin' => array('en' => 'Admin', 'nl' => 'Admin'), - 'app.back_to_sandbox' => array('en' => 'Back to Racket sandbox', 'nl' => 'Terug naar Racket sandbox'), - 'app.download_settings' => array('en' => 'Download settings', 'nl' => 'Downloadinstellingen'), - 'app.maintenance' => array('en' => 'Maintenance', 'nl' => 'Onderhoud'), - 'app.next_tokens' => array('en' => 'Next tokens', 'nl' => 'Next-tokens'), - 'app.current_next_tokens' => array('en' => 'Next tokens', 'nl' => 'Next-tokens'), - 'app.token' => array('en' => 'Token', 'nl' => 'Token'), - 'app.created_at' => array('en' => 'Created at', 'nl' => 'Aangemaakt op'), - 'app.expires_at' => array('en' => 'Expires at', 'nl' => 'Verloopt op'), - 'app.no_current_next_tokens' => array('en' => 'No next tokens.', 'nl' => 'Geen next-tokens.'), - 'app.remove_expired_tokens' => array('en' => 'Remove expired next tokens', 'nl' => 'Verlopen next-tokens verwijderen'), - 'app.racket_zip_chunk_kb' => array('en' => 'Racket installation max base64 chunk size (KiB)', 'nl' => 'Maximale base64-chunkgrootte Racket-installatie (KiB)'), - 'app.package_zip_chunk_kb' => array('en' => 'Package/module max base64 chunk size (KiB)', 'nl' => 'Maximale base64-chunkgrootte packages/modules (KiB)'), - 'app.save_configuration' => array('en' => 'Save configuration', 'nl' => 'Configuratie opslaan'), - 'app.configuration_saved' => array('en' => 'Configuration saved.', 'nl' => 'Configuratie opgeslagen.'), - 'app.racket_parts_regenerated' => array('en' => 'Racket installation parts regenerated:', 'nl' => 'Racket-installatie parts opnieuw gemaakt:'), - 'app.chunk_size_hint_v2' => array( - 'en' => 'Values are maximum base64 payload sizes in KiB. A 6144 KiB binary chunk becomes 8192 KiB base64. Racket installation parts are regenerated when this configuration is saved.', - 'nl' => 'Waarden zijn maximale base64-payloadgroottes in KiB. Een binaire chunk van 6144 KiB wordt 8192 KiB base64. Racket-installatie parts worden opnieuw gemaakt wanneer deze configuratie wordt opgeslagen.', - ), - 'app.effective_binary_chunk' => array('en' => 'Effective binary chunk', 'nl' => 'Effectieve binaire chunk'), - 'app.racket_zip_source' => array('en' => 'Racket installation source', 'nl' => 'Bronbestand Racket-installatie'), - 'app.racket_parts' => array('en' => 'Racket installation parts', 'nl' => 'Racket-installatie parts'), - 'app.racket_parts_current' => array('en' => 'current', 'nl' => 'actueel'), - 'app.racket_parts_missing' => array('en' => 'missing or outdated; save configuration to regenerate', 'nl' => 'ontbreken of verouderd; sla configuratie op om opnieuw te maken'), - 'app.expired_tokens_removed' => array('en' => 'Expired next tokens removed:', 'nl' => 'Verlopen next-tokens verwijderd:'), - 'app.cleanup_help' => array( - 'en' => 'Expired links should return an outdated information message to the AI agent. Cleanup only removes old token rows from SQLite.', - 'nl' => 'Verlopen links moeten een melding over verouderde informatie aan de AI-agent teruggeven. Opruimen verwijdert alleen oude tokenrijen uit SQLite.' - ), -)); +seed_template_translations($languageStore, 'config.html'); if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = post_value('action'); @@ -145,12 +110,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { )); $base64ChunkConfig = load_base64_chunk_config(); $manifest = split_racket_zip_parts($base64ChunkConfig['racket_zip_max_base64_kb']); - $message = t('app.configuration_saved', 'Configuration saved.') . ' ' . - t('app.racket_parts_regenerated', 'Racket installation parts regenerated:') . ' ' . - (int)$manifest['part_count']; + $message = t('app.configuration_saved_with_parts', 'Configuration saved. Racket installation parts regenerated: {{count}}', array( + 'count' => (int)$manifest['part_count'], + )); } elseif ($action === 'cleanup_tokens') { $deleted = $tokens->cleanup(); - $message = t('app.expired_tokens_removed', 'Expired next tokens removed:') . ' ' . $deleted; + $message = t('app.expired_tokens_removed', 'Expired next tokens removed: {{count}}', array( + 'count' => $deleted, + )); } } catch (Throwable $e) { $error = $e->getMessage(); @@ -177,8 +144,9 @@ if (!$racketPartsCurrent && is_file(RACKET_ZIP_FILE) && is_readable(RACKET_ZIP_F $racketPartsCurrent = true; if ($message === '') { - $message = t('app.racket_parts_regenerated', 'Racket installation parts regenerated:') . ' ' . - (int)$racketPartsManifest['part_count']; + $message = t('app.racket_parts_regenerated', 'Racket installation parts regenerated: {{count}}', array( + 'count' => (int)$racketPartsManifest['part_count'], + )); } } catch (Throwable $e) { if ($error === '') { @@ -203,23 +171,10 @@ foreach ($languageStore->supportedLanguages() as $lang) { $headerLanguages[$lang] = $languageStore->languageLabel($lang); } -$styleVersion = @filemtime(__DIR__ . '/styles.css') ?: time(); +$styleVersion = @filemtime(__DIR__ . '/css/styles.css') ?: time(); header('Content-Type: text/html; charset=utf-8'); -?> - - - - -<?= h(t('app.configuration', 'Configuration')) ?> - - - - -
- - t('app.configuration', 'Configuration'), 'nav_items' => array( array('label' => t('app.back_to_sandbox', 'Back to Racket sandbox'), 'url' => '/?lang=' . rawurlencode($language)), @@ -252,98 +207,77 @@ render_app_header(array( 'message' => $message, 'error' => $error, )); -?> -
+$racketPartsStatus = $racketPartsCurrent + ? t('app.racket_parts_current_with_date', 'current, {{created_at}}', array( + 'created_at' => (string)($racketPartsManifest['created_at'] ?? ''), + )) + : t('app.racket_parts_missing', 'missing or outdated; save configuration to regenerate'); -
-

+if (count($currentNextTokens) > 0) { + $tokenRowsHtml = ''; -
- - - - -
+ foreach ($currentNextTokens as $nextToken) { + $tokenRowsHtml .= RacketSandboxTemplate::renderFile('partials/config-token-row.html', array( + 'status_class' => token_status_class($nextToken), + 'token' => $nextToken['token'] ?? '', + 'created_at' => format_token_time($nextToken['created_at'] ?? 0), + 'expires_at' => format_token_time($nextToken['expires_at'] ?? 0), + )) . "\n"; + } -

+ $currentTokensHtml = RacketSandboxTemplate::renderFile('partials/config-token-table.html', array( + 'token_label' => t('app.token', 'Token'), + 'created_at_label' => t('app.created_at', 'Created at'), + 'expires_at_label' => t('app.expires_at', 'Expires at'), + 'token_rows_html' => $tokenRowsHtml, + )); +} else { + $currentTokensHtml = RacketSandboxTemplate::renderFile('partials/paragraph.html', array( + 'class' => 'small', + 'text' => t('app.no_current_next_tokens', 'No current next tokens.'), + )); +} - - - - - - - - - - - - - - - - - - - - - -
KiB: bytes
KiB: bytes
config/racket.zip bytes
- -, - - - - -
-
- -
-

- -
- - -
- - -
- -

- -

- -

- - 0): ?> - - - - - - - - - - - - - -
- -

- -
-
- -
-
- - +echo RacketSandboxTemplate::renderFile('config.html', array( + 'language' => $language, + 'language_url' => rawurlencode($language), + 'title' => t('app.configuration', 'Configuration'), + 'style_version' => $styleVersion, + 'header_html' => $headerHtml, + 'download_settings_label' => t('app.download_settings', 'Download settings'), + 'racket_zip_chunk_label' => t('app.racket_zip_chunk_kb', 'Racket installation max base64 chunk size (KiB)'), + 'package_zip_chunk_label' => t('app.package_zip_chunk_kb', 'Package/module max base64 chunk size (KiB)'), + 'racket_zip_max_base64_kb' => $base64ChunkConfig['racket_zip_max_base64_kb'], + 'package_zip_max_base64_kb' => $base64ChunkConfig['package_zip_max_base64_kb'], + 'save_configuration_label' => t('app.save_configuration', 'Save configuration'), + 'chunk_size_hint' => t('app.chunk_size_hint_v2', 'Values are maximum base64 payload sizes in KiB. A {{chunk_size}} KiB binary chunk becomes {{base64_chunk_size}} KiB base64. Racket installation parts are regenerated when this configuration is saved.', array( + 'chunk_size' => (string)intdiv(base64_binary_chunk_bytes_for_kib($base64ChunkConfig['racket_zip_max_base64_kb']), 1024), + 'base64_chunk_size' => (string)$base64ChunkConfig['racket_zip_max_base64_kb'], + )), + 'racket_zip_base64_chunk_size' => t('app.base64_chunk_size_kib', '{{size}} KiB', array( + 'size' => code_value($base64ChunkConfig['racket_zip_max_base64_kb']), + )), + 'package_zip_base64_chunk_size' => t('app.base64_chunk_size_kib', '{{size}} KiB', array( + 'size' => code_value($base64ChunkConfig['package_zip_max_base64_kb']), + )), + 'racket_zip_effective_binary_chunk' => t('app.effective_binary_chunk_bytes', 'Effective binary chunk: {{bytes}} bytes', array( + 'bytes' => code_value(base64_binary_chunk_bytes_for_kib($base64ChunkConfig['racket_zip_max_base64_kb'])), + )), + 'package_zip_effective_binary_chunk' => t('app.effective_binary_chunk_bytes', 'Effective binary chunk: {{bytes}} bytes', array( + 'bytes' => code_value(base64_binary_chunk_bytes_for_kib($base64ChunkConfig['package_zip_max_base64_kb'])), + )), + 'racket_zip_source_label' => t('app.racket_zip_source', 'Racket installation source'), + 'racket_zip_file_size' => t('app.file_size_bytes', '{{bytes}} bytes', array( + 'bytes' => code_value(is_file(RACKET_ZIP_FILE) ? (string)filesize(RACKET_ZIP_FILE) : '0'), + )), + 'racket_parts_label' => t('app.racket_parts', 'Racket installation parts'), + 'racket_part_count' => (string)($racketPartsManifest['part_count'] ?? 0), + 'racket_parts_status' => $racketPartsStatus, + 'maintenance_label' => t('app.maintenance', 'Maintenance'), + 'next_tokens_label' => t('app.next_tokens', 'Next tokens'), + 'remove_expired_tokens_label' => t('app.remove_expired_tokens', 'Remove expired next tokens'), + 'cleanup_help' => t('app.cleanup_help', 'Expired links should return an outdated information message to the AI agent. Cleanup only removes old token rows from SQLite.'), + 'current_next_tokens_label' => t('app.current_next_tokens', 'Current next tokens'), + 'current_tokens_html' => $currentTokensHtml, +)); diff --git a/config/reporting.php b/config/reporting.php new file mode 100644 index 0000000..46dcb9e --- /dev/null +++ b/config/reporting.php @@ -0,0 +1,9 @@ +translate($key, $language, $fallback); + return $languageStore->translateFormat($key, $language, $values, $fallback); } function current_scheme() @@ -154,64 +152,7 @@ $language = resolve_user_language( $languageStore->supportedLanguages() ); -$languageStore->seedDefaults(array( - 'app.title' => array('en' => 'Racket sandbox', 'nl' => 'Racket sandbox'), - 'app.manage_prompts' => array('en' => 'Manage prompts', 'nl' => 'Prompts beheren'), - 'app.logout' => array('en' => 'Logout', 'nl' => 'Uitloggen'), - 'app.language' => array('en' => 'Language', 'nl' => 'Taal'), - 'app.logged_in_as' => array('en' => 'Logged in as:', 'nl' => 'Ingelogd als:'), - 'app.admin' => array('en' => 'admin', 'nl' => 'admin'), - 'app.bootstrap_link' => array('en' => 'Bootstrap link', 'nl' => 'Bootstraplink'), - 'app.generate_bootstrap_link' => array('en' => 'Generate bootstrap link', 'nl' => 'Bootstraplink genereren'), - 'app.ttl_minutes' => array('en' => 'TTL in minutes', 'nl' => 'TTL in minuten'), - 'app.ttl_range_help' => array('en' => 'Allowed range: 30 minutes to 8 hours.', 'nl' => 'Toegestaan bereik: 30 minuten tot 8 uur.'), - 'app.generated_link' => array('en' => 'Generated link', 'nl' => 'Gegenereerde link'), - 'app.copy' => array('en' => 'Copy', 'nl' => 'Kopieer'), - 'app.copied' => array('en' => 'Copied', 'nl' => 'Gekopieerd'), - 'app.generated_link_help' => array( - 'en' => 'Give this link to the AI agent. The agent should start from this link and then only follow links from the generated HTML pages.', - 'nl' => 'Geef deze link aan de AI-agent. De agent moet vanaf deze link starten en daarna alleen links volgen vanuit de gegenereerde HTML-paginas.' - ), - 'app.select_prompt' => array('en' => 'Select prompt', 'nl' => 'Prompt selecteren'), - 'app.copy_full_prompt' => array('en' => 'Copy full prompt', 'nl' => 'Volledige prompt kopieren'), - 'app.bootstrap_prompt_help' => array( - 'en' => 'Choose one of your prompts. The placeholder {{bootstrap-racket-link}} is replaced by the generated bootstrap link.', - 'nl' => 'Kies een van je prompts. De placeholder {{bootstrap-racket-link}} wordt vervangen door de gegenereerde bootstraplink.' - ), - 'app.no_bootstrap_prompts' => array( - 'en' => 'No personal prompts are available for this language. Copy a default prompt first from prompt management.', - 'nl' => 'Er zijn geen persoonlijke prompts beschikbaar voor deze taal. Kopieer eerst een standaardprompt vanuit promptbeheer.' - ), - 'app.user_management' => array('en' => 'User management', 'nl' => 'Gebruikersbeheer'), - 'app.configuration' => array('en' => 'Configuration', 'nl' => 'Configuratie'), - 'app.user_management_help' => array( - 'en' => 'Users are registered manually by email address, full name and password. This page only manages existing users.', - 'nl' => 'Gebruikers worden handmatig geregistreerd met e-mailadres, volledige naam en wachtwoord. Deze pagina beheert alleen bestaande gebruikers.' - ), - 'app.id' => array('en' => 'ID', 'nl' => 'ID'), - 'app.full_name' => array('en' => 'Full name', 'nl' => 'Volledige naam'), - 'app.email' => array('en' => 'Email', 'nl' => 'E-mail'), - 'app.enabled' => array('en' => 'Enabled', 'nl' => 'Ingeschakeld'), - 'app.created' => array('en' => 'Created', 'nl' => 'Gemaakt'), - 'app.last_login' => array('en' => 'Last login', 'nl' => 'Laatste login'), - 'app.actions' => array('en' => 'Actions', 'nl' => 'Acties'), - 'app.yes' => array('en' => 'yes', 'nl' => 'ja'), - 'app.no' => array('en' => 'no', 'nl' => 'nee'), - 'app.save_flags' => array('en' => 'Save flags', 'nl' => 'Vlaggen opslaan'), - 'app.new_password' => array('en' => 'New password', 'nl' => 'Nieuw wachtwoord'), - 'app.change_password' => array('en' => 'Change password', 'nl' => 'Wachtwoord wijzigen'), - 'app.delete_user' => array('en' => 'Delete user', 'nl' => 'Gebruiker verwijderen'), - 'app.delete_user_confirm' => array('en' => 'Delete user', 'nl' => 'Gebruiker verwijderen'), - 'app.cannot_delete_self' => array('en' => 'You cannot delete your own account.', 'nl' => 'Je kunt je eigen account niet verwijderen.'), - 'app.bootstrap_link_issued' => array('en' => 'Bootstrap link issued.', 'nl' => 'Bootstraplink aangemaakt.'), - 'app.password_changed_for' => array('en' => 'Password changed for:', 'nl' => 'Wachtwoord gewijzigd voor:'), - 'app.user_flags_updated' => array('en' => 'User flags updated.', 'nl' => 'Gebruikersvlaggen bijgewerkt.'), - 'app.user_deleted' => array('en' => 'User deleted.', 'nl' => 'Gebruiker verwijderd.'), - 'app.admin_rights_required' => array('en' => 'Admin rights required.', 'nl' => 'Adminrechten vereist.'), - 'app.cannot_remove_own_admin' => array('en' => 'You cannot remove your own admin rights.', 'nl' => 'Je kunt je eigen adminrechten niet verwijderen.'), - 'app.cannot_disable_self' => array('en' => 'You cannot disable your own account.', 'nl' => 'Je kunt je eigen account niet uitschakelen.'), - 'app.unknown_action' => array('en' => 'Unknown action:', 'nl' => 'Onbekende actie:'), -)); +seed_template_translations($languageStore, 'index.html'); if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = post_value('action'); @@ -260,7 +201,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $password = (string)post_value('password'); $auth->setPassword($email, $password); - $message = t('app.password_changed_for', 'Password changed for:') . ' ' . $email; + $message = t('app.password_changed_for', 'Password changed for: {{email}}', array('email' => $email)); } elseif ($action === 'set_flags') { $userId = (int)post_value('user_id'); $isAdmin = post_bool('is_admin'); @@ -281,13 +222,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $userId = (int)post_value('user_id'); if ($userId === $currentUser->id()) { - throw new Exception('You cannot delete your own account.'); + throw new Exception(t('app.cannot_delete_self', 'You cannot delete your own account.')); } $auth->deleteUser($userId); $message = t('app.user_deleted', 'User deleted.'); } elseif ($action !== '') { - throw new Exception(t('app.unknown_action', 'Unknown action:') . ' ' . $action); + throw new Exception(t('app.unknown_action', 'Unknown action: {{action}}', array('action' => $action))); } } } catch (Throwable $e) { @@ -345,20 +286,59 @@ if ($currentUser->isAdmin()) { } header('Content-Type: text/html; charset=utf-8'); -?> - - - - -<?= h(t('app.title', 'Racket sandbox')) ?> - - - +$generatedLinkHtml = ''; -
+if ($issuedLink !== '') { + $generatedLinkHtml = RacketSandboxTemplate::renderFile('partials/index-generated-link.html', array( + 'generated_link_label' => t('app.generated_link', 'Generated link'), + 'issued_link' => $issuedLink, + 'copy_label' => t('app.copy', 'Copy'), + 'copied_label' => t('app.copied', 'Copied'), + )); +} - 0) { + $promptOptionsHtml = ''; + + foreach ($bootstrapPrompts as $prompt) { + $promptOptionsHtml .= RacketSandboxTemplate::renderFile('partials/select-option.html', array( + 'value' => $prompt['id'], + 'selected' => '', + 'label' => $prompt['name'], + )) . "\n"; + } + + $promptToolHtml = RacketSandboxTemplate::renderFile('partials/index-prompt-select.html', array( + 'select_prompt_label' => t('app.select_prompt', 'Select prompt'), + 'prompt_options_html' => $promptOptionsHtml, + 'copy_label' => t('app.copy', 'Copy'), + 'copied_label' => t('app.copied', 'Copied'), + 'copy_full_prompt_label' => t('app.copy_full_prompt', 'Copy full prompt'), + 'bootstrap_prompt_json' => json_encode(array( + 'link' => $issuedLink, + 'prompts' => $bootstrapPrompts, + ), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT), + )); + } else { + $promptToolHtml = RacketSandboxTemplate::renderFile('partials/paragraph.html', array( + 'class' => 'small', + 'text' => t('app.no_bootstrap_prompts', 'No personal prompts are available for this language. Copy a default prompt first from prompt management.'), + )); + } + + $promptPanelHtml = RacketSandboxTemplate::renderFile('partials/index-prompt-tool.html', array( + 'copy_full_prompt_label' => t('app.copy_full_prompt', 'Copy full prompt'), + 'bootstrap_prompt_help' => t('app.bootstrap_prompt_help', 'Choose one of your prompts. The placeholder {{bootstrap-racket-link}} is replaced by the generated bootstrap link.'), + 'prompt_tool_html' => $promptToolHtml, + )); +} + +$headerHtml = app_header_html(array( 'title' => t('app.title', 'Racket sandbox'), 'nav_items' => $headerNavItems, 'user' => $currentUser, @@ -373,98 +353,20 @@ render_app_header(array( 'message' => $message, 'error' => $error, )); -?> -
- -
-

- -
- - - - -
-
- -
- -
- - - - - +echo RacketSandboxTemplate::renderFile('index.html', array( + 'language' => $language, + 'language_url' => rawurlencode($language), + 'title' => t('app.title', 'Racket sandbox'), + 'header_html' => $headerHtml, + 'bootstrap_link_label' => t('app.bootstrap_link', 'Bootstrap link'), + 'generate_bootstrap_link_label' => t('app.generate_bootstrap_link', 'Generate bootstrap link'), + 'bootstrap_result_class' => $issuedLink !== '' ? 'has-prompt' : '', + 'ttl_minutes_label' => t('app.ttl_minutes', 'TTL in minutes'), + 'bootstrap_ttl_minutes' => $bootstrapTtlMinutes, + 'bootstrap_ttl_min_minutes' => BOOTSTRAP_TTL_MIN_MINUTES, + 'bootstrap_ttl_max_minutes' => BOOTSTRAP_TTL_MAX_MINUTES, + 'ttl_range_help' => t('app.ttl_range_help', 'Allowed range: 30 minutes to 8 hours.'), + 'generated_link_html' => $generatedLinkHtml, + 'prompt_panel_html' => $promptPanelHtml, +)); diff --git a/login.php b/login.php index 9c99369..d66d7f0 100644 --- a/login.php +++ b/login.php @@ -4,21 +4,14 @@ */ require_once __DIR__ . '/private/auth.php'; +require_once __DIR__ . '/private/Template.php'; -ini_set('display_errors', '1'); -ini_set('display_startup_errors', '1'); -ini_set('log_errors', '1'); -error_reporting(E_ALL); +require_once __DIR__ . '/config/reporting.php'; $auth = new RacketSandboxAuth(__DIR__ . '/data/racket-sandbox.sqlite'); $error = ''; -function h($s) -{ - return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); -} - function detect_login_language($supported, $fallback) { $header = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''; @@ -61,26 +54,10 @@ function detect_login_language($supported, $fallback) } $pageTitle = 'Racket ChatGPT Agent Sandbox Creator'; -$texts = array( - 'en' => array( - 'email' => 'Email address', - 'password' => 'Password', - 'login' => 'Login', - 'account_title' => 'Want to try it?', - 'account_text' => 'If you would like an account to try the sandbox, please request one from Hans Dijkema through the Racket Discourse pages.', - 'account_link' => 'Go to Racket Discourse', - ), - 'nl' => array( - 'email' => 'E-mailadres', - 'password' => 'Wachtwoord', - 'login' => 'Inloggen', - 'account_title' => 'Wil je het eens proberen?', - 'account_text' => 'Als je een account wilt om de sandbox eens uit te proberen, doe dan een verzoek aan Hans Dijkema via de Racket Discourse-pagina\'s.', - 'account_link' => 'Naar Racket Discourse', - ), -); +$templateData = RacketSandboxTemplate::dataFile('login.html'); +$texts = $templateData['translations'] ?? array(); $language = detect_login_language($texts, 'en'); -$styleVersion = @filemtime(__DIR__ . '/styles.css') ?: time(); +$styleVersion = @filemtime(__DIR__ . '/css/styles.css') ?: time(); if ($auth->currentUser() !== null && $_SERVER['REQUEST_METHOD'] !== 'POST') { header('Location: /'); @@ -98,46 +75,24 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } header('Content-Type: text/html; charset=utf-8'); -?> - - - - -<?= h($pageTitle) ?> - - - -
-
-

+$loginText = $texts[$language]; +$errorHtml = $error === '' + ? '' + : RacketSandboxTemplate::renderFile('partials/message.html', array( + 'class' => 'error', + 'message' => $error, + )); - -
- - -
- - - - - - -
-
- - -
- - - +echo RacketSandboxTemplate::renderFile('login.html', array( + 'language' => $language, + 'page_title' => $pageTitle, + 'style_version' => $styleVersion, + 'error_html' => $errorHtml, + 'email_label' => $loginText['email'], + 'password_label' => $loginText['password'], + 'login_label' => $loginText['login'], + 'account_title' => $loginText['account_title'], + 'account_text' => $loginText['account_text'], + 'account_link' => $loginText['account_link'], +)); diff --git a/package.php b/package.php index 9fcd98f..8a07830 100644 --- a/package.php +++ b/package.php @@ -23,10 +23,7 @@ * - Eén next-id per gegenereerde HTML-pagina. */ -ini_set('display_errors', '1'); -ini_set('display_startup_errors', '1'); -ini_set('log_errors', '1'); -error_reporting(E_ALL); +require_once __DIR__ . '/config/reporting.php'; require_once __DIR__ . '/private/nexttoken.php'; @@ -40,6 +37,7 @@ require_once __DIR__ . '/private/b64parts.php'; require_once __DIR__ . '/private/base64config.php'; require_once __DIR__ . '/private/lib/catalog-http.php'; require_once __DIR__ . '/private/lib/racket-data.php'; +require_once __DIR__ . '/private/Template.php'; define('DATA_DIR', __DIR__ . '/data'); define('CATALOG_PACKAGE_BASE', 'https://pkgs.racket-lang.org/pkg/'); @@ -123,11 +121,10 @@ function text_response($text, $status = 200) function fail_html($message, $status = 500) { html_response( - '' . - 'Package error' . - '

Package error

' . - '
' . h($message) . '
' . - '', + RacketSandboxTemplate::renderFile('protocol-error.html', array( + 'title' => 'Package error', + 'message' => $message, + )), $status ); } @@ -362,79 +359,46 @@ function serve_package_page() 'part' => $n, )); - $rows .= - '' . - '' . h($n) . '' . - '' . h((string)$part['base64_bytes']) . '' . - '' . h($url) . '' . - '' . "\n"; + $rows .= RacketSandboxTemplate::renderFile('partials/package-part-row.html', array( + 'number' => $n, + 'base64_bytes' => (string)$part['base64_bytes'], + 'url' => $url, + )) . "\n"; } - html_response(' - - - -Package ' . h($package) . ' - - - + $sourceRows = ''; + $sourceValues = array( + 'catalog source' => $info['source'], + 'repo url' => $info['repo_url'], + 'fetch status' => $zipInfo['status'] ?? '', + 'default branch' => $zipInfo['default_branch'] ?? '', + 'head sha' => $zipInfo['head_sha'] ?? '', + 'zip file' => $zipInfo['zip_file'] ?? '', + 'zip bytes' => (string)($zipInfo['zip_bytes'] ?? ''), + 'zip sha256' => $zipInfo['zip_sha256'] ?? '', + 'parts status' => $manifest['parts_status'] ?? '', + 'max base64 part size' => '' . h((string)PACKAGE_ZIP_MAX_BASE64_KB) . ' KiB (' . h((string)PACKAGE_ZIP_MAX_BASE64_BYTES) . ' bytes)', + 'binary chunk size' => '' . h((string)($manifest['binary_chunk_bytes'] ?? '')) . ' bytes', + 'part count' => (string)$manifest['part_count'], + 'next id' => $NEXT_ID, + ); -

Package ' . h($package) . '

+ foreach ($sourceValues as $label => $value) { + $valueHtml = strpos((string)$value, '') !== false + ? (string)$value + : '' . h((string)$value) . ''; -

-Deze pagina is HTML. Alle part-links hieronder geven text/plain -met base64-inhoud terug. Dezelfde next wordt gebruikt voor alle -part-links op deze pagina. -

+ $sourceRows .= RacketSandboxTemplate::renderFile('partials/package-source-row.html', array( + 'label' => $label, + 'value_html' => $valueHtml, + )) . "\n"; + } -

Bron

- - - - - - - - - - - - - - - -
catalog source' . h($info['source']) . '
repo url' . h($info['repo_url']) . '
fetch status' . h($zipInfo['status'] ?? '') . '
default branch' . h($zipInfo['default_branch'] ?? '') . '
head sha' . h($zipInfo['head_sha'] ?? '') . '
zip file' . h($zipInfo['zip_file'] ?? '') . '
zip bytes' . h((string)($zipInfo['zip_bytes'] ?? '')) . '
zip sha256' . h($zipInfo['zip_sha256'] ?? '') . '
parts status' . h($manifest['parts_status'] ?? '') . '
max base64 part size' . h((string)PACKAGE_ZIP_MAX_BASE64_KB) . ' KiB (' . h((string)PACKAGE_ZIP_MAX_BASE64_BYTES) . ' bytes)
binary chunk size' . h((string)($manifest['binary_chunk_bytes'] ?? '')) . ' bytes
part count' . h((string)$manifest['part_count']) . '
next id' . h($NEXT_ID) . '
- -

Base64 parts

- - - - - - - - - - -' . $rows . ' - -
partbase64 bytestext/plain URL
- -

Reconstructie in de sandbox

- -
-# download alle links als:
-# ' . h($package) . '.part.000001.b64
-# ' . h($package) . '.part.000002.b64
-# enz.
-
-cat ' . h($package) . '.part.*.b64 > ' . h($package) . '.zip.b64
-base64 -d ' . h($package) . '.zip.b64 > ' . h($package) . '.zip
-raco pkg install --auto ./' . h($package) . '.zip
-
- - -'); + html_response(RacketSandboxTemplate::renderFile('package.html', array( + 'package' => $package, + 'source_rows_html' => $sourceRows, + 'part_rows_html' => $rows, + ))); } function serve_package_part() diff --git a/private/Template.php b/private/Template.php new file mode 100644 index 0000000..904d4ec --- /dev/null +++ b/private/Template.php @@ -0,0 +1,100 @@ + $chunks[0], + 'data' => $chunks[1] ?? '', + ); + } +} diff --git a/private/auth.php b/private/auth.php index 17ce216..deb4aa9 100644 --- a/private/auth.php +++ b/private/auth.php @@ -14,6 +14,8 @@ * Outside code should not inspect DB rows directly. */ +require_once __DIR__ . '/Template.php'; + class RacketSandboxAuthException extends Exception { } @@ -696,7 +698,7 @@ class RacketSandboxAuth $this->messageHtml( 'Login required', 'Please log in to continue.', - '

Login

' + RacketSandboxTemplate::renderFile('partials/login-link.html', array('login_label' => 'Login')) ); } @@ -705,18 +707,12 @@ class RacketSandboxAuth http_response_code(200); header('Content-Type: text/html; charset=utf-8'); - echo ' - - - -' . htmlspecialchars($title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . ' - - -

' . htmlspecialchars($title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '

-

' . htmlspecialchars($message, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '

-' . $extra . ' - -'; + echo RacketSandboxTemplate::renderFile('simple-message.html', array( + 'language' => 'en', + 'title' => $title, + 'message' => $message, + 'extra_html' => $extra, + )); exit; } } diff --git a/private/header.php b/private/header.php index 6fa928b..74e5f5e 100644 --- a/private/header.php +++ b/private/header.php @@ -5,12 +5,19 @@ * Shared page header renderer for logged-in application pages. */ +require_once __DIR__ . '/Template.php'; + function app_header_h($s) { - return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + return RacketSandboxTemplate::escape($s); } function render_app_header($options) +{ + echo app_header_html($options); +} + +function app_header_html($options) { $title = (string)($options['title'] ?? ''); $navItems = $options['nav_items'] ?? array(); @@ -26,69 +33,114 @@ function render_app_header($options) $logoutLabel = (string)($options['logout_label'] ?? ''); $message = (string)($options['message'] ?? ''); $error = (string)($options['error'] ?? ''); - ?> - - $title, + 'nav_html' => app_header_nav_html($navItems), + 'logout_html' => app_header_logout_html($logoutAction, $logoutLabel), + 'user_html' => app_header_user_html($user, $userPrefix, $adminLabel), + 'language_html' => app_header_language_html($languageLabel, $language, $languages, $languageAction, $languageHidden), + 'message_html' => app_header_status_html('message', $message), + 'error_html' => app_header_status_html('error', $error), + )); +} + +function app_header_status_html($class, $message) +{ + if ($message === '') { + return ''; + } + + return RacketSandboxTemplate::renderFile('partials/message.html', array( + 'class' => $class, + 'message' => $message, + )); +} + +function app_header_nav_html($navItems) +{ + $html = ''; + + foreach ($navItems as $item) { + if (!empty($item['separator_before'])) { + $html .= RacketSandboxTemplate::renderFile('partials/header-separator.html', array()) . "\n"; + } + + if (!empty($item['active'])) { + $html .= RacketSandboxTemplate::renderFile('partials/header-nav-active.html', array( + 'label' => $item['label'] ?? '', + )) . "\n"; + continue; + } + + $html .= RacketSandboxTemplate::renderFile('partials/header-nav-link.html', array( + 'url' => $item['url'] ?? '#', + 'label' => $item['label'] ?? '', + )) . "\n"; + } + + return $html; +} + +function app_header_logout_html($logoutAction, $logoutLabel) +{ + if ($logoutAction === '' || $logoutLabel === '') { + return ''; + } + + return RacketSandboxTemplate::renderFile('partials/header-logout.html', array( + 'logout_action' => $logoutAction, + 'logout_label' => $logoutLabel, + )) . "\n"; +} + +function app_header_user_html($user, $userPrefix, $adminLabel) +{ + if ($user === null) { + return ''; + } + + $adminHtml = ''; + + if ($user->isAdmin()) { + $adminHtml = RacketSandboxTemplate::renderFile('partials/header-admin-badge.html', array( + 'admin_label' => $adminLabel, + )); + } + + return RacketSandboxTemplate::renderFile('partials/header-user.html', array( + 'user_prefix' => $userPrefix, + 'display_name' => $user->displayName(), + 'admin_html' => $adminHtml, + )) . "\n"; +} + +function app_header_language_html($languageLabel, $language, $languages, $languageAction, $languageHidden) +{ + $hiddenInputsHtml = ''; + + foreach ($languageHidden as $name => $value) { + $hiddenInputsHtml .= RacketSandboxTemplate::renderFile('partials/header-hidden-input.html', array( + 'name' => $name, + 'value' => $value, + )) . "\n"; + } + + $languageOptionsHtml = ''; + + foreach ($languages as $lang => $label) { + $selected = $lang === $language ? ' selected' : ''; + $languageOptionsHtml .= RacketSandboxTemplate::renderFile('partials/select-option.html', array( + 'value' => $lang, + 'selected' => $selected, + 'label' => $label, + )) . "\n"; + } + + return RacketSandboxTemplate::renderFile('partials/header-language.html', array( + 'language_action' => $languageAction, + 'hidden_inputs_html' => $hiddenInputsHtml, + 'language_label' => $languageLabel, + 'language_options_html' => $languageOptionsHtml, + )) . "\n"; } diff --git a/private/languagestore.php b/private/languagestore.php index 26e8dbe..e37a0b9 100644 --- a/private/languagestore.php +++ b/private/languagestore.php @@ -79,6 +79,43 @@ class LanguageStore return $fallback !== null ? (string)$fallback : $key; } + public function translateFormat($key, $language, $values = array(), $fallback = null) + { + return self::formatText($this->translate($key, $language, $fallback), $values); + } + + public static function formatText($text, $values, $maxDepth = 8) + { + if (!is_array($values) || count($values) === 0) { + return (string)$text; + } + + $text = (string)$text; + + for ($depth = 0; $depth < $maxDepth; $depth++) { + $changed = false; + + $next = preg_replace_callback('/\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/', function ($match) use ($values, &$changed) { + $name = $match[1]; + + if (!array_key_exists($name, $values)) { + return $match[0]; + } + + $changed = true; + return (string)$values[$name]; + }, $text); + + $text = $next; + + if (!$changed) { + break; + } + } + + return $text; + } + public function setTranslation($key, $language, $text) { $key = $this->safeKey($key); diff --git a/private/viewdata.php b/private/viewdata.php new file mode 100644 index 0000000..e9ac082 --- /dev/null +++ b/private/viewdata.php @@ -0,0 +1,11 @@ +seedDefaults(RacketSandboxTemplate::translationsFile($template)); +} diff --git a/private/views/app-header.html b/private/views/app-header.html new file mode 100644 index 0000000..7ebf425 --- /dev/null +++ b/private/views/app-header.html @@ -0,0 +1,15 @@ + diff --git a/private/views/bootstrap-racket.html b/private/views/bootstrap-racket.html new file mode 100644 index 0000000..1a933a2 --- /dev/null +++ b/private/views/bootstrap-racket.html @@ -0,0 +1,69 @@ + + + + +Racket bootstrap + + + + +

Racket bootstrap

+ +

+racket.zip is vooraf via de configuratiepagina gesplitst naar +parts in de map data. +

+ +

+Package index: Racket package index +

+ +

+Bronbestand: config/racket.zip
+Bronbestand bytes: {{zip_size}}
+Maximale base64 part-grootte: {{max_base64_kb}} KiB ({{max_base64_bytes}} bytes)
+Binaire chunk-grootte: {{binary_chunk_bytes}} bytes
+Aantal parts: {{part_count}}
+Parts gemaakt op: {{created_at}}
+next-id voor alle links op deze pagina: {{next_id}} +

+ +

+Elke link hieronder geeft text/plain met de +base64-representatie van een binair part. +De URL bevat alleen een nummer, geen bestandsnaam en geen extensie. +

+ + + + + + + + + + + +{{{part_rows_html}}} + +
#partnummerbytesbase64 text/plain URL
+ +

Reconstructie in de sandbox

+ +

+Decodeer ieder base64-part afzonderlijk naar een binair part. Plak daarna de +binaire parts in numerieke volgorde aan elkaar. +

+ +
+base64 -d part-000001.txt > part-000001
+base64 -d part-000002.txt > part-000002
+base64 -d part-000003.txt > part-000003
+# enzovoort
+
+cat part-* > racket.zip
+unzip racket.zip -d /tmp/racket
+
+ + + diff --git a/private/views/config.html b/private/views/config.html new file mode 100644 index 0000000..fcb027c --- /dev/null +++ b/private/views/config.html @@ -0,0 +1,229 @@ + + + + +{{title}} + + + + +
+ +{{{header_html}}} + +
+ +
+

{{download_settings_label}}

+ +
+ + + + +
+ +

{{chunk_size_hint}}

+ + + + + + + + + + + + + + + + + + + + + + +
{{racket_zip_chunk_label}}{{{racket_zip_base64_chunk_size}}}{{{racket_zip_effective_binary_chunk}}}
{{package_zip_chunk_label}}{{{package_zip_base64_chunk_size}}}{{{package_zip_effective_binary_chunk}}}
{{racket_zip_source_label}}config/racket.zip{{{racket_zip_file_size}}}
{{racket_parts_label}}{{racket_part_count}}{{racket_parts_status}}
+
+ +
+

{{maintenance_label}}

+ +
+{{next_tokens_label}} + +
+ + +
+ +

{{cleanup_help}}

+ +

{{current_next_tokens_label}}

+ +{{{current_tokens_html}}} +
+
+ +
+
+ + +=== +{ + "translations": { + "app.title": { + "en": "Racket sandbox", + "nl": "Racket sandbox" + }, + "app.manage_prompts": { + "en": "Manage prompts", + "nl": "Prompts beheren" + }, + "app.user_management": { + "en": "User management", + "nl": "Gebruikersbeheer" + }, + "app.configuration": { + "en": "Configuration", + "nl": "Configuratie" + }, + "app.logout": { + "en": "Logout", + "nl": "Uitloggen" + }, + "app.language": { + "en": "Language", + "nl": "Taal" + }, + "app.logged_in_as": { + "en": "Logged in as:", + "nl": "Ingelogd als:" + }, + "app.admin": { + "en": "Admin", + "nl": "Admin" + }, + "app.back_to_sandbox": { + "en": "Back to Racket sandbox", + "nl": "Terug naar Racket sandbox" + }, + "app.download_settings": { + "en": "Download settings", + "nl": "Downloadinstellingen" + }, + "app.maintenance": { + "en": "Maintenance", + "nl": "Onderhoud" + }, + "app.next_tokens": { + "en": "Next tokens", + "nl": "Next-tokens" + }, + "app.current_next_tokens": { + "en": "Next tokens", + "nl": "Next-tokens" + }, + "app.token": { + "en": "Token", + "nl": "Token" + }, + "app.created_at": { + "en": "Created at", + "nl": "Aangemaakt op" + }, + "app.expires_at": { + "en": "Expires at", + "nl": "Verloopt op" + }, + "app.no_current_next_tokens": { + "en": "No next tokens.", + "nl": "Geen next-tokens." + }, + "app.remove_expired_tokens": { + "en": "Remove expired next tokens", + "nl": "Verlopen next-tokens verwijderen" + }, + "app.racket_zip_chunk_kb": { + "en": "Racket installation max base64 chunk size (KiB)", + "nl": "Maximale base64-chunkgrootte Racket-installatie (KiB)" + }, + "app.package_zip_chunk_kb": { + "en": "Package/module max base64 chunk size (KiB)", + "nl": "Maximale base64-chunkgrootte packages/modules (KiB)" + }, + "app.save_configuration": { + "en": "Save configuration", + "nl": "Configuratie opslaan" + }, + "app.configuration_saved": { + "en": "Configuration saved.", + "nl": "Configuratie opgeslagen." + }, + "app.configuration_saved_with_parts": { + "en": "Configuration saved. Racket installation parts regenerated: {{count}}", + "nl": "Configuratie opgeslagen. Racket-installatie parts opnieuw gemaakt: {{count}}" + }, + "app.racket_parts_regenerated": { + "en": "Racket installation parts regenerated: {{count}}", + "nl": "Racket-installatie parts opnieuw gemaakt: {{count}}" + }, + "app.chunk_size_hint_v2": { + "en": "Values are maximum base64 payload sizes in KiB. A {{chunk_size}} KiB binary chunk becomes {{base64_chunk_size}} KiB base64. Racket installation parts are regenerated when this configuration is saved.", + "nl": "Waarden zijn maximale base64-payloadgroottes in KiB. Een binaire chunk van {{chunk_size}} KiB wordt {{base64_chunk_size}} KiB base64. Racket-installatie parts worden opnieuw gemaakt wanneer deze configuratie wordt opgeslagen." + }, + "app.effective_binary_chunk": { + "en": "Effective binary chunk", + "nl": "Effectieve binaire chunk" + }, + "app.effective_binary_chunk_bytes": { + "en": "Effective binary chunk: {{bytes}} bytes", + "nl": "Effectieve binaire chunk: {{bytes}} bytes" + }, + "app.base64_chunk_size_kib": { + "en": "{{size}} KiB", + "nl": "{{size}} KiB" + }, + "app.file_size_bytes": { + "en": "{{bytes}} bytes", + "nl": "{{bytes}} bytes" + }, + "app.racket_zip_source": { + "en": "Racket installation source", + "nl": "Bronbestand Racket-installatie" + }, + "app.racket_parts": { + "en": "Racket installation parts", + "nl": "Racket-installatie parts" + }, + "app.racket_parts_current": { + "en": "current", + "nl": "actueel" + }, + "app.racket_parts_current_with_date": { + "en": "current, {{created_at}}", + "nl": "actueel, {{created_at}}" + }, + "app.racket_parts_missing": { + "en": "missing or outdated; save configuration to regenerate", + "nl": "ontbreken of verouderd; sla configuratie op om opnieuw te maken" + }, + "app.expired_tokens_removed": { + "en": "Expired next tokens removed: {{count}}", + "nl": "Verlopen next-tokens verwijderd: {{count}}" + }, + "app.cleanup_help": { + "en": "Expired links should return an outdated information message to the AI agent. Cleanup only removes old token rows from SQLite.", + "nl": "Verlopen links moeten een melding over verouderde informatie aan de AI-agent teruggeven. Opruimen verwijdert alleen oude tokenrijen uit SQLite." + } + } +} diff --git a/private/views/index.html b/private/views/index.html new file mode 100644 index 0000000..99b685c --- /dev/null +++ b/private/views/index.html @@ -0,0 +1,237 @@ + + + + +{{title}} + + + + +
+ +{{{header_html}}} + +
+ +
+

{{bootstrap_link_label}}

+ +
+{{generate_bootstrap_link_label}} + +
+
+
+ + + +

{{ttl_range_help}}

+ + +
+ +{{{generated_link_html}}} +
+ +{{{prompt_panel_html}}} +
+ +
+
+ +
+ +
+ + + + + +=== +{ + "translations": { + "app.title": { + "en": "Racket sandbox", + "nl": "Racket sandbox" + }, + "app.manage_prompts": { + "en": "Manage prompts", + "nl": "Prompts beheren" + }, + "app.logout": { + "en": "Logout", + "nl": "Uitloggen" + }, + "app.language": { + "en": "Language", + "nl": "Taal" + }, + "app.logged_in_as": { + "en": "Logged in as:", + "nl": "Ingelogd als:" + }, + "app.admin": { + "en": "admin", + "nl": "admin" + }, + "app.bootstrap_link": { + "en": "Bootstrap link", + "nl": "Bootstraplink" + }, + "app.generate_bootstrap_link": { + "en": "Generate bootstrap link", + "nl": "Bootstraplink genereren" + }, + "app.ttl_minutes": { + "en": "TTL in minutes", + "nl": "TTL in minuten" + }, + "app.ttl_range_help": { + "en": "Allowed range: 30 minutes to 8 hours.", + "nl": "Toegestaan bereik: 30 minuten tot 8 uur." + }, + "app.generated_link": { + "en": "Generated link", + "nl": "Gegenereerde link" + }, + "app.copy": { + "en": "Copy", + "nl": "Kopieer" + }, + "app.copied": { + "en": "Copied", + "nl": "Gekopieerd" + }, + "app.generated_link_help": { + "en": "Give this link to the AI agent. The agent should start from this link and then only follow links from the generated HTML pages.", + "nl": "Geef deze link aan de AI-agent. De agent moet vanaf deze link starten en daarna alleen links volgen vanuit de gegenereerde HTML-paginas." + }, + "app.select_prompt": { + "en": "Select prompt", + "nl": "Prompt selecteren" + }, + "app.copy_full_prompt": { + "en": "Copy full prompt", + "nl": "Volledige prompt kopieren" + }, + "app.bootstrap_prompt_help": { + "en": "Choose one of your prompts. The placeholder {{bootstrap-racket-link}} is replaced by the generated bootstrap link.", + "nl": "Kies een van je prompts. De placeholder {{bootstrap-racket-link}} wordt vervangen door de gegenereerde bootstraplink." + }, + "app.no_bootstrap_prompts": { + "en": "No personal prompts are available for this language. Copy a default prompt first from prompt management.", + "nl": "Er zijn geen persoonlijke prompts beschikbaar voor deze taal. Kopieer eerst een standaardprompt vanuit promptbeheer." + }, + "app.user_management": { + "en": "User management", + "nl": "Gebruikersbeheer" + }, + "app.configuration": { + "en": "Configuration", + "nl": "Configuratie" + }, + "app.user_management_help": { + "en": "Users are registered manually by email address, full name and password. This page only manages existing users.", + "nl": "Gebruikers worden handmatig geregistreerd met e-mailadres, volledige naam en wachtwoord. Deze pagina beheert alleen bestaande gebruikers." + }, + "app.id": { + "en": "ID", + "nl": "ID" + }, + "app.full_name": { + "en": "Full name", + "nl": "Volledige naam" + }, + "app.email": { + "en": "Email", + "nl": "E-mail" + }, + "app.enabled": { + "en": "Enabled", + "nl": "Ingeschakeld" + }, + "app.created": { + "en": "Created", + "nl": "Gemaakt" + }, + "app.last_login": { + "en": "Last login", + "nl": "Laatste login" + }, + "app.actions": { + "en": "Actions", + "nl": "Acties" + }, + "app.yes": { + "en": "yes", + "nl": "ja" + }, + "app.no": { + "en": "no", + "nl": "nee" + }, + "app.save_flags": { + "en": "Save flags", + "nl": "Vlaggen opslaan" + }, + "app.new_password": { + "en": "New password", + "nl": "Nieuw wachtwoord" + }, + "app.change_password": { + "en": "Change password", + "nl": "Wachtwoord wijzigen" + }, + "app.delete_user": { + "en": "Delete user", + "nl": "Gebruiker verwijderen" + }, + "app.delete_user_confirm": { + "en": "Delete user", + "nl": "Gebruiker verwijderen" + }, + "app.cannot_delete_self": { + "en": "You cannot delete your own account.", + "nl": "Je kunt je eigen account niet verwijderen." + }, + "app.bootstrap_link_issued": { + "en": "Bootstrap link issued.", + "nl": "Bootstraplink aangemaakt." + }, + "app.password_changed_for": { + "en": "Password changed for: {{email}}", + "nl": "Wachtwoord gewijzigd voor: {{email}}" + }, + "app.user_flags_updated": { + "en": "User flags updated.", + "nl": "Gebruikersvlaggen bijgewerkt." + }, + "app.user_deleted": { + "en": "User deleted.", + "nl": "Gebruiker verwijderd." + }, + "app.admin_rights_required": { + "en": "Admin rights required.", + "nl": "Adminrechten vereist." + }, + "app.cannot_remove_own_admin": { + "en": "You cannot remove your own admin rights.", + "nl": "Je kunt je eigen adminrechten niet verwijderen." + }, + "app.cannot_disable_self": { + "en": "You cannot disable your own account.", + "nl": "Je kunt je eigen account niet uitschakelen." + }, + "app.unknown_action": { + "en": "Unknown action: {{action}}", + "nl": "Onbekende actie: {{action}}" + } + } +} diff --git a/private/views/login.html b/private/views/login.html new file mode 100644 index 0000000..f609fa7 --- /dev/null +++ b/private/views/login.html @@ -0,0 +1,61 @@ + + + + +{{page_title}} + + + + +
+ + + +
+ + + +=== +{ + "translations": { + "en": { + "email": "Email address", + "password": "Password", + "login": "Login", + "account_title": "Want to try it?", + "account_text": "If you would like an account to try the sandbox, please request one from Hans Dijkema through the Racket Discourse pages.", + "account_link": "Go to Racket Discourse" + }, + "nl": { + "email": "E-mailadres", + "password": "Wachtwoord", + "login": "Inloggen", + "account_title": "Wil je het eens proberen?", + "account_text": "Als je een account wilt om de sandbox eens uit te proberen, doe dan een verzoek aan Hans Dijkema via de Racket Discourse-pagina's.", + "account_link": "Naar Racket Discourse" + } + } +} diff --git a/private/views/package.html b/private/views/package.html new file mode 100644 index 0000000..c998d1b --- /dev/null +++ b/private/views/package.html @@ -0,0 +1,53 @@ + + + + +Package {{package}} + + + + +

Package {{package}}

+ +

+Deze pagina is HTML. Alle part-links hieronder geven text/plain +met base64-inhoud terug. Dezelfde next wordt gebruikt voor alle +part-links op deze pagina. +

+ +

Bron

+ + +{{{source_rows_html}}} +
+ +

Base64 parts

+ + + + + + + + + + +{{{part_rows_html}}} + +
partbase64 bytestext/plain URL
+ +

Reconstructie in de sandbox

+ +
+# download alle links als:
+# {{package}}.part.000001.b64
+# {{package}}.part.000002.b64
+# enz.
+
+cat {{package}}.part.*.b64 > {{package}}.zip.b64
+base64 -d {{package}}.zip.b64 > {{package}}.zip
+raco pkg install --auto ./{{package}}.zip
+
+ + + diff --git a/private/views/partials/config-token-row.html b/private/views/partials/config-token-row.html new file mode 100644 index 0000000..c89b333 --- /dev/null +++ b/private/views/partials/config-token-row.html @@ -0,0 +1,5 @@ + +{{token}} +{{created_at}} +{{expires_at}} + diff --git a/private/views/partials/config-token-table.html b/private/views/partials/config-token-table.html new file mode 100644 index 0000000..eace19a --- /dev/null +++ b/private/views/partials/config-token-table.html @@ -0,0 +1,8 @@ + + + + + + +{{{token_rows_html}}} +
{{token_label}}{{created_at_label}}{{expires_at_label}}
diff --git a/private/views/partials/header-admin-badge.html b/private/views/partials/header-admin-badge.html new file mode 100644 index 0000000..b3dd8b8 --- /dev/null +++ b/private/views/partials/header-admin-badge.html @@ -0,0 +1 @@ +({{admin_label}}) diff --git a/private/views/partials/header-hidden-input.html b/private/views/partials/header-hidden-input.html new file mode 100644 index 0000000..1b02fd6 --- /dev/null +++ b/private/views/partials/header-hidden-input.html @@ -0,0 +1 @@ + diff --git a/private/views/partials/header-language.html b/private/views/partials/header-language.html new file mode 100644 index 0000000..b5b65e1 --- /dev/null +++ b/private/views/partials/header-language.html @@ -0,0 +1,9 @@ +
+{{{hidden_inputs_html}}} + +
diff --git a/private/views/partials/header-logout.html b/private/views/partials/header-logout.html new file mode 100644 index 0000000..fed960b --- /dev/null +++ b/private/views/partials/header-logout.html @@ -0,0 +1,5 @@ + +
+ + +
diff --git a/private/views/partials/header-nav-active.html b/private/views/partials/header-nav-active.html new file mode 100644 index 0000000..c11e825 --- /dev/null +++ b/private/views/partials/header-nav-active.html @@ -0,0 +1 @@ +{{label}} diff --git a/private/views/partials/header-nav-link.html b/private/views/partials/header-nav-link.html new file mode 100644 index 0000000..7cb7d47 --- /dev/null +++ b/private/views/partials/header-nav-link.html @@ -0,0 +1 @@ +{{label}} diff --git a/private/views/partials/header-separator.html b/private/views/partials/header-separator.html new file mode 100644 index 0000000..7215506 --- /dev/null +++ b/private/views/partials/header-separator.html @@ -0,0 +1 @@ + diff --git a/private/views/partials/header-user.html b/private/views/partials/header-user.html new file mode 100644 index 0000000..b6d9337 --- /dev/null +++ b/private/views/partials/header-user.html @@ -0,0 +1,6 @@ + + +{{user_prefix}} +{{display_name}} +{{{admin_html}}} + diff --git a/private/views/partials/index-generated-link.html b/private/views/partials/index-generated-link.html new file mode 100644 index 0000000..7e477fc --- /dev/null +++ b/private/views/partials/index-generated-link.html @@ -0,0 +1,11 @@ +

{{generated_link_label}}

+ diff --git a/private/views/partials/index-prompt-select.html b/private/views/partials/index-prompt-select.html new file mode 100644 index 0000000..f3681ac --- /dev/null +++ b/private/views/partials/index-prompt-select.html @@ -0,0 +1,21 @@ +
+ + + + + +
+ diff --git a/private/views/partials/index-prompt-tool.html b/private/views/partials/index-prompt-tool.html new file mode 100644 index 0000000..898459b --- /dev/null +++ b/private/views/partials/index-prompt-tool.html @@ -0,0 +1,6 @@ +
+

{{copy_full_prompt_label}}

+

{{bootstrap_prompt_help}}

+ +{{{prompt_tool_html}}} +
diff --git a/private/views/partials/login-link.html b/private/views/partials/login-link.html new file mode 100644 index 0000000..9f77bec --- /dev/null +++ b/private/views/partials/login-link.html @@ -0,0 +1 @@ +

{{login_label}}

diff --git a/private/views/partials/message.html b/private/views/partials/message.html new file mode 100644 index 0000000..dd5cfec --- /dev/null +++ b/private/views/partials/message.html @@ -0,0 +1 @@ +
{{message}}
diff --git a/private/views/partials/package-index-row.html b/private/views/partials/package-index-row.html new file mode 100644 index 0000000..e716974 --- /dev/null +++ b/private/views/partials/package-index-row.html @@ -0,0 +1,6 @@ + +{{index}} + +{{name}} + + diff --git a/private/views/partials/package-part-row.html b/private/views/partials/package-part-row.html new file mode 100644 index 0000000..3dfc153 --- /dev/null +++ b/private/views/partials/package-part-row.html @@ -0,0 +1,5 @@ + +{{number}} +{{base64_bytes}} +{{url}} + diff --git a/private/views/partials/package-source-row.html b/private/views/partials/package-source-row.html new file mode 100644 index 0000000..e45925c --- /dev/null +++ b/private/views/partials/package-source-row.html @@ -0,0 +1 @@ +{{label}}{{{value_html}}} diff --git a/private/views/partials/paragraph.html b/private/views/partials/paragraph.html new file mode 100644 index 0000000..bb5c2c2 --- /dev/null +++ b/private/views/partials/paragraph.html @@ -0,0 +1 @@ +

{{text}}

diff --git a/private/views/partials/prompt-admin-notice.html b/private/views/partials/prompt-admin-notice.html new file mode 100644 index 0000000..4e74800 --- /dev/null +++ b/private/views/partials/prompt-admin-notice.html @@ -0,0 +1,4 @@ +
+{{badge}} +{{hint}} +
diff --git a/private/views/partials/prompt-create-default.html b/private/views/partials/prompt-create-default.html new file mode 100644 index 0000000..7286ee1 --- /dev/null +++ b/private/views/partials/prompt-create-default.html @@ -0,0 +1,15 @@ +
+{{create_default_label}} +
+ + + + + + +
+
diff --git a/private/views/partials/prompt-default-delete.html b/private/views/partials/prompt-default-delete.html new file mode 100644 index 0000000..b95c5d3 --- /dev/null +++ b/private/views/partials/prompt-default-delete.html @@ -0,0 +1,6 @@ +
+ + + +
diff --git a/private/views/partials/prompt-default-item.html b/private/views/partials/prompt-default-item.html new file mode 100644 index 0000000..b57c34d --- /dev/null +++ b/private/views/partials/prompt-default-item.html @@ -0,0 +1,17 @@ +
+ + +
+ + + +
+ +{{{admin_delete_html}}} +
diff --git a/private/views/partials/prompt-personal-item.html b/private/views/partials/prompt-personal-item.html new file mode 100644 index 0000000..9c48be9 --- /dev/null +++ b/private/views/partials/prompt-personal-item.html @@ -0,0 +1,15 @@ +
+ +
+ + + +
+
diff --git a/private/views/partials/racket-part-row.html b/private/views/partials/racket-part-row.html new file mode 100644 index 0000000..1cd2d4f --- /dev/null +++ b/private/views/partials/racket-part-row.html @@ -0,0 +1,6 @@ + +{{index}} +{{number}} +{{size}} +{{url}} + diff --git a/private/views/partials/select-option.html b/private/views/partials/select-option.html new file mode 100644 index 0000000..88ed4c2 --- /dev/null +++ b/private/views/partials/select-option.html @@ -0,0 +1 @@ + diff --git a/private/views/partials/user-delete-form.html b/private/views/partials/user-delete-form.html new file mode 100644 index 0000000..4366904 --- /dev/null +++ b/private/views/partials/user-delete-form.html @@ -0,0 +1,6 @@ +
+ + + +
diff --git a/private/views/partials/user-row.html b/private/views/partials/user-row.html new file mode 100644 index 0000000..933daef --- /dev/null +++ b/private/views/partials/user-row.html @@ -0,0 +1,27 @@ + + +
+ + + + + + + +{{created_at}} +{{last_login_at}} + +
+ +
+
+ + + + +
+ +{{{delete_html}}} +
+ + diff --git a/private/views/partials/user-self-note.html b/private/views/partials/user-self-note.html new file mode 100644 index 0000000..e7ef564 --- /dev/null +++ b/private/views/partials/user-self-note.html @@ -0,0 +1 @@ +

{{cannot_delete_self}}

diff --git a/private/views/prompts.html b/private/views/prompts.html new file mode 100644 index 0000000..5a9acad --- /dev/null +++ b/private/views/prompts.html @@ -0,0 +1,462 @@ + + + + +{{title}} + + + + +
+ +{{{header_html}}} + +
+ + + +
+
+{{select_prompt_label}} +
+ +
+
+
+
{{prompt_label}}
+
+
+ +
+

+
+
+ +
+ + + +
+ + + + + + + + + + + +=== +{ + "translations": { + "prompts.title": { + "en": "Prompt administration", + "nl": "Promptbeheer" + }, + "prompts.back": { + "en": "Back to Racket sandbox", + "nl": "Terug naar Racket sandbox" + }, + "prompts.your_prompts": { + "en": "Your prompts", + "nl": "Jouw prompts" + }, + "prompts.default_admin": { + "en": "Default prompt administration", + "nl": "Standaardpromptbeheer" + }, + "prompts.language": { + "en": "Language", + "nl": "Taal" + }, + "prompts.available_defaults": { + "en": "Available default prompts", + "nl": "Beschikbare standaardprompts" + }, + "prompts.default_admin_badge": { + "en": "Admin default prompts", + "nl": "Admin standaardprompts" + }, + "prompts.default_admin_hint": { + "en": "Defaults are shared with every user and can be copied into personal prompts.", + "nl": "Standaarden worden gedeeld met alle gebruikers en kunnen naar persoonlijke prompts worden gekopieerd." + }, + "prompts.copy_all": { + "en": "Copy all", + "nl": "Alles kopieren" + }, + "prompts.copy": { + "en": "copy", + "nl": "kopieer" + }, + "prompts.delete": { + "en": "delete", + "nl": "verwijder" + }, + "prompts.create_default": { + "en": "Create default prompt", + "nl": "Standaardprompt maken" + }, + "prompts.create_personal": { + "en": "Create personal prompt", + "nl": "Persoonlijke prompt maken" + }, + "prompts.name": { + "en": "Name", + "nl": "Naam" + }, + "prompts.default_key": { + "en": "Default key", + "nl": "Standaardsleutel" + }, + "prompts.prompt_content": { + "en": "Prompt content", + "nl": "Promptinhoud" + }, + "prompts.no_defaults": { + "en": "No default prompts for this language yet.", + "nl": "Nog geen standaardprompts voor deze taal." + }, + "prompts.no_personal": { + "en": "No personal prompts yet for this language.", + "nl": "Nog geen persoonlijke prompts voor deze taal." + }, + "prompts.select_prompt": { + "en": "Select a prompt on the left to view it.", + "nl": "Selecteer links een prompt om deze te bekijken." + }, + "prompts.edit": { + "en": "Edit", + "nl": "Bewerk" + }, + "prompts.close": { + "en": "Close", + "nl": "Sluiten" + }, + "prompts.newer_previous": { + "en": "newer previous version", + "nl": "nieuwere vorige versie" + }, + "prompts.older_previous": { + "en": "older previous version", + "nl": "oudere vorige versie" + }, + "prompts.diff_view": { + "en": "Diff view:", + "nl": "Verschilweergave:" + }, + "prompts.diff_plain": { + "en": "text, no diff", + "nl": "tekst, geen verschil" + }, + "prompts.diff_all": { + "en": "all diff", + "nl": "alle verschillen" + }, + "prompts.diff_same": { + "en": "unchanged only", + "nl": "alleen ongewijzigd" + }, + "prompts.diff_added": { + "en": "additions only", + "nl": "alleen toevoegingen" + }, + "prompts.diff_deleted": { + "en": "deletions only", + "nl": "alleen verwijderingen" + }, + "prompts.diff_changed": { + "en": "changes only", + "nl": "alleen wijzigingen" + }, + "prompts.previous_version": { + "en": "Previous version", + "nl": "Vorige versie" + }, + "prompts.store_version": { + "en": "store this edit as a new version", + "nl": "bewaar deze bewerking als nieuwe versie" + }, + "prompts.version_note": { + "en": "Version note:", + "nl": "Versienotitie:" + }, + "prompts.save": { + "en": "Save", + "nl": "Opslaan" + }, + "prompts.store_snapshot": { + "en": "Store snapshot", + "nl": "Snapshot bewaren" + }, + "prompts.restore_version": { + "en": "Restore selected version", + "nl": "Geselecteerde versie herstellen" + }, + "prompts.delete_version": { + "en": "Delete selected version", + "nl": "Geselecteerde versie verwijderen" + }, + "prompts.prompt": { + "en": "Prompt", + "nl": "Prompt" + }, + "prompts.prompt_not_found": { + "en": "Prompt not found", + "nl": "Prompt niet gevonden" + }, + "prompts.no_previous_versions": { + "en": "No previous versions stored.", + "nl": "Geen vorige versies opgeslagen." + }, + "prompts.no_lines_for_view": { + "en": "No lines for this view.", + "nl": "Geen regels voor deze weergave." + }, + "prompts.restore_version_confirm": { + "en": "Restore version", + "nl": "Versie herstellen" + }, + "prompts.delete_version_confirm": { + "en": "Delete version", + "nl": "Versie verwijderen" + }, + "prompts.delete_default_confirm": { + "en": "Delete default prompt {{name}}?", + "nl": "Standaardprompt {{name}} verwijderen?" + }, + "prompts.delete_prompt_confirm": { + "en": "Delete prompt {{name}}?", + "nl": "Prompt {{name}} verwijderen?" + }, + "prompts.default_metadata": { + "en": "{{default_key}} · {{updated_at}}", + "nl": "{{default_key}} · {{updated_at}}" + }, + "prompts.personal_metadata": { + "en": "{{language}} · {{updated_at}}", + "nl": "{{language}} · {{updated_at}}" + }, + "prompts.default_prompt_prefix": { + "en": "Default prompt: ", + "nl": "Standaardprompt: " + }, + "prompts.prompt_prefix": { + "en": "Prompt: ", + "nl": "Prompt: " + }, + "prompts.created": { + "en": "created", + "nl": "gemaakt" + }, + "prompts.updated": { + "en": "updated", + "nl": "bijgewerkt" + }, + "prompts.default_prompt": { + "en": "default prompt", + "nl": "standaardprompt" + }, + "prompts.version": { + "en": "version", + "nl": "versie" + }, + "prompts.showing_version": { + "en": "showing version", + "nl": "toont versie" + }, + "prompts.of": { + "en": "of", + "nl": "van" + }, + "prompts.old": { + "en": "old", + "nl": "oud" + }, + "prompts.new": { + "en": "new", + "nl": "nieuw" + }, + "prompts.unknown_action": { + "en": "Unknown action: {{action}}", + "nl": "Onbekende actie: {{action}}" + }, + "app.admin": { + "en": "admin", + "nl": "admin" + }, + "app.user_management": { + "en": "User management", + "nl": "Gebruikersbeheer" + }, + "app.configuration": { + "en": "Configuration", + "nl": "Configuratie" + }, + "app.logout": { + "en": "Logout", + "nl": "Uitloggen" + } + } +} diff --git a/private/views/protocol-error.html b/private/views/protocol-error.html new file mode 100644 index 0000000..9ae9eb5 --- /dev/null +++ b/private/views/protocol-error.html @@ -0,0 +1,11 @@ + + + + +{{title}} + + +

{{title}}

+
{{message}}
+ + diff --git a/private/views/racket-pkg-index.html b/private/views/racket-pkg-index.html new file mode 100644 index 0000000..e984563 --- /dev/null +++ b/private/views/racket-pkg-index.html @@ -0,0 +1,37 @@ + + + + +Racket package index + + + + +

Racket package index

+ +

+Volledige HTML-index van de Racket package catalogus op basis van +pkgs-all. De package-naam is de ophaallink via +rktsndbx.dijkewijk.nl. +

+ +

+Aantal packages: {{package_count}}
+next-id voor alle package-links op deze pagina: +{{next_id}} +

+ + + + + + + + + +{{{package_rows_html}}} + +
#package
+ + + diff --git a/private/views/simple-message.html b/private/views/simple-message.html new file mode 100644 index 0000000..fdc3fc5 --- /dev/null +++ b/private/views/simple-message.html @@ -0,0 +1,12 @@ + + + + +{{title}} + + +

{{title}}

+

{{message}}

+{{{extra_html}}} + + diff --git a/private/views/users.html b/private/views/users.html new file mode 100644 index 0000000..c2a3c1a --- /dev/null +++ b/private/views/users.html @@ -0,0 +1,175 @@ + + + + +{{title}} + + + + +
+ +{{{header_html}}} + +
+ +
+

{{create_user_label}}

+ +
+ + + + + + + +
+
+ +
+

{{user_management_label}}

+ + + + + + + + + + + + + + +{{{user_rows_html}}} + +
{{full_name_label}}{{email_label}}{{admin_label}}{{enabled_label}}{{created_label}}{{last_login_label}}{{actions_label}}
+
+ +
+
+ + +=== +{ + "translations": { + "app.title": { + "en": "Racket sandbox", + "nl": "Racket sandbox" + }, + "app.manage_prompts": { + "en": "Manage prompts", + "nl": "Prompts beheren" + }, + "app.user_management": { + "en": "User management", + "nl": "Gebruikersbeheer" + }, + "app.logout": { + "en": "Logout", + "nl": "Uitloggen" + }, + "app.language": { + "en": "Language", + "nl": "Taal" + }, + "app.logged_in_as": { + "en": "Logged in as:", + "nl": "Ingelogd als:" + }, + "app.admin": { + "en": "Admin", + "nl": "Admin" + }, + "app.enabled": { + "en": "Enabled", + "nl": "Ingeschakeld" + }, + "app.full_name": { + "en": "Full name", + "nl": "Volledige naam" + }, + "app.email": { + "en": "Email", + "nl": "E-mail" + }, + "app.password": { + "en": "Password", + "nl": "Wachtwoord" + }, + "app.new_password": { + "en": "New password", + "nl": "Nieuw wachtwoord" + }, + "app.created": { + "en": "Created", + "nl": "Gemaakt" + }, + "app.last_login": { + "en": "Last login", + "nl": "Laatste login" + }, + "app.actions": { + "en": "Actions", + "nl": "Acties" + }, + "app.create_user": { + "en": "Create user", + "nl": "Gebruiker aanmaken" + }, + "app.update_user": { + "en": "Update user", + "nl": "Gebruiker aanpassen" + }, + "app.change_password": { + "en": "Change password", + "nl": "Wachtwoord wijzigen" + }, + "app.delete_user": { + "en": "Delete user", + "nl": "Gebruiker verwijderen" + }, + "app.delete_user_confirm": { + "en": "Delete user {{email}}?", + "nl": "Gebruiker {{email}} verwijderen?" + }, + "app.cannot_delete_self": { + "en": "You cannot delete your own account.", + "nl": "Je kunt je eigen account niet verwijderen." + }, + "app.cannot_disable_self": { + "en": "You cannot disable your own account.", + "nl": "Je kunt je eigen account niet uitschakelen." + }, + "app.cannot_remove_own_admin": { + "en": "You cannot remove your own admin rights.", + "nl": "Je kunt je eigen adminrechten niet verwijderen." + }, + "app.user_created": { + "en": "User created.", + "nl": "Gebruiker aangemaakt." + }, + "app.user_updated": { + "en": "User updated.", + "nl": "Gebruiker aangepast." + }, + "app.password_changed": { + "en": "Password changed.", + "nl": "Wachtwoord gewijzigd." + }, + "app.user_deleted": { + "en": "User deleted.", + "nl": "Gebruiker verwijderd." + }, + "app.back_to_sandbox": { + "en": "Back to Racket sandbox", + "nl": "Terug naar Racket sandbox" + }, + "app.configuration": { + "en": "Configuration", + "nl": "Configuratie" + } + } +} diff --git a/prompts.php b/prompts.php index ea665f2..a779d39 100644 --- a/prompts.php +++ b/prompts.php @@ -18,11 +18,9 @@ require_once __DIR__ . '/private/header.php'; require_once __DIR__ . '/private/languagestore.php'; require_once __DIR__ . '/private/promptstore.php'; require_once __DIR__ . '/private/usersettings.php'; +require_once __DIR__ . '/private/viewdata.php'; -ini_set('display_errors', '1'); -ini_set('display_startup_errors', '1'); -ini_set('log_errors', '1'); -error_reporting(E_ALL); +require_once __DIR__ . '/config/reporting.php'; $DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite'; @@ -41,11 +39,11 @@ function h($s) return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } -function t($key, $fallback = null) +function t($key, $fallback = null, $values = array()) { global $languageStore, $language; - return $languageStore->translate($key, $language, $fallback); + return $languageStore->translateFormat($key, $language, $values, $fallback); } function post_value($name, $default = '') @@ -116,70 +114,7 @@ $language = resolve_user_language( $store->supportedLanguages() ); -$languageStore->seedDefaults(array( - 'prompts.title' => array('en' => 'Prompt administration', 'nl' => 'Promptbeheer'), - 'prompts.back' => array('en' => 'Back to Racket sandbox', 'nl' => 'Terug naar Racket sandbox'), - 'prompts.your_prompts' => array('en' => 'Your prompts', 'nl' => 'Jouw prompts'), - 'prompts.default_admin' => array('en' => 'Default prompt administration', 'nl' => 'Standaardpromptbeheer'), - 'prompts.language' => array('en' => 'Language', 'nl' => 'Taal'), - 'prompts.available_defaults' => array('en' => 'Available default prompts', 'nl' => 'Beschikbare standaardprompts'), - 'prompts.default_admin_badge' => array('en' => 'Admin default prompts', 'nl' => 'Admin standaardprompts'), - 'prompts.default_admin_hint' => array( - 'en' => 'You are editing global default prompts. Users can copy these to their own prompts.', - 'nl' => 'Je bewerkt globale standaardprompts. Gebruikers kunnen deze naar hun eigen prompts kopieren.' - ), - 'prompts.copy_all' => array('en' => 'Copy all', 'nl' => 'Alles kopieren'), - 'prompts.copy' => array('en' => 'copy', 'nl' => 'kopieer'), - 'prompts.delete' => array('en' => 'delete', 'nl' => 'verwijder'), - 'prompts.create_default' => array('en' => 'Create default prompt', 'nl' => 'Standaardprompt maken'), - 'prompts.create_personal' => array('en' => 'Create personal prompt', 'nl' => 'Persoonlijke prompt maken'), - 'prompts.name' => array('en' => 'Name', 'nl' => 'Naam'), - 'prompts.default_key' => array('en' => 'Default key', 'nl' => 'Standaardsleutel'), - 'prompts.prompt_content' => array('en' => 'Prompt content', 'nl' => 'Promptinhoud'), - 'prompts.no_defaults' => array('en' => 'No default prompts for this language yet.', 'nl' => 'Nog geen standaardprompts voor deze taal.'), - 'prompts.no_personal' => array('en' => 'No personal prompts yet for this language.', 'nl' => 'Nog geen persoonlijke prompts voor deze taal.'), - 'prompts.select_prompt' => array('en' => 'Select a prompt on the left to view it.', 'nl' => 'Selecteer links een prompt om deze te bekijken.'), - 'prompts.edit' => array('en' => 'Edit', 'nl' => 'Bewerk'), - 'prompts.close' => array('en' => 'Close', 'nl' => 'Sluiten'), - 'prompts.newer_previous' => array('en' => 'newer previous version', 'nl' => 'nieuwere vorige versie'), - 'prompts.older_previous' => array('en' => 'older previous version', 'nl' => 'oudere vorige versie'), - 'prompts.diff_view' => array('en' => 'Diff view:', 'nl' => 'Verschilweergave:'), - 'prompts.diff_plain' => array('en' => 'text, no diff', 'nl' => 'tekst, geen verschil'), - 'prompts.diff_all' => array('en' => 'all diff', 'nl' => 'alle verschillen'), - 'prompts.diff_same' => array('en' => 'unchanged only', 'nl' => 'alleen ongewijzigd'), - 'prompts.diff_added' => array('en' => 'additions only', 'nl' => 'alleen toevoegingen'), - 'prompts.diff_deleted' => array('en' => 'deletions only', 'nl' => 'alleen verwijderingen'), - 'prompts.diff_changed' => array('en' => 'changes only', 'nl' => 'alleen wijzigingen'), - 'prompts.previous_version' => array('en' => 'Previous version', 'nl' => 'Vorige versie'), - 'prompts.store_version' => array('en' => 'store this edit as a new version', 'nl' => 'bewaar deze bewerking als nieuwe versie'), - 'prompts.version_note' => array('en' => 'Version note:', 'nl' => 'Versienotitie:'), - 'prompts.save' => array('en' => 'Save', 'nl' => 'Opslaan'), - 'prompts.store_snapshot' => array('en' => 'Store snapshot', 'nl' => 'Snapshot bewaren'), - 'prompts.restore_version' => array('en' => 'Restore selected version', 'nl' => 'Geselecteerde versie herstellen'), - 'prompts.delete_version' => array('en' => 'Delete selected version', 'nl' => 'Geselecteerde versie verwijderen'), - 'prompts.prompt' => array('en' => 'Prompt', 'nl' => 'Prompt'), - 'prompts.prompt_not_found' => array('en' => 'Prompt not found', 'nl' => 'Prompt niet gevonden'), - 'prompts.no_previous_versions' => array('en' => 'No previous versions stored.', 'nl' => 'Geen vorige versies opgeslagen.'), - 'prompts.no_lines_for_view' => array('en' => 'No lines for this view.', 'nl' => 'Geen regels voor deze weergave.'), - 'prompts.restore_version_confirm' => array('en' => 'Restore version', 'nl' => 'Versie herstellen'), - 'prompts.delete_version_confirm' => array('en' => 'Delete version', 'nl' => 'Versie verwijderen'), - 'prompts.delete_default_confirm' => array('en' => 'Delete default prompt', 'nl' => 'Standaardprompt verwijderen'), - 'prompts.delete_prompt_confirm' => array('en' => 'Delete prompt', 'nl' => 'Prompt verwijderen'), - 'prompts.default_prompt_prefix' => array('en' => 'Default prompt: ', 'nl' => 'Standaardprompt: '), - 'prompts.prompt_prefix' => array('en' => 'Prompt: ', 'nl' => 'Prompt: '), - 'prompts.created' => array('en' => 'created', 'nl' => 'gemaakt'), - 'prompts.updated' => array('en' => 'updated', 'nl' => 'bijgewerkt'), - 'prompts.default_prompt' => array('en' => 'default prompt', 'nl' => 'standaardprompt'), - 'prompts.version' => array('en' => 'version', 'nl' => 'versie'), - 'prompts.showing_version' => array('en' => 'showing version', 'nl' => 'toont versie'), - 'prompts.of' => array('en' => 'of', 'nl' => 'van'), - 'prompts.old' => array('en' => 'old', 'nl' => 'oud'), - 'prompts.new' => array('en' => 'new', 'nl' => 'nieuw'), - 'app.admin' => array('en' => 'admin', 'nl' => 'admin'), - 'app.user_management' => array('en' => 'User management', 'nl' => 'Gebruikersbeheer'), - 'app.configuration' => array('en' => 'Configuration', 'nl' => 'Configuratie'), - 'app.logout' => array('en' => 'Logout', 'nl' => 'Uitloggen'), -)); +seed_template_translations($languageStore, 'prompts.html'); $mode = get_value('mode', 'personal'); @@ -326,7 +261,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $message = 'Default prompt deleted.'; } } elseif ($action !== '') { - throw new Exception('Unknown action: ' . $action); + throw new Exception(t('prompts.unknown_action', 'Unknown action: {{action}}', array('action' => $action))); } } catch (Throwable $e) { $error = $e->getMessage(); @@ -394,309 +329,115 @@ if ($user->isAdmin()) { ); } -$styleVersion = @filemtime(__DIR__ . '/styles.css') ?: time(); +$styleVersion = @filemtime(__DIR__ . '/css/styles.css') ?: time(); $promptEditorVersion = @filemtime(__DIR__ . '/js/prompt-editor.js') ?: time(); header('Content-Type: text/html; charset=utf-8'); -?> - - - - -<?= h(t('prompts.title', 'Prompt administration')) ?> - - - -
+$languageOptionsHtml = ''; +$languageOptionsPlainHtml = ''; - t('prompts.title', 'Prompt administration'), - 'nav_items' => $headerNavItems, - 'user' => $user, - 'admin_label' => t('app.admin', 'admin'), - 'language_label' => t('prompts.language', 'Language'), - 'language' => $language, - 'languages' => $headerLanguages, - 'language_action' => '/prompts', - 'language_hidden' => array('mode' => $mode), - 'logout_action' => '/prompts?lang=' . rawurlencode($language) . '&mode=' . rawurlencode($mode), - 'logout_label' => t('app.logout', 'Logout'), - 'message' => $message, - 'error' => $error, -)); -?> +foreach ($store->supportedLanguages() as $lang) { + $languageOptionsHtml .= RacketSandboxTemplate::renderFile('partials/select-option.html', array( + 'value' => $lang, + 'selected' => $lang === $language ? ' selected' : '', + 'label' => $store->languageLabel($lang), + )) . "\n"; -
+ $languageOptionsPlainHtml .= RacketSandboxTemplate::renderFile('partials/select-option.html', array( + 'value' => $lang, + 'selected' => '', + 'label' => $store->languageLabel($lang), + )) . "\n"; +} - +foreach ($personalPrompts as $prompt) { + $personalPromptsHtml .= RacketSandboxTemplate::renderFile('partials/prompt-personal-item.html', array( + 'language_url' => rawurlencode($language), + 'id' => $prompt->id(), + 'name' => $prompt->name(), + 'metadata' => t('prompts.personal_metadata', '{{language}} · {{updated_at}}', array( + 'language' => $store->languageLabel($prompt->language()), + 'updated_at' => fmt_time($prompt->updatedAt()), + )), + 'confirm_json' => json_encode(t('prompts.delete_prompt_confirm', 'Delete prompt {{name}}?', array( + 'name' => $prompt->name(), + )), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT), + 'delete_label' => t('prompts.delete', 'delete'), + )) . "\n"; +} -
-
- -
+$noPersonalHtml = count($personalPrompts) === 0 + ? RacketSandboxTemplate::renderFile('partials/paragraph.html', array( + 'class' => 'empty-state', + 'text' => t('prompts.no_personal', 'No personal prompts yet for this language.'), + )) + : ''; -
-
-
-
-
-
- -
-

-
-
- -
- -
- -
- -
- - - - - - - - +), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); - - +$headerHtml = app_header_html(array( + 'title' => t('prompts.title', 'Prompt administration'), + 'nav_items' => $headerNavItems, + 'user' => $user, + 'admin_label' => t('app.admin', 'admin'), + 'language_label' => t('prompts.language', 'Language'), + 'language' => $language, + 'languages' => $headerLanguages, + 'language_action' => '/prompts', + 'language_hidden' => array('mode' => $mode), + 'logout_action' => '/prompts?lang=' . rawurlencode($language) . '&mode=' . rawurlencode($mode), + 'logout_label' => t('app.logout', 'Logout'), + 'message' => $message, + 'error' => $error, +)); + +echo RacketSandboxTemplate::renderFile('prompts.html', array( + 'language' => $language, + 'language_url' => rawurlencode($language), + 'mode_url' => rawurlencode($mode), + 'title' => t('prompts.title', 'Prompt administration'), + 'style_version' => $styleVersion, + 'prompt_editor_version' => $promptEditorVersion, + 'header_html' => $headerHtml, + 'defaults_tab_active_class' => $mode === 'defaults' ? 'active' : '', + 'defaults_tab_selected' => $mode === 'defaults' ? 'true' : 'false', + 'personal_tab_active_class' => $mode !== 'defaults' ? 'active' : '', + 'personal_tab_selected' => $mode !== 'defaults' ? 'true' : 'false', + 'available_defaults_label' => t('prompts.available_defaults', 'Available default prompts'), + 'your_prompts_label' => t('prompts.your_prompts', 'Your prompts'), + 'default_admin_notice_html' => $defaultAdminNoticeHtml, + 'copy_all_label' => t('prompts.copy_all', 'Copy all'), + 'default_prompts_html' => $defaultPromptsHtml, + 'no_defaults_html' => $noDefaultsHtml, + 'create_default_html' => $createDefaultHtml, + 'personal_prompts_html' => $personalPromptsHtml, + 'no_personal_html' => $noPersonalHtml, + 'create_personal_label' => t('prompts.create_personal', 'Create personal prompt'), + 'name_label' => t('prompts.name', 'Name'), + 'language_label' => t('prompts.language', 'Language'), + 'language_options_html' => $languageOptionsHtml, + 'language_options_plain_html' => $languageOptionsPlainHtml, + 'prompt_content_label' => t('prompts.prompt_content', 'Prompt content'), + 'select_prompt_label' => t('prompts.select_prompt', 'Select a prompt on the left to view it.'), + 'prompt_label' => t('prompts.prompt', 'Prompt'), + 'edit_label' => t('prompts.edit', 'Edit'), + 'back_label' => t('prompts.back', 'Back to Racket sandbox'), + 'close_label' => t('prompts.close', 'Close'), + 'newer_previous_label' => t('prompts.newer_previous', 'newer previous version'), + 'older_previous_label' => t('prompts.older_previous', 'older previous version'), + 'diff_view_label' => t('prompts.diff_view', 'Diff view:'), + 'diff_plain_label' => t('prompts.diff_plain', 'text, no diff'), + 'diff_all_label' => t('prompts.diff_all', 'all diff'), + 'diff_same_label' => t('prompts.diff_same', 'unchanged only'), + 'diff_added_label' => t('prompts.diff_added', 'additions only'), + 'diff_deleted_label' => t('prompts.diff_deleted', 'deletions only'), + 'diff_changed_label' => t('prompts.diff_changed', 'changes only'), + 'default_key_label' => t('prompts.default_key', 'Default key'), + 'previous_version_label' => t('prompts.previous_version', 'Previous version'), + 'store_version_label' => t('prompts.store_version', 'store this edit as a new version'), + 'version_note_label' => t('prompts.version_note', 'Version note:'), + 'save_label' => t('prompts.save', 'Save'), + 'store_snapshot_label' => t('prompts.store_snapshot', 'Store snapshot'), + 'restore_version_label' => t('prompts.restore_version', 'Restore selected version'), + 'delete_version_label' => t('prompts.delete_version', 'Delete selected version'), + 'prompt_data_json' => json_encode($promptData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), + 'prompt_text_json' => $promptTextJson, +)); diff --git a/rkt.php b/rkt.php index a48ea73..7ee1982 100644 --- a/rkt.php +++ b/rkt.php @@ -31,14 +31,12 @@ * links op die pagina gebruikt. */ -ini_set('display_errors', '1'); -ini_set('display_startup_errors', '1'); -ini_set('log_errors', '1'); -error_reporting(E_ALL); +require_once __DIR__ . '/config/reporting.php'; require_once __DIR__ . '/private/nexttoken.php'; require_once __DIR__ . '/private/base64config.php'; require_once __DIR__ . '/private/racketzip.php'; +require_once __DIR__ . '/private/Template.php'; $TOKENS = new NextTokenStore(__DIR__ . '/data/racket-sandbox.sqlite'); @@ -131,9 +129,10 @@ function text_response($text, $status = 200) function fail_html($message, $status = 500) { html_response( - '' . - 'Error' . - '

Error

' . h($message) . '
', + RacketSandboxTemplate::renderFile('protocol-error.html', array( + 'title' => 'Error', + 'message' => $message, + )), $status ); } @@ -185,88 +184,29 @@ function serve_bootstrap() $rows = ''; foreach ($parts as $i => $part) { - $rows .= - '' . - '' . h((string)($i + 1)) . '' . - '' . h($part['number']) . '' . - '' . h((string)$part['size']) . '' . - '' . h($part['url']) . '' . - '' . "\n"; + $rows .= RacketSandboxTemplate::renderFile('partials/racket-part-row.html', array( + 'index' => (string)($i + 1), + 'number' => $part['number'], + 'size' => (string)$part['size'], + 'url' => $part['url'], + )) . "\n"; } $zipSize = (int)($manifest['source_bytes'] ?? 0); $pkg_url = make_url('/racket-pkg-index'); - html_response(' - - - -Racket bootstrap - - - - -

Racket bootstrap

- -

-racket.zip is vooraf via de configuratiepagina gesplitst naar -parts in de map data. -

- -

-Package index: Racket package index -

- -

-Bronbestand: config/racket.zip
-Bronbestand bytes: ' . h((string)$zipSize) . '
-Maximale base64 part-grootte: ' . h((string)RACKET_ZIP_MAX_BASE64_KB) . ' KiB (' . h((string)RACKET_ZIP_MAX_BASE64_BYTES) . ' bytes)
-Binaire chunk-grootte: ' . h((string)RACKET_ZIP_BINARY_CHUNK_BYTES) . ' bytes
-Aantal parts: ' . h((string)count($parts)) . '
-Parts gemaakt op: ' . h((string)($manifest['created_at'] ?? '')) . '
-next-id voor alle links op deze pagina: ' . h($NEXT_ID) . ' -

- -

-Elke link hieronder geeft text/plain met de -base64-representatie van één binair part. -De URL bevat alleen een nummer, geen bestandsnaam en geen extensie. -

- - - - - - - - - - - -' . $rows . ' - -
#partnummerbytesbase64 text/plain URL
- -

Reconstructie in de sandbox

- -

-Decodeer ieder base64-part afzonderlijk naar een binair part. Plak daarna de -binaire parts in numerieke volgorde aan elkaar. -

- -
-base64 -d part-000001.txt > part-000001
-base64 -d part-000002.txt > part-000002
-base64 -d part-000003.txt > part-000003
-# enzovoort
-
-cat part-* > racket.zip
-unzip racket.zip -d /tmp/racket
-
- - -'); + html_response(RacketSandboxTemplate::renderFile('bootstrap-racket.html', array( + 'pkg_url' => $pkg_url, + 'zip_size' => (string)$zipSize, + 'max_base64_kb' => (string)RACKET_ZIP_MAX_BASE64_KB, + 'max_base64_bytes' => (string)RACKET_ZIP_MAX_BASE64_BYTES, + 'binary_chunk_bytes' => (string)RACKET_ZIP_BINARY_CHUNK_BYTES, + 'part_count' => (string)count($parts), + 'created_at' => (string)($manifest['created_at'] ?? ''), + 'next_id' => $NEXT_ID, + 'part_rows_html' => $rows, + ))); } function serve_part() diff --git a/rktpkgs.php b/rktpkgs.php index 6b095ad..01ba163 100644 --- a/rktpkgs.php +++ b/rktpkgs.php @@ -28,14 +28,12 @@ * RewriteRule ^racket-pkg-index$ rktpkgs.php [L,QSA] */ -ini_set('display_errors', '1'); -ini_set('display_startup_errors', '1'); -ini_set('log_errors', '1'); -error_reporting(E_ALL); +require_once __DIR__ . '/config/reporting.php'; require_once __DIR__ . '/private/nexttoken.php'; require_once __DIR__ . '/private/lib/catalog-http.php'; require_once __DIR__ . '/private/lib/racket-data.php'; +require_once __DIR__ . '/private/Template.php'; $TOKENS = new NextTokenStore(__DIR__ . '/data/racket-sandbox.sqlite'); @@ -96,9 +94,10 @@ function html_response($html, $status = 200) function fail_html($message, $status = 500) { html_response( - '' . - 'Error' . - '

Error

' . h($message) . '
', + RacketSandboxTemplate::renderFile('protocol-error.html', array( + 'title' => 'Error', + 'message' => $message, + )), $status ); } @@ -135,52 +134,19 @@ function serve_index() foreach ($names as $i => $name) { $url = make_url('/package', array('name' => $name)); - $rows .= - '' . - '' . h((string)($i + 1)) . '' . - '' . - '' . h($name) . '' . - '' . - '' . "\n"; + $rows .= RacketSandboxTemplate::renderFile('partials/package-index-row.html', array( + 'id' => $name, + 'index' => (string)($i + 1), + 'url' => $url, + 'name' => $name, + )) . "\n"; } - html_response(' - - - -Racket package index - - - - -

Racket package index

- -

-Volledige HTML-index van de Racket package catalogus op basis van -pkgs-all. De package-naam is de ophaallink via -rktsndbx.dijkewijk.nl. -

- -

-Aantal packages: ' . h((string)count($names)) . '
-next-id voor alle package-links op deze pagina: -' . h($NEXT_ID) . ' -

- - - - - - - - - -' . $rows . ' - -
#package
- - -'); + html_response(RacketSandboxTemplate::renderFile('racket-pkg-index.html', array( + 'package_count' => (string)count($names), + 'next_id' => $NEXT_ID, + 'package_rows_html' => $rows, + ))); } $TOKENS->check_valid_next('html'); diff --git a/users.php b/users.php index 7cea83c..29aa1d6 100644 --- a/users.php +++ b/users.php @@ -9,11 +9,9 @@ require_once __DIR__ . '/private/auth.php'; require_once __DIR__ . '/private/header.php'; require_once __DIR__ . '/private/languagestore.php'; require_once __DIR__ . '/private/usersettings.php'; +require_once __DIR__ . '/private/viewdata.php'; -ini_set('display_errors', '1'); -ini_set('display_startup_errors', '1'); -ini_set('log_errors', '1'); -error_reporting(E_ALL); +require_once __DIR__ . '/config/reporting.php'; $DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite'; @@ -30,11 +28,11 @@ function h($s) return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } -function t($key, $fallback = null) +function t($key, $fallback = null, $values = array()) { global $languageStore, $language; - return $languageStore->translate($key, $language, $fallback); + return $languageStore->translateFormat($key, $language, $values, $fallback); } function post_value($name, $default = '') @@ -77,37 +75,7 @@ $language = resolve_user_language( $languageStore->supportedLanguages() ); -$languageStore->seedDefaults(array( - 'app.title' => array('en' => 'Racket sandbox', 'nl' => 'Racket sandbox'), - 'app.manage_prompts' => array('en' => 'Manage prompts', 'nl' => 'Prompts beheren'), - 'app.user_management' => array('en' => 'User management', 'nl' => 'Gebruikersbeheer'), - 'app.logout' => array('en' => 'Logout', 'nl' => 'Uitloggen'), - 'app.language' => array('en' => 'Language', 'nl' => 'Taal'), - 'app.logged_in_as' => array('en' => 'Logged in as:', 'nl' => 'Ingelogd als:'), - 'app.admin' => array('en' => 'Admin', 'nl' => 'Admin'), - 'app.enabled' => array('en' => 'Enabled', 'nl' => 'Ingeschakeld'), - 'app.full_name' => array('en' => 'Full name', 'nl' => 'Volledige naam'), - 'app.email' => array('en' => 'Email', 'nl' => 'E-mail'), - 'app.password' => array('en' => 'Password', 'nl' => 'Wachtwoord'), - 'app.new_password' => array('en' => 'New password', 'nl' => 'Nieuw wachtwoord'), - 'app.created' => array('en' => 'Created', 'nl' => 'Gemaakt'), - 'app.last_login' => array('en' => 'Last login', 'nl' => 'Laatste login'), - 'app.actions' => array('en' => 'Actions', 'nl' => 'Acties'), - 'app.create_user' => array('en' => 'Create user', 'nl' => 'Gebruiker aanmaken'), - 'app.update_user' => array('en' => 'Update user', 'nl' => 'Gebruiker aanpassen'), - 'app.change_password' => array('en' => 'Change password', 'nl' => 'Wachtwoord wijzigen'), - 'app.delete_user' => array('en' => 'Delete user', 'nl' => 'Gebruiker verwijderen'), - 'app.delete_user_confirm' => array('en' => 'Delete user', 'nl' => 'Gebruiker verwijderen'), - 'app.cannot_delete_self' => array('en' => 'You cannot delete your own account.', 'nl' => 'Je kunt je eigen account niet verwijderen.'), - 'app.cannot_disable_self' => array('en' => 'You cannot disable your own account.', 'nl' => 'Je kunt je eigen account niet uitschakelen.'), - 'app.cannot_remove_own_admin' => array('en' => 'You cannot remove your own admin rights.', 'nl' => 'Je kunt je eigen adminrechten niet verwijderen.'), - 'app.user_created' => array('en' => 'User created.', 'nl' => 'Gebruiker aangemaakt.'), - 'app.user_updated' => array('en' => 'User updated.', 'nl' => 'Gebruiker aangepast.'), - 'app.password_changed' => array('en' => 'Password changed.', 'nl' => 'Wachtwoord gewijzigd.'), - 'app.user_deleted' => array('en' => 'User deleted.', 'nl' => 'Gebruiker verwijderd.'), - 'app.back_to_sandbox' => array('en' => 'Back to Racket sandbox', 'nl' => 'Terug naar Racket sandbox'), - 'app.configuration' => array('en' => 'Configuration', 'nl' => 'Configuratie'), -)); +seed_template_translations($languageStore, 'users.html'); if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = post_value('action'); @@ -169,20 +137,46 @@ foreach ($languageStore->supportedLanguages() as $lang) { } header('Content-Type: text/html; charset=utf-8'); -?> - - - - -<?= h(t('app.user_management', 'User management')) ?> - - - +$userRowsHtml = ''; -
+foreach ($users as $managedUser) { + if ($managedUser->id() !== $currentUser->id()) { + $deleteHtml = RacketSandboxTemplate::renderFile('partials/user-delete-form.html', array( + 'language_url' => rawurlencode($language), + 'confirm_json' => json_encode(t('app.delete_user_confirm', 'Delete user {{email}}?', array( + 'email' => $managedUser->email(), + )), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT), + 'email' => $managedUser->email(), + 'user_id' => $managedUser->id(), + 'delete_user_label' => t('app.delete_user', 'Delete user'), + )); + } else { + $deleteHtml = RacketSandboxTemplate::renderFile('partials/user-self-note.html', array( + 'cannot_delete_self' => t('app.cannot_delete_self', 'You cannot delete your own account.'), + )); + } - rawurlencode($language), + 'user_id' => $managedUser->id(), + 'full_name_label' => t('app.full_name', 'Full name'), + 'full_name' => $managedUser->fullName(), + 'email_label' => t('app.email', 'Email'), + 'email' => $managedUser->email(), + 'admin_label' => t('app.admin', 'Admin'), + 'enabled_label' => t('app.enabled', 'Enabled'), + 'is_admin_checked' => $managedUser->isAdmin() ? ' checked' : '', + 'is_enabled_checked' => $managedUser->isEnabled() ? ' checked' : '', + 'created_at' => fmt_time($managedUser->createdAt()), + 'last_login_at' => fmt_time($managedUser->lastLoginAt()), + 'update_user_label' => t('app.update_user', 'Update user'), + 'new_password_label' => t('app.new_password', 'New password'), + 'change_password_label' => t('app.change_password', 'Change password'), + 'delete_html' => $deleteHtml, + )) . "\n"; +} + +$headerHtml = app_header_html(array( 'title' => t('app.user_management', 'User management'), 'nav_items' => array( array('label' => t('app.back_to_sandbox', 'Back to Racket sandbox'), 'url' => '/?lang=' . rawurlencode($language)), @@ -215,83 +209,21 @@ render_app_header(array( 'message' => $message, 'error' => $error, )); -?> -
- -
-

- -
- - - - - - - -
-
- -
-

- - - - - - - - - - - - - - - - - - - - -
-
- - - - - - - -createdAt())) ?> -lastLoginAt())) ?> - -
- -
-
- - - - -
- -id() !== $currentUser->id()): ?> -
- - - -
- -

- -
-
-
- -
-
- - +echo RacketSandboxTemplate::renderFile('users.html', array( + 'language' => $language, + 'language_url' => rawurlencode($language), + 'title' => t('app.user_management', 'User management'), + 'header_html' => $headerHtml, + 'create_user_label' => t('app.create_user', 'Create user'), + 'full_name_label' => t('app.full_name', 'Full name'), + 'email_label' => t('app.email', 'Email'), + 'password_label' => t('app.password', 'Password'), + 'admin_label' => t('app.admin', 'Admin'), + 'enabled_label' => t('app.enabled', 'Enabled'), + 'user_management_label' => t('app.user_management', 'User management'), + 'created_label' => t('app.created', 'Created'), + 'last_login_label' => t('app.last_login', 'Last login'), + 'actions_label' => t('app.actions', 'Actions'), + 'user_rows_html' => $userRowsHtml, +));