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');
-
-= h(t('app.download_settings', 'Download settings')) ?>
+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";
+ }
-= h(t('app.chunk_size_hint_v2', '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.')) ?>
+ $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.'),
+ ));
+}
-
-
-= h(t('app.racket_zip_chunk_kb', 'Racket installation max base64 chunk size (KiB)')) ?>
-= h($base64ChunkConfig['racket_zip_max_base64_kb']) ?> KiB
-= h(t('app.effective_binary_chunk', 'Effective binary chunk')) ?>: = h(base64_binary_chunk_bytes_for_kib($base64ChunkConfig['racket_zip_max_base64_kb'])) ?> bytes
-
-
-= h(t('app.package_zip_chunk_kb', 'Package/module max base64 chunk size (KiB)')) ?>
-= h($base64ChunkConfig['package_zip_max_base64_kb']) ?> KiB
-= h(t('app.effective_binary_chunk', 'Effective binary chunk')) ?>: = h(base64_binary_chunk_bytes_for_kib($base64ChunkConfig['package_zip_max_base64_kb'])) ?> bytes
-
-
-= h(t('app.racket_zip_source', 'Racket installation source')) ?>
-config/racket.zip
-= h(is_file(RACKET_ZIP_FILE) ? (string)filesize(RACKET_ZIP_FILE) : '0') ?> bytes
-
-
-= h(t('app.racket_parts', 'Racket installation parts')) ?>
-= h((string)($racketPartsManifest['part_count'] ?? 0)) ?>
-
-
-= h(t('app.racket_parts_current', 'current')) ?>,
-= h((string)($racketPartsManifest['created_at'] ?? '')) ?>
-
-= h(t('app.racket_parts_missing', 'missing or outdated; save configuration to regenerate')) ?>
-
-
-
-
-
-
-
-= h(t('app.maintenance', 'Maintenance')) ?>
-
-
-= h(t('app.next_tokens', 'Next tokens')) ?>
-
-
-
-
-= h(t('app.cleanup_help', 'Expired links should return an outdated information message to the AI agent. Cleanup only removes old token rows from SQLite.')) ?>
-
-
-= h(t('app.current_next_tokens', 'Current next tokens')) ?>
-
- 0): ?>
-
-
-= h(t('app.token', 'Token')) ?>
-= h(t('app.created_at', 'Created at')) ?>
-= h(t('app.expires_at', 'Expires at')) ?>
-
-
-
-= h($nextToken['token'] ?? '') ?>
-= h(format_token_time($nextToken['created_at'] ?? 0)) ?>
-= h(format_token_time($nextToken['expires_at'] ?? 0)) ?>
-
-
-
-
-= h(t('app.no_current_next_tokens', 'No current next tokens.')) ?>
-
-
-
-
-
-
-
-
+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,
));
-?>
-
-
-
-= h(t('app.bootstrap_link', 'Bootstrap link')) ?>
-
-
-= h(t('app.generate_bootstrap_link', 'Generate bootstrap link')) ?>
-
-
-
-
-
-
-
= h(t('app.generated_link', 'Generated link')) ?>
-
-
-
-
-
-
-
= h(t('app.copy_full_prompt', 'Copy full prompt')) ?>
-
-= h(t('app.bootstrap_prompt_help', 'Choose one of your prompts. The placeholder {{bootstrap-racket-link}} is replaced by the generated bootstrap link.')) ?>
-
-
- 0): ?>
-
-
-= h(t('app.select_prompt', 'Select prompt')) ?>
-
-
-= h($prompt['name']) ?>
-
-
-
-
-
-
-
-= h(t('app.copy_full_prompt', 'Copy full prompt')) ?>
-
-
-
-
-
= h(t('app.no_bootstrap_prompts', 'No personal prompts are available for this language. Copy a default prompt first from prompt management.')) ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+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) ?>
-
-
-
-
-
-= h($pageTitle) ?>
+$loginText = $texts[$language];
+$errorHtml = $error === ''
+ ? ''
+ : RacketSandboxTemplate::renderFile('partials/message.html', array(
+ 'class' => 'error',
+ 'message' => $error,
+ ));
-
-= h($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
-
-
-
-
-part
-base64 bytes
-text/plain URL
-
-
-
-' . $rows . '
-
-
-
-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.
+
+
+
+
+
+#
+partnummer
+bytes
+base64 text/plain URL
+
+
+
+{{{part_rows_html}}}
+
+
+
+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}}
+
+
+
+
+
+{{{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}}
+
+
+
+
+
+
+{{page_title}}
+
+{{{error_html}}}
+
+
+
+
+
+
+
+
+
+===
+{
+ "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
+base64 bytes
+text/plain URL
+
+
+
+{{{part_rows_html}}}
+
+
+
+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_label}}
+{{created_at_label}}
+{{expires_at_label}}
+
+{{{token_rows_html}}}
+
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 @@
+
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 @@
+
+
+{{select_prompt_label}}
+
+{{{prompt_options_html}}}
+
+
+
+
+
+
+{{copy_full_prompt_label}}
+
+
+
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 @@
+
+
+
+{{delete_label}}
+
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 @@
+
+
+{{name}}
+{{{metadata}}}
+
+
+
+
+
+{{copy_label}}
+
+
+{{{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 @@
+
+
+{{name}}
+{{metadata}}
+
+
+
+
+{{delete_label}}
+
+
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 @@
+{{label}}
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 @@
+
+
+
+{{delete_user_label}}
+
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 @@
+
+
+
+
+
+
+{{full_name_label}}
+{{email_label}}
+ {{admin_label}}
+ {{enabled_label}}
+{{created_at}}
+{{last_login_at}}
+{{update_user_label}}
+
+
+
+
+
+
+{{new_password_label}}
+{{change_password_label}}
+
+
+{{{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}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{newer_previous_label}}
+{{older_previous_label}}
+
+
+{{diff_view_label}}
+
+{{diff_plain_label}}
+{{diff_all_label}}
+{{diff_same_label}}
+{{diff_added_label}}
+{{diff_deleted_label}}
+{{diff_changed_label}}
+
+
+
+
+
+
+
+
+
+
+
+{{name_label}}
+
+
+
+
+{{language_label}}
+
+{{{language_options_plain_html}}}
+
+
+
+
+
+
+{{default_key_label}}
+
+
+
+
+
+{{prompt_content_label}}
+
+
+
+
+
+
+
{{previous_version_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
+
+
+
+{{{package_rows_html}}}
+
+
+
+
+
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}}}
+
+
+
+
+
+
+{{user_management_label}}
+
+
+
+
+{{full_name_label}}
+{{email_label}}
+{{admin_label}}
+{{enabled_label}}
+{{created_label}}
+{{last_login_label}}
+{{actions_label}}
+
+
+
+{{{user_rows_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.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";
+}
-
-
-= h(t('prompts.select_prompt', 'Select a prompt on the left to view it.')) ?>
-
+$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.'),
+ ))
+ : '';
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-= h(t('prompts.newer_previous', 'newer previous version')) ?>
-= h(t('prompts.older_previous', 'older previous version')) ?>
-
-
-= h(t('prompts.diff_view', 'Diff view:')) ?>
-
-= h(t('prompts.diff_plain', 'text, no diff')) ?>
-= h(t('prompts.diff_all', 'all diff')) ?>
-= h(t('prompts.diff_same', 'unchanged only')) ?>
-= h(t('prompts.diff_added', 'additions only')) ?>
-= h(t('prompts.diff_deleted', 'deletions only')) ?>
-= h(t('prompts.diff_changed', 'changes only')) ?>
-
-
-
-
-
-
-
-
-
-
-
-= h(t('prompts.name', 'Name')) ?>
-
-
-
-
-= h(t('prompts.language', 'Language')) ?>
-
-supportedLanguages() as $lang): ?>
-= h($store->languageLabel($lang)) ?>
-
-
-
-
-
-
-
-= h(t('prompts.default_key', 'Default key')) ?>
-
-
-
-
-
-= h(t('prompts.prompt_content', 'Prompt content')) ?>
-
-
-
-
-
-
-
= h(t('prompts.previous_version', 'Previous version')) ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+), 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.
-
-
-
-
-
-#
-partnummer
-bytes
-base64 text/plain URL
-
-
-
-' . $rows . '
-
-
-
-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) . '
-
-
-
-
-
-#
-package
-
-
-
-' . $rows . '
-
-
-
-
-');
+ 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,
));
-?>
-
-
-
-
-
-= h(t('app.user_management', 'User management')) ?>
-
-
-
-
-= h(t('app.full_name', 'Full name')) ?>
-= h(t('app.email', 'Email')) ?>
-= h(t('app.admin', 'Admin')) ?>
-= h(t('app.enabled', 'Enabled')) ?>
-= h(t('app.created', 'Created')) ?>
-= h(t('app.last_login', 'Last login')) ?>
-= h(t('app.actions', 'Actions')) ?>
-
-
-
-
-
-
-
-
-
-
-= h(t('app.full_name', 'Full name')) ?>
-= h(t('app.email', 'Email')) ?>
- isAdmin() ? 'checked' : '' ?>> = h(t('app.admin', 'Admin')) ?>
- isEnabled() ? 'checked' : '' ?>> = h(t('app.enabled', 'Enabled')) ?>
-= h(fmt_time($managedUser->createdAt())) ?>
-= h(fmt_time($managedUser->lastLoginAt())) ?>
-= h(t('app.update_user', 'Update user')) ?>
-
-
-
-
-
-
-= h(t('app.new_password', 'New password')) ?>
-= h(t('app.change_password', 'Change password')) ?>
-
-
-id() !== $currentUser->id()): ?>
-
-
-
-= h(t('app.delete_user', 'Delete user')) ?>
-
-
-
= h(t('app.cannot_delete_self', 'You cannot delete your own account.')) ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
+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,
+));