Move rendering into private templates

Add an explicit template renderer with HTML views and partials for the app, bootstrap, package, and catalog pages. Move shared reporting setup into config/reporting.php and relocate stylesheet assets under css/.
This commit is contained in:
www-data
2026-05-26 12:50:26 +02:00
parent 2f2e8869d6
commit 475765e31f
55 changed files with 2328 additions and 1175 deletions
+90 -156
View File
@@ -12,11 +12,9 @@ require_once __DIR__ . '/private/nexttoken.php';
require_once __DIR__ . '/private/usersettings.php'; require_once __DIR__ . '/private/usersettings.php';
require_once __DIR__ . '/private/base64config.php'; require_once __DIR__ . '/private/base64config.php';
require_once __DIR__ . '/private/racketzip.php'; require_once __DIR__ . '/private/racketzip.php';
require_once __DIR__ . '/private/viewdata.php';
ini_set('display_errors', '1'); require_once __DIR__ . '/config/reporting.php';
ini_set('display_startup_errors', '1');
ini_set('log_errors', '1');
error_reporting(E_ALL);
$DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite'; $DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite';
@@ -36,11 +34,11 @@ function h($s)
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); 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; global $languageStore, $language;
return $languageStore->translate($key, $language, $fallback); return $languageStore->translateFormat($key, $language, $values, $fallback);
} }
function post_value($name, $default = '') function post_value($name, $default = '')
@@ -84,51 +82,18 @@ function token_status_class($token)
: 'token-expired'; : 'token-expired';
} }
function code_value($value)
{
return '<code>' . h($value) . '</code>';
}
$language = resolve_user_language( $language = resolve_user_language(
$userSettings, $userSettings,
$currentUser->id(), $currentUser->id(),
$languageStore->supportedLanguages() $languageStore->supportedLanguages()
); );
$languageStore->seedDefaults(array( seed_template_translations($languageStore, 'config.html');
'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.'
),
));
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = post_value('action'); $action = post_value('action');
@@ -145,12 +110,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
)); ));
$base64ChunkConfig = load_base64_chunk_config(); $base64ChunkConfig = load_base64_chunk_config();
$manifest = split_racket_zip_parts($base64ChunkConfig['racket_zip_max_base64_kb']); $manifest = split_racket_zip_parts($base64ChunkConfig['racket_zip_max_base64_kb']);
$message = t('app.configuration_saved', 'Configuration saved.') . ' ' . $message = t('app.configuration_saved_with_parts', 'Configuration saved. Racket installation parts regenerated: {{count}}', array(
t('app.racket_parts_regenerated', 'Racket installation parts regenerated:') . ' ' . 'count' => (int)$manifest['part_count'],
(int)$manifest['part_count']; ));
} elseif ($action === 'cleanup_tokens') { } elseif ($action === 'cleanup_tokens') {
$deleted = $tokens->cleanup(); $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) { } catch (Throwable $e) {
$error = $e->getMessage(); $error = $e->getMessage();
@@ -177,8 +144,9 @@ if (!$racketPartsCurrent && is_file(RACKET_ZIP_FILE) && is_readable(RACKET_ZIP_F
$racketPartsCurrent = true; $racketPartsCurrent = true;
if ($message === '') { if ($message === '') {
$message = t('app.racket_parts_regenerated', 'Racket installation parts regenerated:') . ' ' . $message = t('app.racket_parts_regenerated', 'Racket installation parts regenerated: {{count}}', array(
(int)$racketPartsManifest['part_count']; 'count' => (int)$racketPartsManifest['part_count'],
));
} }
} catch (Throwable $e) { } catch (Throwable $e) {
if ($error === '') { if ($error === '') {
@@ -203,23 +171,10 @@ foreach ($languageStore->supportedLanguages() as $lang) {
$headerLanguages[$lang] = $languageStore->languageLabel($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'); header('Content-Type: text/html; charset=utf-8');
?> $headerHtml = app_header_html(array(
<!doctype html>
<html lang="<?= h($language) ?>">
<head>
<meta charset="utf-8">
<title><?= h(t('app.configuration', 'Configuration')) ?></title>
<link rel="stylesheet" href="/styles.css?v=<?= h($styleVersion) ?>">
</head>
<body>
<div class="page">
<?php
render_app_header(array(
'title' => t('app.configuration', 'Configuration'), 'title' => t('app.configuration', 'Configuration'),
'nav_items' => array( 'nav_items' => array(
array('label' => t('app.back_to_sandbox', 'Back to Racket sandbox'), 'url' => '/?lang=' . rawurlencode($language)), 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, 'message' => $message,
'error' => $error, 'error' => $error,
)); ));
?>
<main class="page-main dashboard-main"> $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');
<section class="panel"> if (count($currentNextTokens) > 0) {
<h2><?= h(t('app.download_settings', 'Download settings')) ?></h2> $tokenRowsHtml = '';
<form method="post" action="/admin-config?lang=<?= h($language) ?>" class="admin-form-grid"> foreach ($currentNextTokens as $nextToken) {
<input type="hidden" name="action" value="update_config"> $tokenRowsHtml .= RacketSandboxTemplate::renderFile('partials/config-token-row.html', array(
<label> 'status_class' => token_status_class($nextToken),
<?= h(t('app.racket_zip_chunk_kb', 'Racket installation max base64 chunk size (KiB)')) ?><br> 'token' => $nextToken['token'] ?? '',
<input type="number" name="racket_zip_max_base64_kb" min="1" step="1" value="<?= h($base64ChunkConfig['racket_zip_max_base64_kb']) ?>" required> 'created_at' => format_token_time($nextToken['created_at'] ?? 0),
</label> 'expires_at' => format_token_time($nextToken['expires_at'] ?? 0),
<label> )) . "\n";
<?= h(t('app.package_zip_chunk_kb', 'Package/module max base64 chunk size (KiB)')) ?><br> }
<input type="number" name="package_zip_max_base64_kb" min="1" step="1" value="<?= h($base64ChunkConfig['package_zip_max_base64_kb']) ?>" required>
</label>
<button type="submit"><?= h(t('app.save_configuration', 'Save configuration')) ?></button>
</form>
<p class="small"><?= 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.')) ?></p> $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.'),
));
}
<table> echo RacketSandboxTemplate::renderFile('config.html', array(
<tr> 'language' => $language,
<th><?= h(t('app.racket_zip_chunk_kb', 'Racket installation max base64 chunk size (KiB)')) ?></th> 'language_url' => rawurlencode($language),
<td><code><?= h($base64ChunkConfig['racket_zip_max_base64_kb']) ?></code> KiB</td> 'title' => t('app.configuration', 'Configuration'),
<td><?= h(t('app.effective_binary_chunk', 'Effective binary chunk')) ?>: <code><?= h(base64_binary_chunk_bytes_for_kib($base64ChunkConfig['racket_zip_max_base64_kb'])) ?></code> bytes</td> 'style_version' => $styleVersion,
</tr> 'header_html' => $headerHtml,
<tr> 'download_settings_label' => t('app.download_settings', 'Download settings'),
<th><?= h(t('app.package_zip_chunk_kb', 'Package/module max base64 chunk size (KiB)')) ?></th> 'racket_zip_chunk_label' => t('app.racket_zip_chunk_kb', 'Racket installation max base64 chunk size (KiB)'),
<td><code><?= h($base64ChunkConfig['package_zip_max_base64_kb']) ?></code> KiB</td> 'package_zip_chunk_label' => t('app.package_zip_chunk_kb', 'Package/module max base64 chunk size (KiB)'),
<td><?= h(t('app.effective_binary_chunk', 'Effective binary chunk')) ?>: <code><?= h(base64_binary_chunk_bytes_for_kib($base64ChunkConfig['package_zip_max_base64_kb'])) ?></code> bytes</td> 'racket_zip_max_base64_kb' => $base64ChunkConfig['racket_zip_max_base64_kb'],
</tr> 'package_zip_max_base64_kb' => $base64ChunkConfig['package_zip_max_base64_kb'],
<tr> 'save_configuration_label' => t('app.save_configuration', 'Save configuration'),
<th><?= h(t('app.racket_zip_source', 'Racket installation source')) ?></th> '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(
<td><code>config/racket.zip</code></td> 'chunk_size' => (string)intdiv(base64_binary_chunk_bytes_for_kib($base64ChunkConfig['racket_zip_max_base64_kb']), 1024),
<td><code><?= h(is_file(RACKET_ZIP_FILE) ? (string)filesize(RACKET_ZIP_FILE) : '0') ?></code> bytes</td> 'base64_chunk_size' => (string)$base64ChunkConfig['racket_zip_max_base64_kb'],
</tr> )),
<tr> 'racket_zip_base64_chunk_size' => t('app.base64_chunk_size_kib', '{{size}} KiB', array(
<th><?= h(t('app.racket_parts', 'Racket installation parts')) ?></th> 'size' => code_value($base64ChunkConfig['racket_zip_max_base64_kb']),
<td><code><?= h((string)($racketPartsManifest['part_count'] ?? 0)) ?></code></td> )),
<td> 'package_zip_base64_chunk_size' => t('app.base64_chunk_size_kib', '{{size}} KiB', array(
<?php if ($racketPartsCurrent): ?> 'size' => code_value($base64ChunkConfig['package_zip_max_base64_kb']),
<?= h(t('app.racket_parts_current', 'current')) ?>, )),
<?= h((string)($racketPartsManifest['created_at'] ?? '')) ?> 'racket_zip_effective_binary_chunk' => t('app.effective_binary_chunk_bytes', 'Effective binary chunk: {{bytes}} bytes', array(
<?php else: ?> 'bytes' => code_value(base64_binary_chunk_bytes_for_kib($base64ChunkConfig['racket_zip_max_base64_kb'])),
<?= h(t('app.racket_parts_missing', 'missing or outdated; save configuration to regenerate')) ?> )),
<?php endif; ?> 'package_zip_effective_binary_chunk' => t('app.effective_binary_chunk_bytes', 'Effective binary chunk: {{bytes}} bytes', array(
</td> 'bytes' => code_value(base64_binary_chunk_bytes_for_kib($base64ChunkConfig['package_zip_max_base64_kb'])),
</tr> )),
</table> 'racket_zip_source_label' => t('app.racket_zip_source', 'Racket installation source'),
</section> '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'),
<section class="panel"> )),
<h2><?= h(t('app.maintenance', 'Maintenance')) ?></h2> 'racket_parts_label' => t('app.racket_parts', 'Racket installation parts'),
'racket_part_count' => (string)($racketPartsManifest['part_count'] ?? 0),
<fieldset> 'racket_parts_status' => $racketPartsStatus,
<legend><?= h(t('app.next_tokens', 'Next tokens')) ?></legend> 'maintenance_label' => t('app.maintenance', 'Maintenance'),
'next_tokens_label' => t('app.next_tokens', 'Next tokens'),
<form method="post" action="/admin-config?lang=<?= h($language) ?>"> 'remove_expired_tokens_label' => t('app.remove_expired_tokens', 'Remove expired next tokens'),
<input type="hidden" name="action" value="cleanup_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.'),
<button type="submit"><?= h(t('app.remove_expired_tokens', 'Remove expired next tokens')) ?></button> 'current_next_tokens_label' => t('app.current_next_tokens', 'Current next tokens'),
</form> 'current_tokens_html' => $currentTokensHtml,
));
<p class="small">
<?= 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.')) ?>
</p>
<h3><?= h(t('app.current_next_tokens', 'Current next tokens')) ?></h3>
<?php if (count($currentNextTokens) > 0): ?>
<table>
<tr>
<th><?= h(t('app.token', 'Token')) ?></th>
<th><?= h(t('app.created_at', 'Created at')) ?></th>
<th><?= h(t('app.expires_at', 'Expires at')) ?></th>
</tr>
<?php foreach ($currentNextTokens as $nextToken): ?>
<tr class="<?= h(token_status_class($nextToken)) ?>">
<td><code><?= h($nextToken['token'] ?? '') ?></code></td>
<td><?= h(format_token_time($nextToken['created_at'] ?? 0)) ?></td>
<td><?= h(format_token_time($nextToken['expires_at'] ?? 0)) ?></td>
</tr>
<?php endforeach; ?>
</table>
<?php else: ?>
<p class="small"><?= h(t('app.no_current_next_tokens', 'No current next tokens.')) ?></p>
<?php endif; ?>
</fieldset>
</section>
</main>
</div>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
<?php
/*
* Shared runtime error reporting settings for public entrypoints.
*/
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
ini_set('log_errors', '1');
error_reporting(E_ALL);
View File
+75 -173
View File
@@ -24,11 +24,9 @@ require_once __DIR__ . '/private/languagestore.php';
require_once __DIR__ . '/private/nexttoken.php'; require_once __DIR__ . '/private/nexttoken.php';
require_once __DIR__ . '/private/promptstore.php'; require_once __DIR__ . '/private/promptstore.php';
require_once __DIR__ . '/private/usersettings.php'; require_once __DIR__ . '/private/usersettings.php';
require_once __DIR__ . '/private/viewdata.php';
ini_set('display_errors', '1'); require_once __DIR__ . '/config/reporting.php';
ini_set('display_startup_errors', '1');
ini_set('log_errors', '1');
error_reporting(E_ALL);
$DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite'; $DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite';
@@ -57,11 +55,11 @@ function get_value($name, $default = '')
return $_GET[$name] ?? $default; return $_GET[$name] ?? $default;
} }
function t($key, $fallback = null) function t($key, $fallback = null, $values = array())
{ {
global $languageStore, $language; global $languageStore, $language;
return $languageStore->translate($key, $language, $fallback); return $languageStore->translateFormat($key, $language, $values, $fallback);
} }
function current_scheme() function current_scheme()
@@ -154,64 +152,7 @@ $language = resolve_user_language(
$languageStore->supportedLanguages() $languageStore->supportedLanguages()
); );
$languageStore->seedDefaults(array( seed_template_translations($languageStore, 'index.html');
'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:'),
));
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = post_value('action'); $action = post_value('action');
@@ -260,7 +201,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$password = (string)post_value('password'); $password = (string)post_value('password');
$auth->setPassword($email, $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') { } elseif ($action === 'set_flags') {
$userId = (int)post_value('user_id'); $userId = (int)post_value('user_id');
$isAdmin = post_bool('is_admin'); $isAdmin = post_bool('is_admin');
@@ -281,13 +222,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$userId = (int)post_value('user_id'); $userId = (int)post_value('user_id');
if ($userId === $currentUser->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); $auth->deleteUser($userId);
$message = t('app.user_deleted', 'User deleted.'); $message = t('app.user_deleted', 'User deleted.');
} elseif ($action !== '') { } 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) { } catch (Throwable $e) {
@@ -345,20 +286,59 @@ if ($currentUser->isAdmin()) {
} }
header('Content-Type: text/html; charset=utf-8'); header('Content-Type: text/html; charset=utf-8');
?> $generatedLinkHtml = '';
<!doctype html>
<html lang="<?= h($language) ?>">
<head>
<meta charset="utf-8">
<title><?= h(t('app.title', 'Racket sandbox')) ?></title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div class="page"> 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'),
));
}
<?php $promptPanelHtml = '';
render_app_header(array(
if ($issuedLink !== '') {
$promptToolHtml = '';
if (count($bootstrapPrompts) > 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'), 'title' => t('app.title', 'Racket sandbox'),
'nav_items' => $headerNavItems, 'nav_items' => $headerNavItems,
'user' => $currentUser, 'user' => $currentUser,
@@ -373,98 +353,20 @@ render_app_header(array(
'message' => $message, 'message' => $message,
'error' => $error, 'error' => $error,
)); ));
?>
<main class="page-main dashboard-main"> echo RacketSandboxTemplate::renderFile('index.html', array(
'language' => $language,
<section class="panel"> 'language_url' => rawurlencode($language),
<h2><?= h(t('app.bootstrap_link', 'Bootstrap link')) ?></h2> 'title' => t('app.title', 'Racket sandbox'),
'header_html' => $headerHtml,
<fieldset> 'bootstrap_link_label' => t('app.bootstrap_link', 'Bootstrap link'),
<legend><?= h(t('app.generate_bootstrap_link', 'Generate bootstrap link')) ?></legend> 'generate_bootstrap_link_label' => t('app.generate_bootstrap_link', 'Generate bootstrap link'),
'bootstrap_result_class' => $issuedLink !== '' ? 'has-prompt' : '',
<div class="bootstrap-result-grid <?= $issuedLink !== '' ? 'has-prompt' : '' ?>"> 'ttl_minutes_label' => t('app.ttl_minutes', 'TTL in minutes'),
<div> 'bootstrap_ttl_minutes' => $bootstrapTtlMinutes,
<form method="post" action="/?lang=<?= h($language) ?>"> 'bootstrap_ttl_min_minutes' => BOOTSTRAP_TTL_MIN_MINUTES,
<input type="hidden" name="action" value="issue_bootstrap"> 'bootstrap_ttl_max_minutes' => BOOTSTRAP_TTL_MAX_MINUTES,
'ttl_range_help' => t('app.ttl_range_help', 'Allowed range: 30 minutes to 8 hours.'),
<label> 'generated_link_html' => $generatedLinkHtml,
<?= h(t('app.ttl_minutes', 'TTL in minutes')) ?><br> 'prompt_panel_html' => $promptPanelHtml,
<input type="number" ));
name="ttl_minutes"
value="<?= h($bootstrapTtlMinutes) ?>"
min="<?= h(BOOTSTRAP_TTL_MIN_MINUTES) ?>"
max="<?= h(BOOTSTRAP_TTL_MAX_MINUTES) ?>">
</label>
<p class="small"><?= h(t('app.ttl_range_help', 'Allowed range: 30 minutes to 8 hours.')) ?></p>
<button type="submit"><?= h(t('app.generate_bootstrap_link', 'Generate bootstrap link')) ?></button>
</form>
<?php if ($issuedLink !== ''): ?>
<h3><?= h(t('app.generated_link', 'Generated link')) ?></h3>
<div class="generated-link-row">
<a href="<?= h($issuedLink) ?>" target="_blank" rel="noopener noreferrer"><?= h($issuedLink) ?></a>
<button type="button"
class="js-copy-button"
data-copy-text="<?= h($issuedLink) ?>"
data-copy-label="<?= h(t('app.copy', 'Copy')) ?>"
data-copied-label="<?= h(t('app.copied', 'Copied')) ?>">
<?= h(t('app.copy', 'Copy')) ?>
</button>
</div>
<?php endif; ?>
</div>
<?php if ($issuedLink !== ''): ?>
<div>
<h3><?= h(t('app.copy_full_prompt', 'Copy full prompt')) ?></h3>
<p>
<?= h(t('app.bootstrap_prompt_help', 'Choose one of your prompts. The placeholder {{bootstrap-racket-link}} is replaced by the generated bootstrap link.')) ?>
</p>
<?php if (count($bootstrapPrompts) > 0): ?>
<div class="bootstrap-prompt-tool">
<label>
<?= h(t('app.select_prompt', 'Select prompt')) ?><br>
<select id="bootstrapPromptSelect">
<?php foreach ($bootstrapPrompts as $prompt): ?>
<option value="<?= h($prompt['id']) ?>"><?= h($prompt['name']) ?></option>
<?php endforeach; ?>
</select>
</label>
<textarea id="bootstrapPromptOutput" readonly rows="12"></textarea>
<button type="button"
class="js-copy-button"
data-copy-target="bootstrapPromptOutput"
data-copy-label="<?= h(t('app.copy', 'Copy')) ?>"
data-copied-label="<?= h(t('app.copied', 'Copied')) ?>">
<?= h(t('app.copy_full_prompt', 'Copy full prompt')) ?>
</button>
</div>
<script type="application/json" id="bootstrapPromptData">
<?= 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) ?>
</script>
<?php else: ?>
<p class="small"><?= h(t('app.no_bootstrap_prompts', 'No personal prompts are available for this language. Copy a default prompt first from prompt management.')) ?></p>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</fieldset>
</section>
</main>
</div>
<script src="/js/clipboard.js" defer></script>
<script src="/js/bootstrap-prompt.js" defer></script>
</body>
</html>
+24 -69
View File
@@ -4,21 +4,14 @@
*/ */
require_once __DIR__ . '/private/auth.php'; require_once __DIR__ . '/private/auth.php';
require_once __DIR__ . '/private/Template.php';
ini_set('display_errors', '1'); require_once __DIR__ . '/config/reporting.php';
ini_set('display_startup_errors', '1');
ini_set('log_errors', '1');
error_reporting(E_ALL);
$auth = new RacketSandboxAuth(__DIR__ . '/data/racket-sandbox.sqlite'); $auth = new RacketSandboxAuth(__DIR__ . '/data/racket-sandbox.sqlite');
$error = ''; $error = '';
function h($s)
{
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function detect_login_language($supported, $fallback) function detect_login_language($supported, $fallback)
{ {
$header = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''; $header = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
@@ -61,26 +54,10 @@ function detect_login_language($supported, $fallback)
} }
$pageTitle = 'Racket ChatGPT Agent Sandbox Creator'; $pageTitle = 'Racket ChatGPT Agent Sandbox Creator';
$texts = array( $templateData = RacketSandboxTemplate::dataFile('login.html');
'en' => array( $texts = $templateData['translations'] ?? 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',
),
);
$language = detect_login_language($texts, 'en'); $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') { if ($auth->currentUser() !== null && $_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /'); header('Location: /');
@@ -98,46 +75,24 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
header('Content-Type: text/html; charset=utf-8'); header('Content-Type: text/html; charset=utf-8');
?>
<!doctype html>
<html lang="<?= h($language) ?>">
<head>
<meta charset="utf-8">
<title><?= h($pageTitle) ?></title>
<link rel="stylesheet" href="/styles.css?v=<?= h($styleVersion) ?>">
</head>
<body class="simple-doc login-page">
<main class="login-layout"> $loginText = $texts[$language];
<section class="login-panel"> $errorHtml = $error === ''
<h1><?= h($pageTitle) ?></h1> ? ''
: RacketSandboxTemplate::renderFile('partials/message.html', array(
'class' => 'error',
'message' => $error,
));
<?php if ($error !== ''): ?> echo RacketSandboxTemplate::renderFile('login.html', array(
<div class="error"><?= h($error) ?></div> 'language' => $language,
<?php endif; ?> 'page_title' => $pageTitle,
'style_version' => $styleVersion,
<form method="post" action="/login.php"> 'error_html' => $errorHtml,
'email_label' => $loginText['email'],
<label> 'password_label' => $loginText['password'],
<?= h($texts[$language]['email']) ?><br> 'login_label' => $loginText['login'],
<input type="email" name="email" autocomplete="username" required> 'account_title' => $loginText['account_title'],
</label> 'account_text' => $loginText['account_text'],
'account_link' => $loginText['account_link'],
<label> ));
<?= h($texts[$language]['password']) ?><br>
<input type="password" name="password" autocomplete="current-password" required>
</label>
<button type="submit"><?= h($texts[$language]['login']) ?></button>
</form>
</section>
<aside class="login-request-panel">
<h2><?= h($texts[$language]['account_title']) ?></h2>
<p><?= h($texts[$language]['account_text']) ?></p>
<p><a href="https://racket.discourse.group/"><?= h($texts[$language]['account_link']) ?></a></p>
</aside>
</main>
</body>
</html>
+41 -77
View File
@@ -23,10 +23,7 @@
* - Eén next-id per gegenereerde HTML-pagina. * - Eén next-id per gegenereerde HTML-pagina.
*/ */
ini_set('display_errors', '1'); require_once __DIR__ . '/config/reporting.php';
ini_set('display_startup_errors', '1');
ini_set('log_errors', '1');
error_reporting(E_ALL);
require_once __DIR__ . '/private/nexttoken.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/base64config.php';
require_once __DIR__ . '/private/lib/catalog-http.php'; require_once __DIR__ . '/private/lib/catalog-http.php';
require_once __DIR__ . '/private/lib/racket-data.php'; require_once __DIR__ . '/private/lib/racket-data.php';
require_once __DIR__ . '/private/Template.php';
define('DATA_DIR', __DIR__ . '/data'); define('DATA_DIR', __DIR__ . '/data');
define('CATALOG_PACKAGE_BASE', 'https://pkgs.racket-lang.org/pkg/'); 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) function fail_html($message, $status = 500)
{ {
html_response( html_response(
'<!doctype html><html><head><meta charset="utf-8">' . RacketSandboxTemplate::renderFile('protocol-error.html', array(
'<title>Package error</title></head><body>' . 'title' => 'Package error',
'<h1>Package error</h1>' . 'message' => $message,
'<pre>' . h($message) . '</pre>' . )),
'</body></html>',
$status $status
); );
} }
@@ -362,79 +359,46 @@ function serve_package_page()
'part' => $n, 'part' => $n,
)); ));
$rows .= $rows .= RacketSandboxTemplate::renderFile('partials/package-part-row.html', array(
'<tr>' . 'number' => $n,
'<td>' . h($n) . '</td>' . 'base64_bytes' => (string)$part['base64_bytes'],
'<td>' . h((string)$part['base64_bytes']) . '</td>' . 'url' => $url,
'<td><a href="' . h($url) . '">' . h($url) . '</a></td>' . )) . "\n";
'</tr>' . "\n";
} }
html_response('<!doctype html> $sourceRows = '';
<html lang="nl"> $sourceValues = array(
<head> 'catalog source' => $info['source'],
<meta charset="utf-8"> 'repo url' => $info['repo_url'],
<title>Package ' . h($package) . '</title> 'fetch status' => $zipInfo['status'] ?? '',
<link rel="stylesheet" href="/styles.css"> 'default branch' => $zipInfo['default_branch'] ?? '',
</head> 'head sha' => $zipInfo['head_sha'] ?? '',
<body class="simple-doc"> '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' => '<code>' . h((string)PACKAGE_ZIP_MAX_BASE64_KB) . '</code> KiB (<code>' . h((string)PACKAGE_ZIP_MAX_BASE64_BYTES) . '</code> bytes)',
'binary chunk size' => '<code>' . h((string)($manifest['binary_chunk_bytes'] ?? '')) . '</code> bytes',
'part count' => (string)$manifest['part_count'],
'next id' => $NEXT_ID,
);
<h1>Package ' . h($package) . '</h1> foreach ($sourceValues as $label => $value) {
$valueHtml = strpos((string)$value, '<code>') !== false
? (string)$value
: '<code>' . h((string)$value) . '</code>';
<p> $sourceRows .= RacketSandboxTemplate::renderFile('partials/package-source-row.html', array(
Deze pagina is HTML. Alle part-links hieronder geven <code>text/plain</code> 'label' => $label,
met base64-inhoud terug. Dezelfde <code>next</code> wordt gebruikt voor alle 'value_html' => $valueHtml,
part-links op deze pagina. )) . "\n";
</p> }
<h2>Bron</h2> html_response(RacketSandboxTemplate::renderFile('package.html', array(
'package' => $package,
<table> 'source_rows_html' => $sourceRows,
<tr><th>catalog source</th><td><code>' . h($info['source']) . '</code></td></tr> 'part_rows_html' => $rows,
<tr><th>repo url</th><td><code>' . h($info['repo_url']) . '</code></td></tr> )));
<tr><th>fetch status</th><td><code>' . h($zipInfo['status'] ?? '') . '</code></td></tr>
<tr><th>default branch</th><td><code>' . h($zipInfo['default_branch'] ?? '') . '</code></td></tr>
<tr><th>head sha</th><td><code>' . h($zipInfo['head_sha'] ?? '') . '</code></td></tr>
<tr><th>zip file</th><td><code>' . h($zipInfo['zip_file'] ?? '') . '</code></td></tr>
<tr><th>zip bytes</th><td><code>' . h((string)($zipInfo['zip_bytes'] ?? '')) . '</code></td></tr>
<tr><th>zip sha256</th><td><code>' . h($zipInfo['zip_sha256'] ?? '') . '</code></td></tr>
<tr><th>parts status</th><td><code>' . h($manifest['parts_status'] ?? '') . '</code></td></tr>
<tr><th>max base64 part size</th><td><code>' . h((string)PACKAGE_ZIP_MAX_BASE64_KB) . '</code> KiB (<code>' . h((string)PACKAGE_ZIP_MAX_BASE64_BYTES) . '</code> bytes)</td></tr>
<tr><th>binary chunk size</th><td><code>' . h((string)($manifest['binary_chunk_bytes'] ?? '')) . '</code> bytes</td></tr>
<tr><th>part count</th><td><code>' . h((string)$manifest['part_count']) . '</code></td></tr>
<tr><th>next id</th><td><code>' . h($NEXT_ID) . '</code></td></tr>
</table>
<h2>Base64 parts</h2>
<table>
<thead>
<tr>
<th>part</th>
<th>base64 bytes</th>
<th>text/plain URL</th>
</tr>
</thead>
<tbody>
' . $rows . '
</tbody>
</table>
<h2>Reconstructie in de sandbox</h2>
<pre>
# download alle links als:
# ' . h($package) . '.part.000001.b64
# ' . h($package) . '.part.000002.b64
# enz.
cat ' . h($package) . '.part.*.b64 &gt; ' . h($package) . '.zip.b64
base64 -d ' . h($package) . '.zip.b64 &gt; ' . h($package) . '.zip
raco pkg install --auto ./' . h($package) . '.zip
</pre>
</body>
</html>');
} }
function serve_package_part() function serve_package_part()
+100
View File
@@ -0,0 +1,100 @@
<?php
/*
* Small explicit template renderer.
*
* Templates use escaped {{name}} placeholders and raw {{{name}}} placeholders.
* Values are provided as an array, so templates do not depend on globals.
*/
final class RacketSandboxTemplate
{
const DATA_DELIMITER = "\n===\n";
public static function renderFile($template, $values)
{
$parts = self::readFile($template);
return self::renderString($parts['html'], $values);
}
public static function dataFile($template)
{
$parts = self::readFile($template);
$json = trim($parts['data']);
if ($json === '') {
return array();
}
$data = json_decode($json, true);
if (!is_array($data)) {
throw new RuntimeException('Template data is invalid JSON: ' . $template);
}
return $data;
}
public static function translationsFile($template)
{
$data = self::dataFile($template);
$translations = $data['translations'] ?? array();
if (!is_array($translations)) {
throw new RuntimeException('Template translations must be an object: ' . $template);
}
return $translations;
}
public static function renderString($source, $values)
{
$source = preg_replace_callback('/\{\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}\}/', function ($match) use ($values) {
return self::value($values, $match[1]);
}, $source);
return preg_replace_callback('/\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/', function ($match) use ($values) {
return self::escape(self::value($values, $match[1]));
}, $source);
}
public static function escape($value)
{
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
private static function value($values, $name)
{
if (!is_array($values) || !array_key_exists($name, $values)) {
return '';
}
return (string)$values[$name];
}
private static function readFile($template)
{
$file = __DIR__ . '/views/' . ltrim((string)$template, '/');
if (!is_file($file)) {
throw new RuntimeException('Template not found: ' . $template);
}
$source = file_get_contents($file);
if ($source === false) {
throw new RuntimeException('Template could not be read: ' . $template);
}
if (strpos($source, "===\n") === 0) {
$chunks = array('', substr($source, 4));
} else {
$chunks = explode(self::DATA_DELIMITER, $source, 2);
}
return array(
'html' => $chunks[0],
'data' => $chunks[1] ?? '',
);
}
}
+9 -13
View File
@@ -14,6 +14,8 @@
* Outside code should not inspect DB rows directly. * Outside code should not inspect DB rows directly.
*/ */
require_once __DIR__ . '/Template.php';
class RacketSandboxAuthException extends Exception class RacketSandboxAuthException extends Exception
{ {
} }
@@ -696,7 +698,7 @@ class RacketSandboxAuth
$this->messageHtml( $this->messageHtml(
'Login required', 'Login required',
'Please log in to continue.', 'Please log in to continue.',
'<p><a href="/login.php">Login</a></p>' RacketSandboxTemplate::renderFile('partials/login-link.html', array('login_label' => 'Login'))
); );
} }
@@ -705,18 +707,12 @@ class RacketSandboxAuth
http_response_code(200); http_response_code(200);
header('Content-Type: text/html; charset=utf-8'); header('Content-Type: text/html; charset=utf-8');
echo '<!doctype html> echo RacketSandboxTemplate::renderFile('simple-message.html', array(
<html lang="en"> 'language' => 'en',
<head> 'title' => $title,
<meta charset="utf-8"> 'message' => $message,
<title>' . htmlspecialchars($title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</title> 'extra_html' => $extra,
</head> ));
<body>
<h1>' . htmlspecialchars($title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</h1>
<p>' . htmlspecialchars($message, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</p>
' . $extra . '
</body>
</html>';
exit; exit;
} }
} }
+117 -65
View File
@@ -5,12 +5,19 @@
* Shared page header renderer for logged-in application pages. * Shared page header renderer for logged-in application pages.
*/ */
require_once __DIR__ . '/Template.php';
function app_header_h($s) function app_header_h($s)
{ {
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); return RacketSandboxTemplate::escape($s);
} }
function render_app_header($options) function render_app_header($options)
{
echo app_header_html($options);
}
function app_header_html($options)
{ {
$title = (string)($options['title'] ?? ''); $title = (string)($options['title'] ?? '');
$navItems = $options['nav_items'] ?? array(); $navItems = $options['nav_items'] ?? array();
@@ -26,69 +33,114 @@ function render_app_header($options)
$logoutLabel = (string)($options['logout_label'] ?? ''); $logoutLabel = (string)($options['logout_label'] ?? '');
$message = (string)($options['message'] ?? ''); $message = (string)($options['message'] ?? '');
$error = (string)($options['error'] ?? ''); $error = (string)($options['error'] ?? '');
?>
<header class="page-header">
<div class="page-titlebar">
<h1><?= app_header_h($title) ?></h1>
<nav class="header-nav" aria-label="<?= app_header_h($title) ?> navigation"> return RacketSandboxTemplate::renderFile('app-header.html', array(
<?php foreach ($navItems as $item): ?> 'title' => $title,
<?php if (!empty($item['separator_before'])): ?> 'nav_html' => app_header_nav_html($navItems),
<span class="nav-separator" aria-hidden="true">|</span> 'logout_html' => app_header_logout_html($logoutAction, $logoutLabel),
<?php endif; ?> 'user_html' => app_header_user_html($user, $userPrefix, $adminLabel),
<?php if (!empty($item['active'])): ?> 'language_html' => app_header_language_html($languageLabel, $language, $languages, $languageAction, $languageHidden),
<strong><?= app_header_h($item['label'] ?? '') ?></strong> 'message_html' => app_header_status_html('message', $message),
<?php else: ?> 'error_html' => app_header_status_html('error', $error),
<a href="<?= app_header_h($item['url'] ?? '#') ?>"><?= app_header_h($item['label'] ?? '') ?></a> ));
<?php endif; ?> }
<?php endforeach; ?>
function app_header_status_html($class, $message)
<?php if ($logoutAction !== '' && $logoutLabel !== ''): ?> {
<span class="nav-separator" aria-hidden="true">|</span> if ($message === '') {
<form class="header-action-form" method="post" action="<?= app_header_h($logoutAction) ?>"> return '';
<input type="hidden" name="action" value="logout"> }
<button type="submit"><?= app_header_h($logoutLabel) ?></button>
</form> return RacketSandboxTemplate::renderFile('partials/message.html', array(
<?php endif; ?> 'class' => $class,
'message' => $message,
<?php if ($user !== null): ?> ));
<span class="nav-separator" aria-hidden="true">|</span> }
<span class="nav-user">
<?php if ($userPrefix !== ''): ?> function app_header_nav_html($navItems)
<?= app_header_h($userPrefix) ?> {
<?php endif; ?> $html = '';
<?= app_header_h($user->displayName()) ?>
<?php if ($user->isAdmin()): ?> foreach ($navItems as $item) {
<span class="small">(<?= app_header_h($adminLabel) ?>)</span> if (!empty($item['separator_before'])) {
<?php endif; ?> $html .= RacketSandboxTemplate::renderFile('partials/header-separator.html', array()) . "\n";
</span> }
<?php endif; ?>
if (!empty($item['active'])) {
<form class="header-language-form" method="get" action="<?= app_header_h($languageAction) ?>"> $html .= RacketSandboxTemplate::renderFile('partials/header-nav-active.html', array(
<?php foreach ($languageHidden as $name => $value): ?> 'label' => $item['label'] ?? '',
<input type="hidden" name="<?= app_header_h($name) ?>" value="<?= app_header_h($value) ?>"> )) . "\n";
<?php endforeach; ?> continue;
<label> }
<?= app_header_h($languageLabel) ?>
<select name="lang" onchange="this.form.submit()"> $html .= RacketSandboxTemplate::renderFile('partials/header-nav-link.html', array(
<?php foreach ($languages as $lang => $label): ?> 'url' => $item['url'] ?? '#',
<option value="<?= app_header_h($lang) ?>" <?= $lang === $language ? 'selected' : '' ?>> 'label' => $item['label'] ?? '',
<?= app_header_h($label) ?> )) . "\n";
</option> }
<?php endforeach; ?>
</select> return $html;
</label> }
</form>
</nav> function app_header_logout_html($logoutAction, $logoutLabel)
</div> {
if ($logoutAction === '' || $logoutLabel === '') {
<?php if ($message !== ''): ?> return '';
<div class="message"><?= app_header_h($message) ?></div> }
<?php endif; ?>
return RacketSandboxTemplate::renderFile('partials/header-logout.html', array(
<?php if ($error !== ''): ?> 'logout_action' => $logoutAction,
<div class="error"><?= app_header_h($error) ?></div> 'logout_label' => $logoutLabel,
<?php endif; ?> )) . "\n";
</header> }
<?php
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";
} }
+37
View File
@@ -79,6 +79,43 @@ class LanguageStore
return $fallback !== null ? (string)$fallback : $key; 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) public function setTranslation($key, $language, $text)
{ {
$key = $this->safeKey($key); $key = $this->safeKey($key);
+11
View File
@@ -0,0 +1,11 @@
<?php
/*
* Helpers for reading non-rendered data from view templates.
*/
require_once __DIR__ . '/Template.php';
function seed_template_translations($languageStore, $template)
{
$languageStore->seedDefaults(RacketSandboxTemplate::translationsFile($template));
}
+15
View File
@@ -0,0 +1,15 @@
<header class="page-header">
<div class="page-titlebar">
<h1>{{title}}</h1>
<nav class="header-nav" aria-label="{{title}} navigation">
{{{nav_html}}}
{{{logout_html}}}
{{{user_html}}}
{{{language_html}}}
</nav>
</div>
{{{message_html}}}
{{{error_html}}}
</header>
+69
View File
@@ -0,0 +1,69 @@
<!doctype html>
<html lang="nl">
<head>
<meta charset="utf-8">
<title>Racket bootstrap</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body class="simple-doc">
<h1>Racket bootstrap</h1>
<p>
<code>racket.zip</code> is vooraf via de configuratiepagina gesplitst naar
parts in de map <code>data</code>.
</p>
<p>
Package index: <a href="{{pkg_url}}">Racket package index</a>
</p>
<p>
Bronbestand: <code>config/racket.zip</code><br>
Bronbestand bytes: <code>{{zip_size}}</code><br>
Maximale base64 part-grootte: <code>{{max_base64_kb}}</code> KiB (<code>{{max_base64_bytes}}</code> bytes)<br>
Binaire chunk-grootte: <code>{{binary_chunk_bytes}}</code> bytes<br>
Aantal parts: <code>{{part_count}}</code><br>
Parts gemaakt op: <code>{{created_at}}</code><br>
next-id voor alle links op deze pagina: <code>{{next_id}}</code>
</p>
<p>
Elke link hieronder geeft <strong>text/plain</strong> met de
<strong>base64-representatie van een binair part</strong>.
De URL bevat alleen een nummer, geen bestandsnaam en geen extensie.
</p>
<table>
<thead>
<tr>
<th>#</th>
<th>partnummer</th>
<th>bytes</th>
<th>base64 text/plain URL</th>
</tr>
</thead>
<tbody>
{{{part_rows_html}}}
</tbody>
</table>
<h2>Reconstructie in de sandbox</h2>
<p>
Decodeer ieder base64-part afzonderlijk naar een binair part. Plak daarna de
binaire parts in numerieke volgorde aan elkaar.
</p>
<pre>
base64 -d part-000001.txt &gt; part-000001
base64 -d part-000002.txt &gt; part-000002
base64 -d part-000003.txt &gt; part-000003
# enzovoort
cat part-* &gt; racket.zip
unzip racket.zip -d /tmp/racket
</pre>
</body>
</html>
+229
View File
@@ -0,0 +1,229 @@
<!doctype html>
<html lang="{{language}}">
<head>
<meta charset="utf-8">
<title>{{title}}</title>
<link rel="stylesheet" href="/css/styles.css?v={{style_version}}">
</head>
<body>
<div class="page">
{{{header_html}}}
<main class="page-main dashboard-main">
<section class="panel">
<h2>{{download_settings_label}}</h2>
<form method="post" action="/admin-config?lang={{language_url}}" class="admin-form-grid">
<input type="hidden" name="action" value="update_config">
<label>
{{racket_zip_chunk_label}}<br>
<input type="number" name="racket_zip_max_base64_kb" min="1" step="1" value="{{racket_zip_max_base64_kb}}" required>
</label>
<label>
{{package_zip_chunk_label}}<br>
<input type="number" name="package_zip_max_base64_kb" min="1" step="1" value="{{package_zip_max_base64_kb}}" required>
</label>
<button type="submit">{{save_configuration_label}}</button>
</form>
<p class="small">{{chunk_size_hint}}</p>
<table>
<tr>
<th>{{racket_zip_chunk_label}}</th>
<td>{{{racket_zip_base64_chunk_size}}}</td>
<td>{{{racket_zip_effective_binary_chunk}}}</td>
</tr>
<tr>
<th>{{package_zip_chunk_label}}</th>
<td>{{{package_zip_base64_chunk_size}}}</td>
<td>{{{package_zip_effective_binary_chunk}}}</td>
</tr>
<tr>
<th>{{racket_zip_source_label}}</th>
<td><code>config/racket.zip</code></td>
<td>{{{racket_zip_file_size}}}</td>
</tr>
<tr>
<th>{{racket_parts_label}}</th>
<td><code>{{racket_part_count}}</code></td>
<td>{{racket_parts_status}}</td>
</tr>
</table>
</section>
<section class="panel">
<h2>{{maintenance_label}}</h2>
<fieldset>
<legend>{{next_tokens_label}}</legend>
<form method="post" action="/admin-config?lang={{language_url}}">
<input type="hidden" name="action" value="cleanup_tokens">
<button type="submit">{{remove_expired_tokens_label}}</button>
</form>
<p class="small">{{cleanup_help}}</p>
<h3>{{current_next_tokens_label}}</h3>
{{{current_tokens_html}}}
</fieldset>
</section>
</main>
</div>
</body>
</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."
}
}
}
+237
View File
@@ -0,0 +1,237 @@
<!doctype html>
<html lang="{{language}}">
<head>
<meta charset="utf-8">
<title>{{title}}</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<div class="page">
{{{header_html}}}
<main class="page-main dashboard-main">
<section class="panel">
<h2>{{bootstrap_link_label}}</h2>
<fieldset>
<legend>{{generate_bootstrap_link_label}}</legend>
<div class="bootstrap-result-grid {{bootstrap_result_class}}">
<div>
<form method="post" action="/?lang={{language_url}}">
<input type="hidden" name="action" value="issue_bootstrap">
<label>
{{ttl_minutes_label}}<br>
<input type="number"
name="ttl_minutes"
value="{{bootstrap_ttl_minutes}}"
min="{{bootstrap_ttl_min_minutes}}"
max="{{bootstrap_ttl_max_minutes}}">
</label>
<p class="small">{{ttl_range_help}}</p>
<button type="submit">{{generate_bootstrap_link_label}}</button>
</form>
{{{generated_link_html}}}
</div>
{{{prompt_panel_html}}}
</div>
</fieldset>
</section>
</main>
</div>
<script src="/js/clipboard.js" defer></script>
<script src="/js/bootstrap-prompt.js" defer></script>
</body>
</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}}"
}
}
}
+61
View File
@@ -0,0 +1,61 @@
<!doctype html>
<html lang="{{language}}">
<head>
<meta charset="utf-8">
<title>{{page_title}}</title>
<link rel="stylesheet" href="/css/styles.css?v={{style_version}}">
</head>
<body class="simple-doc login-page">
<main class="login-layout">
<section class="login-panel">
<h1>{{page_title}}</h1>
{{{error_html}}}
<form method="post" action="/login.php">
<label>
{{email_label}}<br>
<input type="email" name="email" autocomplete="username" required>
</label>
<label>
{{password_label}}<br>
<input type="password" name="password" autocomplete="current-password" required>
</label>
<button type="submit">{{login_label}}</button>
</form>
</section>
<aside class="login-request-panel">
<h2>{{account_title}}</h2>
<p>{{account_text}}</p>
<p><a href="https://racket.discourse.group/">{{account_link}}</a></p>
</aside>
</main>
</body>
</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"
}
}
}
+53
View File
@@ -0,0 +1,53 @@
<!doctype html>
<html lang="nl">
<head>
<meta charset="utf-8">
<title>Package {{package}}</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body class="simple-doc">
<h1>Package {{package}}</h1>
<p>
Deze pagina is HTML. Alle part-links hieronder geven <code>text/plain</code>
met base64-inhoud terug. Dezelfde <code>next</code> wordt gebruikt voor alle
part-links op deze pagina.
</p>
<h2>Bron</h2>
<table>
{{{source_rows_html}}}
</table>
<h2>Base64 parts</h2>
<table>
<thead>
<tr>
<th>part</th>
<th>base64 bytes</th>
<th>text/plain URL</th>
</tr>
</thead>
<tbody>
{{{part_rows_html}}}
</tbody>
</table>
<h2>Reconstructie in de sandbox</h2>
<pre>
# download alle links als:
# {{package}}.part.000001.b64
# {{package}}.part.000002.b64
# enz.
cat {{package}}.part.*.b64 &gt; {{package}}.zip.b64
base64 -d {{package}}.zip.b64 &gt; {{package}}.zip
raco pkg install --auto ./{{package}}.zip
</pre>
</body>
</html>
@@ -0,0 +1,5 @@
<tr class="{{status_class}}">
<td><code>{{token}}</code></td>
<td>{{created_at}}</td>
<td>{{expires_at}}</td>
</tr>
@@ -0,0 +1,8 @@
<table>
<tr>
<th>{{token_label}}</th>
<th>{{created_at_label}}</th>
<th>{{expires_at_label}}</th>
</tr>
{{{token_rows_html}}}
</table>
@@ -0,0 +1 @@
<span class="small">({{admin_label}})</span>
@@ -0,0 +1 @@
<input type="hidden" name="{{name}}" value="{{value}}">
@@ -0,0 +1,9 @@
<form class="header-language-form" method="get" action="{{language_action}}">
{{{hidden_inputs_html}}}
<label>
{{language_label}}
<select name="lang" onchange="this.form.submit()">
{{{language_options_html}}}
</select>
</label>
</form>
@@ -0,0 +1,5 @@
<span class="nav-separator" aria-hidden="true">|</span>
<form class="header-action-form" method="post" action="{{logout_action}}">
<input type="hidden" name="action" value="logout">
<button type="submit">{{logout_label}}</button>
</form>
@@ -0,0 +1 @@
<strong>{{label}}</strong>
@@ -0,0 +1 @@
<a href="{{url}}">{{label}}</a>
@@ -0,0 +1 @@
<span class="nav-separator" aria-hidden="true">|</span>
+6
View File
@@ -0,0 +1,6 @@
<span class="nav-separator" aria-hidden="true">|</span>
<span class="nav-user">
{{user_prefix}}
{{display_name}}
{{{admin_html}}}
</span>
@@ -0,0 +1,11 @@
<h3>{{generated_link_label}}</h3>
<div class="generated-link-row">
<a href="{{issued_link}}" target="_blank" rel="noopener noreferrer">{{issued_link}}</a>
<button type="button"
class="js-copy-button"
data-copy-text="{{issued_link}}"
data-copy-label="{{copy_label}}"
data-copied-label="{{copied_label}}">
{{copy_label}}
</button>
</div>
@@ -0,0 +1,21 @@
<div class="bootstrap-prompt-tool">
<label>
{{select_prompt_label}}<br>
<select id="bootstrapPromptSelect">
{{{prompt_options_html}}}
</select>
</label>
<textarea id="bootstrapPromptOutput" readonly rows="12"></textarea>
<button type="button"
class="js-copy-button"
data-copy-target="bootstrapPromptOutput"
data-copy-label="{{copy_label}}"
data-copied-label="{{copied_label}}">
{{copy_full_prompt_label}}
</button>
</div>
<script type="application/json" id="bootstrapPromptData">
{{{bootstrap_prompt_json}}}
</script>
@@ -0,0 +1,6 @@
<div>
<h3>{{copy_full_prompt_label}}</h3>
<p>{{bootstrap_prompt_help}}</p>
{{{prompt_tool_html}}}
</div>
+1
View File
@@ -0,0 +1 @@
<p><a href="/login.php">{{login_label}}</a></p>
+1
View File
@@ -0,0 +1 @@
<div class="{{class}}">{{message}}</div>
@@ -0,0 +1,6 @@
<tr id="pkg-{{id}}">
<td>{{index}}</td>
<td class="pkg-name">
<a class="pkg-link" href="{{url}}">{{name}}</a>
</td>
</tr>
@@ -0,0 +1,5 @@
<tr>
<td>{{number}}</td>
<td>{{base64_bytes}}</td>
<td><a href="{{url}}">{{url}}</a></td>
</tr>
@@ -0,0 +1 @@
<tr><th>{{label}}</th><td>{{{value_html}}}</td></tr>
+1
View File
@@ -0,0 +1 @@
<p class="{{class}}">{{text}}</p>
@@ -0,0 +1,4 @@
<div class="default-admin-notice">
<strong>{{badge}}</strong>
<span>{{hint}}</span>
</div>
@@ -0,0 +1,15 @@
<details class="create-drawer">
<summary>{{create_default_label}}</summary>
<form method="post" action="/prompts?lang={{language_url}}&mode=defaults">
<input type="hidden" name="action" value="create_default">
<label>{{default_key_label}}<br><input type="text" name="default_key" placeholder="bootstrap-racket"></label>
<label>{{name_label}}<br><input type="text" name="name"></label>
<label>{{language_label}}<br>
<select name="language">
{{{language_options_html}}}
</select>
</label>
<label>{{prompt_content_label}}<br><textarea name="content" rows="7"></textarea></label>
<button type="submit">{{create_default_label}}</button>
</form>
</details>
@@ -0,0 +1,6 @@
<form method="post" action="/prompts?lang={{language_url}}&mode=defaults"
onsubmit="return confirm({{{confirm_json}}});">
<input type="hidden" name="action" value="delete_default">
<input type="hidden" name="default_id" value="{{id}}">
<button type="submit">{{delete_label}}</button>
</form>
@@ -0,0 +1,17 @@
<div class="prompt-list-item default-prompt-item">
<button type="button"
class="prompt-select js-open-prompt"
data-kind="default"
data-id="{{id}}">
<span class="prompt-name">{{name}}</span>
<span class="prompt-subline">{{{metadata}}}</span>
</button>
<form method="post" action="/prompts?lang={{language_url}}&mode=personal">
<input type="hidden" name="action" value="copy_default">
<input type="hidden" name="default_id" value="{{id}}">
<button type="submit">{{copy_label}}</button>
</form>
{{{admin_delete_html}}}
</div>
@@ -0,0 +1,15 @@
<div class="prompt-list-item">
<button type="button"
class="prompt-select js-open-prompt"
data-kind="personal"
data-id="{{id}}">
<span class="prompt-name">{{name}}</span>
<span class="prompt-subline">{{metadata}}</span>
</button>
<form method="post" action="/prompts?lang={{language_url}}&mode=personal"
onsubmit="return confirm({{{confirm_json}}});">
<input type="hidden" name="action" value="delete_prompt">
<input type="hidden" name="prompt_id" value="{{id}}">
<button type="submit">{{delete_label}}</button>
</form>
</div>
@@ -0,0 +1,6 @@
<tr>
<td>{{index}}</td>
<td>{{number}}</td>
<td>{{size}}</td>
<td><a href="{{url}}">{{url}}</a></td>
</tr>
@@ -0,0 +1 @@
<option value="{{value}}"{{{selected}}}>{{label}}</option>
@@ -0,0 +1,6 @@
<form method="post" action="/users?lang={{language_url}}"
onsubmit="return confirm({{{confirm_json}}});">
<input type="hidden" name="action" value="delete_user">
<input type="hidden" name="user_id" value="{{user_id}}">
<button type="submit">{{delete_user_label}}</button>
</form>
+27
View File
@@ -0,0 +1,27 @@
<tr>
<td colspan="7">
<form method="post" action="/users?lang={{language_url}}" class="user-row-form">
<input type="hidden" name="action" value="update_user">
<input type="hidden" name="user_id" value="{{user_id}}">
<label>{{full_name_label}}<br><input type="text" name="full_name" value="{{full_name}}" required></label>
<label>{{email_label}}<br><input type="email" name="email" value="{{email}}" required></label>
<label><input type="checkbox" name="is_admin" value="1"{{{is_admin_checked}}}> {{admin_label}}</label>
<label><input type="checkbox" name="is_enabled" value="1"{{{is_enabled_checked}}}> {{enabled_label}}</label>
<span>{{created_at}}</span>
<span>{{last_login_at}}</span>
<button type="submit">{{update_user_label}}</button>
</form>
<div class="user-row-actions">
<form method="post" action="/users?lang={{language_url}}">
<input type="hidden" name="action" value="set_password">
<input type="hidden" name="email" value="{{email}}">
<label>{{new_password_label}}<br><input type="password" name="password" autocomplete="new-password"></label>
<button type="submit">{{change_password_label}}</button>
</form>
{{{delete_html}}}
</div>
</td>
</tr>
@@ -0,0 +1 @@
<p class="small">{{cannot_delete_self}}</p>
+462
View File
@@ -0,0 +1,462 @@
<!doctype html>
<html lang="{{language}}">
<head>
<meta charset="utf-8">
<title>{{title}}</title>
<link rel="stylesheet" href="/css/styles.css?v={{style_version}}">
</head>
<body>
<div class="page">
{{{header_html}}}
<main class="page-main prompt-workbench">
<aside class="prompt-sidebar panel">
<div class="prompt-tabs" role="tablist" aria-label="Prompt lists">
<button type="button" class="prompt-tab {{defaults_tab_active_class}}" data-tab="defaults" role="tab" aria-selected="{{defaults_tab_selected}}">
{{available_defaults_label}}
</button>
<button type="button" class="prompt-tab {{personal_tab_active_class}}" data-tab="personal" role="tab" aria-selected="{{personal_tab_selected}}">
{{your_prompts_label}}
</button>
</div>
<section class="prompt-tab-panel {{defaults_tab_active_class}}" id="tab-defaults" role="tabpanel">
{{{default_admin_notice_html}}}
<div class="sidebar-actions">
<form method="post" action="/prompts?lang={{language_url}}&mode=personal">
<input type="hidden" name="action" value="copy_all_defaults">
<input type="hidden" name="language" value="{{language}}">
<button type="submit">{{copy_all_label}}</button>
</form>
</div>
<div class="prompt-list">
{{{default_prompts_html}}}
{{{no_defaults_html}}}
</div>
{{{create_default_html}}}
</section>
<section class="prompt-tab-panel {{personal_tab_active_class}}" id="tab-personal" role="tabpanel">
<div class="prompt-list">
{{{personal_prompts_html}}}
{{{no_personal_html}}}
</div>
<details class="create-drawer">
<summary>{{create_personal_label}}</summary>
<form method="post" action="/prompts?lang={{language_url}}&mode=personal">
<input type="hidden" name="action" value="create_prompt">
<label>{{name_label}}<br><input type="text" name="name"></label>
<label>{{language_label}}<br>
<select name="language">
{{{language_options_html}}}
</select>
</label>
<label>{{prompt_content_label}}<br><textarea name="content" rows="7"></textarea></label>
<button type="submit">{{create_personal_label}}</button>
</form>
</details>
</section>
</aside>
<section id="promptViewer" class="prompt-viewer panel is-empty">
<div class="viewer-empty">
{{select_prompt_label}}
</div>
<div class="viewer-shell">
<div class="viewer-header">
<div>
<div class="viewer-title" id="viewerTitle">{{prompt_label}}</div>
<div class="version-meta" id="viewerMeta"></div>
</div>
<button type="button" id="editPromptButton">{{edit_label}}</button>
</div>
<pre id="viewerContent" class="viewer-content"></pre>
</div>
</section>
</main>
<footer class="page-footer">
<a href="/">{{back_label}}</a>
</footer>
</div>
<div id="promptModalBackdrop" class="modal-backdrop">
<div class="prompt-modal">
<form id="promptModalForm" class="edit-mode" method="post" action="/prompts?lang={{language_url}}&mode={{mode_url}}">
<div class="modal-header">
<div>
<div class="modal-title" id="modalTitle">{{prompt_label}}</div>
<div class="version-meta" id="modalMeta"></div>
</div>
<button type="button" id="cancelEditButton">{{close_label}}</button>
</div>
<div class="modal-toolbar">
<button type="button" id="versionNewerButton">{{newer_previous_label}}</button>
<button type="button" id="versionOlderButton">{{older_previous_label}}</button>
<label>
{{diff_view_label}}
<select id="diffMode">
<option value="plain">{{diff_plain_label}}</option>
<option value="all" selected>{{diff_all_label}}</option>
<option value="same">{{diff_same_label}}</option>
<option value="added">{{diff_added_label}}</option>
<option value="deleted">{{diff_deleted_label}}</option>
<option value="changed">{{diff_changed_label}}</option>
</select>
</label>
<span id="versionIndicator" class="version-meta"></span>
</div>
<div class="modal-body">
<div class="edit-pane">
<div class="modal-form-grid">
<label>
{{name_label}}<br>
<input type="text" id="modalName" name="name">
</label>
<label>
{{language_label}}<br>
<select id="modalLanguage" name="language">
{{{language_options_plain_html}}}
</select>
</label>
</div>
<div id="defaultKeyRow" class="default-key-row">
<label>
{{default_key_label}}<br>
<input type="text" id="modalDefaultKey" name="default_key">
</label>
</div>
<label class="content-label">
<span>{{prompt_content_label}}</span>
<textarea id="modalContent" name="content"></textarea>
</label>
</div>
<div class="version-pane">
<div>
<strong>{{previous_version_label}}</strong>
<div id="selectedVersionMeta" class="version-meta"></div>
</div>
<div class="version-content" id="versionContent"></div>
</div>
</div>
<div class="modal-footer">
<input type="hidden" id="modalAction" name="action" value="">
<input type="hidden" id="modalPromptId" name="prompt_id" value="">
<input type="hidden" id="modalDefaultId" name="default_id" value="">
<label>
<input type="checkbox" name="create_version" value="1" checked>
{{store_version_label}}
</label>
<label>
{{version_note_label}}
<input type="text" name="version_note" value="editor edit">
</label>
<button type="submit">{{save_label}}</button>
<button type="button" id="snapshotButton">{{store_snapshot_label}}</button>
<button type="button" id="restoreVersionButton">{{restore_version_label}}</button>
<button type="button" id="deleteVersionButton">{{delete_version_label}}</button>
</div>
</form>
</div>
</div>
<form id="modalAuxForm" method="post" action="/prompts?lang={{language_url}}&mode={{mode_url}}" class="hidden">
<input type="hidden" id="auxAction" name="action" value="">
<input type="hidden" id="auxPromptId" name="prompt_id" value="">
<input type="hidden" id="auxDefaultId" name="default_id" value="">
<input type="hidden" id="auxVersionNo" name="version_no" value="">
<input type="hidden" id="auxVersionNote" name="version_note" value="">
</form>
<script type="application/json" id="promptDataJson">
{{{prompt_data_json}}}
</script>
<script type="application/json" id="promptTextJson">
{{{prompt_text_json}}}
</script>
<script src="/js/prompt-editor.js?v={{prompt_editor_version}}" defer></script>
</body>
</html>
===
{
"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"
}
}
}
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{{title}}</title>
</head>
<body>
<h1>{{title}}</h1>
<pre>{{message}}</pre>
</body>
</html>
+37
View File
@@ -0,0 +1,37 @@
<!doctype html>
<html lang="nl">
<head>
<meta charset="utf-8">
<title>Racket package index</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body class="simple-doc package-index-page">
<h1>Racket package index</h1>
<p>
Volledige HTML-index van de Racket package catalogus op basis van
<code>pkgs-all</code>. De package-naam is de ophaallink via
<code>rktsndbx.dijkewijk.nl</code>.
</p>
<p>
Aantal packages: <code>{{package_count}}</code><br>
next-id voor alle package-links op deze pagina:
<code>{{next_id}}</code>
</p>
<table>
<thead>
<tr>
<th>#</th>
<th>package</th>
</tr>
</thead>
<tbody>
{{{package_rows_html}}}
</tbody>
</table>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="{{language}}">
<head>
<meta charset="utf-8">
<title>{{title}}</title>
</head>
<body>
<h1>{{title}}</h1>
<p>{{message}}</p>
{{{extra_html}}}
</body>
</html>
+175
View File
@@ -0,0 +1,175 @@
<!doctype html>
<html lang="{{language}}">
<head>
<meta charset="utf-8">
<title>{{title}}</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<div class="page">
{{{header_html}}}
<main class="page-main dashboard-main">
<section class="panel">
<h2>{{create_user_label}}</h2>
<form method="post" action="/users?lang={{language_url}}" class="admin-form-grid">
<input type="hidden" name="action" value="create_user">
<label>{{full_name_label}}<br><input type="text" name="full_name" required></label>
<label>{{email_label}}<br><input type="email" name="email" required></label>
<label>{{password_label}}<br><input type="password" name="password" autocomplete="new-password" required></label>
<label><input type="checkbox" name="is_admin" value="1"> {{admin_label}}</label>
<label><input type="checkbox" name="is_enabled" value="1" checked> {{enabled_label}}</label>
<button type="submit">{{create_user_label}}</button>
</form>
</section>
<section class="panel">
<h2>{{user_management_label}}</h2>
<table>
<thead>
<tr>
<th>{{full_name_label}}</th>
<th>{{email_label}}</th>
<th>{{admin_label}}</th>
<th>{{enabled_label}}</th>
<th>{{created_label}}</th>
<th>{{last_login_label}}</th>
<th>{{actions_label}}</th>
</tr>
</thead>
<tbody>
{{{user_rows_html}}}
</tbody>
</table>
</section>
</main>
</div>
</body>
</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"
}
}
}
+165 -360
View File
@@ -18,11 +18,9 @@ require_once __DIR__ . '/private/header.php';
require_once __DIR__ . '/private/languagestore.php'; require_once __DIR__ . '/private/languagestore.php';
require_once __DIR__ . '/private/promptstore.php'; require_once __DIR__ . '/private/promptstore.php';
require_once __DIR__ . '/private/usersettings.php'; require_once __DIR__ . '/private/usersettings.php';
require_once __DIR__ . '/private/viewdata.php';
ini_set('display_errors', '1'); require_once __DIR__ . '/config/reporting.php';
ini_set('display_startup_errors', '1');
ini_set('log_errors', '1');
error_reporting(E_ALL);
$DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite'; $DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite';
@@ -41,11 +39,11 @@ function h($s)
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); 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; global $languageStore, $language;
return $languageStore->translate($key, $language, $fallback); return $languageStore->translateFormat($key, $language, $values, $fallback);
} }
function post_value($name, $default = '') function post_value($name, $default = '')
@@ -116,70 +114,7 @@ $language = resolve_user_language(
$store->supportedLanguages() $store->supportedLanguages()
); );
$languageStore->seedDefaults(array( seed_template_translations($languageStore, 'prompts.html');
'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'),
));
$mode = get_value('mode', 'personal'); $mode = get_value('mode', 'personal');
@@ -326,7 +261,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$message = 'Default prompt deleted.'; $message = 'Default prompt deleted.';
} }
} elseif ($action !== '') { } elseif ($action !== '') {
throw new Exception('Unknown action: ' . $action); throw new Exception(t('prompts.unknown_action', 'Unknown action: {{action}}', array('action' => $action)));
} }
} catch (Throwable $e) { } catch (Throwable $e) {
$error = $e->getMessage(); $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(); $promptEditorVersion = @filemtime(__DIR__ . '/js/prompt-editor.js') ?: time();
header('Content-Type: text/html; charset=utf-8'); header('Content-Type: text/html; charset=utf-8');
?>
<!doctype html>
<html lang="<?= h($language) ?>">
<head>
<meta charset="utf-8">
<title><?= h(t('prompts.title', 'Prompt administration')) ?></title>
<link rel="stylesheet" href="/styles.css?v=<?= h($styleVersion) ?>">
</head>
<body>
<div class="page"> $languageOptionsHtml = '';
$languageOptionsPlainHtml = '';
<?php foreach ($store->supportedLanguages() as $lang) {
render_app_header(array( $languageOptionsHtml .= RacketSandboxTemplate::renderFile('partials/select-option.html', array(
'title' => t('prompts.title', 'Prompt administration'), 'value' => $lang,
'nav_items' => $headerNavItems, 'selected' => $lang === $language ? ' selected' : '',
'user' => $user, 'label' => $store->languageLabel($lang),
'admin_label' => t('app.admin', 'admin'), )) . "\n";
'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,
));
?>
<main class="page-main prompt-workbench"> $languageOptionsPlainHtml .= RacketSandboxTemplate::renderFile('partials/select-option.html', array(
'value' => $lang,
'selected' => '',
'label' => $store->languageLabel($lang),
)) . "\n";
}
<aside class="prompt-sidebar panel"> $defaultAdminNoticeHtml = '';
<div class="prompt-tabs" role="tablist" aria-label="Prompt lists">
<button type="button" class="prompt-tab <?= $mode === 'defaults' ? 'active' : '' ?>" data-tab="defaults" role="tab" aria-selected="<?= $mode === 'defaults' ? 'true' : 'false' ?>">
<?= h(t('prompts.available_defaults', 'Available default prompts')) ?>
</button>
<button type="button" class="prompt-tab <?= $mode !== 'defaults' ? 'active' : '' ?>" data-tab="personal" role="tab" aria-selected="<?= $mode !== 'defaults' ? 'true' : 'false' ?>">
<?= h(t('prompts.your_prompts', 'Your prompts')) ?>
</button>
</div>
<section class="prompt-tab-panel <?= $mode === 'defaults' ? 'active' : '' ?>" id="tab-defaults" role="tabpanel"> if ($user->isAdmin()) {
<?php if ($user->isAdmin()): ?> $defaultAdminNoticeHtml = RacketSandboxTemplate::renderFile('partials/prompt-admin-notice.html', array(
<div class="default-admin-notice"> 'badge' => t('prompts.default_admin_badge', 'Admin default prompts'),
<strong><?= h(t('prompts.default_admin_badge', 'Admin default prompts')) ?></strong> 'hint' => t('prompts.default_admin_hint', 'You are editing global default prompts. Users can copy these to their own prompts.'),
<span><?= h(t('prompts.default_admin_hint', 'You are editing global default prompts. Users can copy these to their own prompts.')) ?></span> ));
</div> }
<?php endif; ?>
<div class="sidebar-actions"> $defaultPromptsHtml = '';
<form method="post" action="/prompts?lang=<?= h($language) ?>&mode=personal">
<input type="hidden" name="action" value="copy_all_defaults">
<input type="hidden" name="language" value="<?= h($language) ?>">
<button type="submit"><?= h(t('prompts.copy_all', 'Copy all')) ?></button>
</form>
</div>
<div class="prompt-list"> foreach ($defaultPrompts as $default) {
<?php foreach ($defaultPrompts as $default): ?> $adminDeleteHtml = '';
<div class="prompt-list-item default-prompt-item">
<button type="button"
class="prompt-select js-open-prompt"
data-kind="default"
data-id="<?= h($default->id()) ?>">
<span class="prompt-name"><?= h($default->name()) ?></span>
<span class="prompt-subline"><code><?= h($default->defaultKey()) ?></code> · <?= h(fmt_time($default->updatedAt())) ?></span>
</button>
<form method="post" action="/prompts?lang=<?= h($language) ?>&mode=personal"> if ($user->isAdmin()) {
<input type="hidden" name="action" value="copy_default"> $adminDeleteHtml = RacketSandboxTemplate::renderFile('partials/prompt-default-delete.html', array(
<input type="hidden" name="default_id" value="<?= h($default->id()) ?>"> 'language_url' => rawurlencode($language),
<button type="submit"><?= h(t('prompts.copy', 'copy')) ?></button> 'confirm_json' => json_encode(t('prompts.delete_default_confirm', 'Delete default prompt {{name}}?', array(
</form> 'name' => $default->name(),
)), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT),
'name' => $default->name(),
'id' => $default->id(),
'delete_label' => t('prompts.delete', 'delete'),
));
}
<?php if ($user->isAdmin()): ?> $defaultPromptsHtml .= RacketSandboxTemplate::renderFile('partials/prompt-default-item.html', array(
<form method="post" action="/prompts?lang=<?= h($language) ?>&mode=defaults" 'language_url' => rawurlencode($language),
onsubmit="return confirm('<?= h(t('prompts.delete_default_confirm', 'Delete default prompt')) ?> <?= h($default->name()) ?>?');"> 'id' => $default->id(),
<input type="hidden" name="action" value="delete_default"> 'name' => $default->name(),
<input type="hidden" name="default_id" value="<?= h($default->id()) ?>"> 'default_key' => $default->defaultKey(),
<button type="submit"><?= h(t('prompts.delete', 'delete')) ?></button> 'metadata' => t('prompts.default_metadata', '{{default_key}} · {{updated_at}}', array(
</form> 'default_key' => '<code>' . h($default->defaultKey()) . '</code>',
<?php endif; ?> 'updated_at' => fmt_time($default->updatedAt()),
</div> )),
<?php endforeach; ?> 'copy_label' => t('prompts.copy', 'copy'),
'admin_delete_html' => $adminDeleteHtml,
)) . "\n";
}
<?php if (count($defaultPrompts) === 0): ?> $noDefaultsHtml = count($defaultPrompts) === 0
<p class="empty-state"><?= h(t('prompts.no_defaults', 'No default prompts for this language yet.')) ?></p> ? RacketSandboxTemplate::renderFile('partials/paragraph.html', array(
<?php endif; ?> 'class' => 'empty-state',
</div> 'text' => t('prompts.no_defaults', 'No default prompts for this language yet.'),
))
: '';
<?php if ($user->isAdmin()): ?> $createDefaultHtml = '';
<details class="create-drawer">
<summary><?= h(t('prompts.create_default', 'Create default prompt')) ?></summary>
<form method="post" action="/prompts?lang=<?= h($language) ?>&mode=defaults">
<input type="hidden" name="action" value="create_default">
<label><?= h(t('prompts.default_key', 'Default key')) ?><br><input type="text" name="default_key" placeholder="bootstrap-racket"></label>
<label><?= h(t('prompts.name', 'Name')) ?><br><input type="text" name="name"></label>
<label><?= h(t('prompts.language', 'Language')) ?><br>
<select name="language">
<?php foreach ($store->supportedLanguages() as $lang): ?>
<option value="<?= h($lang) ?>" <?= $lang === $language ? 'selected' : '' ?>>
<?= h($store->languageLabel($lang)) ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label><?= h(t('prompts.prompt_content', 'Prompt content')) ?><br><textarea name="content" rows="7"></textarea></label>
<button type="submit"><?= h(t('prompts.create_default', 'Create default prompt')) ?></button>
</form>
</details>
<?php endif; ?>
</section>
<section class="prompt-tab-panel <?= $mode !== 'defaults' ? 'active' : '' ?>" id="tab-personal" role="tabpanel"> if ($user->isAdmin()) {
<div class="prompt-list"> $createDefaultHtml = RacketSandboxTemplate::renderFile('partials/prompt-create-default.html', array(
<?php foreach ($personalPrompts as $prompt): ?> 'language_url' => rawurlencode($language),
<div class="prompt-list-item"> 'create_default_label' => t('prompts.create_default', 'Create default prompt'),
<button type="button" 'default_key_label' => t('prompts.default_key', 'Default key'),
class="prompt-select js-open-prompt" 'name_label' => t('prompts.name', 'Name'),
data-kind="personal" 'language_label' => t('prompts.language', 'Language'),
data-id="<?= h($prompt->id()) ?>"> 'language_options_html' => $languageOptionsHtml,
<span class="prompt-name"><?= h($prompt->name()) ?></span> 'prompt_content_label' => t('prompts.prompt_content', 'Prompt content'),
<span class="prompt-subline"><?= h($store->languageLabel($prompt->language())) ?> · <?= h(fmt_time($prompt->updatedAt())) ?></span> ));
</button> }
<form method="post" action="/prompts?lang=<?= h($language) ?>&mode=personal"
onsubmit="return confirm('<?= h(t('prompts.delete_prompt_confirm', 'Delete prompt')) ?> <?= h($prompt->name()) ?>?');">
<input type="hidden" name="action" value="delete_prompt">
<input type="hidden" name="prompt_id" value="<?= h($prompt->id()) ?>">
<button type="submit"><?= h(t('prompts.delete', 'delete')) ?></button>
</form>
</div>
<?php endforeach; ?>
<?php if (count($personalPrompts) === 0): ?> $personalPromptsHtml = '';
<p class="empty-state"><?= h(t('prompts.no_personal', 'No personal prompts yet for this language.')) ?></p>
<?php endif; ?>
</div>
<details class="create-drawer"> foreach ($personalPrompts as $prompt) {
<summary><?= h(t('prompts.create_personal', 'Create personal prompt')) ?></summary> $personalPromptsHtml .= RacketSandboxTemplate::renderFile('partials/prompt-personal-item.html', array(
<form method="post" action="/prompts?lang=<?= h($language) ?>&mode=personal"> 'language_url' => rawurlencode($language),
<input type="hidden" name="action" value="create_prompt"> 'id' => $prompt->id(),
<label><?= h(t('prompts.name', 'Name')) ?><br><input type="text" name="name"></label> 'name' => $prompt->name(),
<label><?= h(t('prompts.language', 'Language')) ?><br> 'metadata' => t('prompts.personal_metadata', '{{language}} · {{updated_at}}', array(
<select name="language"> 'language' => $store->languageLabel($prompt->language()),
<?php foreach ($store->supportedLanguages() as $lang): ?> 'updated_at' => fmt_time($prompt->updatedAt()),
<option value="<?= h($lang) ?>" <?= $lang === $language ? 'selected' : '' ?>> )),
<?= h($store->languageLabel($lang)) ?> 'confirm_json' => json_encode(t('prompts.delete_prompt_confirm', 'Delete prompt {{name}}?', array(
</option> 'name' => $prompt->name(),
<?php endforeach; ?> )), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT),
</select> 'delete_label' => t('prompts.delete', 'delete'),
</label> )) . "\n";
<label><?= h(t('prompts.prompt_content', 'Prompt content')) ?><br><textarea name="content" rows="7"></textarea></label> }
<button type="submit"><?= h(t('prompts.create_personal', 'Create prompt')) ?></button>
</form>
</details>
</section>
</aside>
<section id="promptViewer" class="prompt-viewer panel is-empty"> $noPersonalHtml = count($personalPrompts) === 0
<div class="viewer-empty"> ? RacketSandboxTemplate::renderFile('partials/paragraph.html', array(
<?= h(t('prompts.select_prompt', 'Select a prompt on the left to view it.')) ?> 'class' => 'empty-state',
</div> 'text' => t('prompts.no_personal', 'No personal prompts yet for this language.'),
))
: '';
<div class="viewer-shell"> $promptTextJson = json_encode(array(
<div class="viewer-header">
<div>
<div class="viewer-title" id="viewerTitle"><?= h(t('prompts.prompt', 'Prompt')) ?></div>
<div class="version-meta" id="viewerMeta"></div>
</div>
<button type="button" id="editPromptButton"><?= h(t('prompts.edit', 'Edit')) ?></button>
</div>
<pre id="viewerContent" class="viewer-content"></pre>
</div>
</section>
</main>
<footer class="page-footer">
<a href="/"><?= h(t('prompts.back', 'Back to Racket sandbox')) ?></a>
</footer>
</div>
<div id="promptModalBackdrop" class="modal-backdrop">
<div class="prompt-modal">
<form id="promptModalForm" class="edit-mode" method="post" action="/prompts?lang=<?= h($language) ?>&mode=<?= h($mode) ?>">
<div class="modal-header">
<div>
<div class="modal-title" id="modalTitle"><?= h(t('prompts.prompt', 'Prompt')) ?></div>
<div class="version-meta" id="modalMeta"></div>
</div>
<button type="button" id="cancelEditButton"><?= h(t('prompts.close', 'Close')) ?></button>
</div>
<div class="modal-toolbar">
<button type="button" id="versionNewerButton"><?= h(t('prompts.newer_previous', 'newer previous version')) ?></button>
<button type="button" id="versionOlderButton"><?= h(t('prompts.older_previous', 'older previous version')) ?></button>
<label>
<?= h(t('prompts.diff_view', 'Diff view:')) ?>
<select id="diffMode">
<option value="plain"><?= h(t('prompts.diff_plain', 'text, no diff')) ?></option>
<option value="all" selected><?= h(t('prompts.diff_all', 'all diff')) ?></option>
<option value="same"><?= h(t('prompts.diff_same', 'unchanged only')) ?></option>
<option value="added"><?= h(t('prompts.diff_added', 'additions only')) ?></option>
<option value="deleted"><?= h(t('prompts.diff_deleted', 'deletions only')) ?></option>
<option value="changed"><?= h(t('prompts.diff_changed', 'changes only')) ?></option>
</select>
</label>
<span id="versionIndicator" class="version-meta"></span>
</div>
<div class="modal-body">
<div class="edit-pane">
<div class="modal-form-grid">
<label>
<?= h(t('prompts.name', 'Name')) ?><br>
<input type="text" id="modalName" name="name">
</label>
<label>
<?= h(t('prompts.language', 'Language')) ?><br>
<select id="modalLanguage" name="language">
<?php foreach ($store->supportedLanguages() as $lang): ?>
<option value="<?= h($lang) ?>"><?= h($store->languageLabel($lang)) ?></option>
<?php endforeach; ?>
</select>
</label>
</div>
<div id="defaultKeyRow" class="default-key-row">
<label>
<?= h(t('prompts.default_key', 'Default key')) ?><br>
<input type="text" id="modalDefaultKey" name="default_key">
</label>
</div>
<label class="content-label">
<span><?= h(t('prompts.prompt_content', 'Prompt content')) ?></span>
<textarea id="modalContent" name="content"></textarea>
</label>
</div>
<div class="version-pane">
<div>
<strong><?= h(t('prompts.previous_version', 'Previous version')) ?></strong>
<div id="selectedVersionMeta" class="version-meta"></div>
</div>
<div class="version-content" id="versionContent"></div>
</div>
</div>
<div class="modal-footer">
<input type="hidden" id="modalAction" name="action" value="">
<input type="hidden" id="modalPromptId" name="prompt_id" value="">
<input type="hidden" id="modalDefaultId" name="default_id" value="">
<label>
<input type="checkbox" name="create_version" value="1" checked>
<?= h(t('prompts.store_version', 'store this edit as a new version')) ?>
</label>
<label>
<?= h(t('prompts.version_note', 'Version note:')) ?>
<input type="text" name="version_note" value="editor edit">
</label>
<button type="submit"><?= h(t('prompts.save', 'Save')) ?></button>
<button type="button" id="snapshotButton"><?= h(t('prompts.store_snapshot', 'Store snapshot')) ?></button>
<button type="button" id="restoreVersionButton"><?= h(t('prompts.restore_version', 'Restore selected version')) ?></button>
<button type="button" id="deleteVersionButton"><?= h(t('prompts.delete_version', 'Delete selected version')) ?></button>
</div>
</form>
</div>
</div>
<form id="modalAuxForm" method="post" action="/prompts?lang=<?= h($language) ?>&mode=<?= h($mode) ?>" class="hidden">
<input type="hidden" id="auxAction" name="action" value="">
<input type="hidden" id="auxPromptId" name="prompt_id" value="">
<input type="hidden" id="auxDefaultId" name="default_id" value="">
<input type="hidden" id="auxVersionNo" name="version_no" value="">
<input type="hidden" id="auxVersionNote" name="version_note" value="">
</form>
<script type="application/json" id="promptDataJson">
<?= json_encode($promptData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>
</script>
<script type="application/json" id="promptTextJson">
<?= json_encode(array(
'prompt_not_found' => t('prompts.prompt_not_found', 'Prompt not found'), 'prompt_not_found' => t('prompts.prompt_not_found', 'Prompt not found'),
'no_previous_versions' => t('prompts.no_previous_versions', 'No previous versions stored.'), 'no_previous_versions' => t('prompts.no_previous_versions', 'No previous versions stored.'),
'no_lines_for_view' => t('prompts.no_lines_for_view', 'No lines for this view.'), 'no_lines_for_view' => t('prompts.no_lines_for_view', 'No lines for this view.'),
@@ -712,9 +453,73 @@ render_app_header(array(
'of' => t('prompts.of', 'of'), 'of' => t('prompts.of', 'of'),
'old' => t('prompts.old', 'old'), 'old' => t('prompts.old', 'old'),
'new' => t('prompts.new', 'new'), 'new' => t('prompts.new', 'new'),
), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?> ), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
</script>
<script src="/js/prompt-editor.js?v=<?= h($promptEditorVersion) ?>" defer></script>
</body> $headerHtml = app_header_html(array(
</html> '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,
));
+23 -83
View File
@@ -31,14 +31,12 @@
* links op die pagina gebruikt. * links op die pagina gebruikt.
*/ */
ini_set('display_errors', '1'); require_once __DIR__ . '/config/reporting.php';
ini_set('display_startup_errors', '1');
ini_set('log_errors', '1');
error_reporting(E_ALL);
require_once __DIR__ . '/private/nexttoken.php'; require_once __DIR__ . '/private/nexttoken.php';
require_once __DIR__ . '/private/base64config.php'; require_once __DIR__ . '/private/base64config.php';
require_once __DIR__ . '/private/racketzip.php'; require_once __DIR__ . '/private/racketzip.php';
require_once __DIR__ . '/private/Template.php';
$TOKENS = new NextTokenStore(__DIR__ . '/data/racket-sandbox.sqlite'); $TOKENS = new NextTokenStore(__DIR__ . '/data/racket-sandbox.sqlite');
@@ -131,9 +129,10 @@ function text_response($text, $status = 200)
function fail_html($message, $status = 500) function fail_html($message, $status = 500)
{ {
html_response( html_response(
'<!doctype html>' . RacketSandboxTemplate::renderFile('protocol-error.html', array(
'<html><head><meta charset="utf-8"><title>Error</title></head>' . 'title' => 'Error',
'<body><h1>Error</h1><pre>' . h($message) . '</pre></body></html>', 'message' => $message,
)),
$status $status
); );
} }
@@ -185,88 +184,29 @@ function serve_bootstrap()
$rows = ''; $rows = '';
foreach ($parts as $i => $part) { foreach ($parts as $i => $part) {
$rows .= $rows .= RacketSandboxTemplate::renderFile('partials/racket-part-row.html', array(
'<tr>' . 'index' => (string)($i + 1),
'<td>' . h((string)($i + 1)) . '</td>' . 'number' => $part['number'],
'<td>' . h($part['number']) . '</td>' . 'size' => (string)$part['size'],
'<td>' . h((string)$part['size']) . '</td>' . 'url' => $part['url'],
'<td><a href="' . h($part['url']) . '">' . h($part['url']) . '</a></td>' . )) . "\n";
'</tr>' . "\n";
} }
$zipSize = (int)($manifest['source_bytes'] ?? 0); $zipSize = (int)($manifest['source_bytes'] ?? 0);
$pkg_url = make_url('/racket-pkg-index'); $pkg_url = make_url('/racket-pkg-index');
html_response('<!doctype html> html_response(RacketSandboxTemplate::renderFile('bootstrap-racket.html', array(
<html lang="nl"> 'pkg_url' => $pkg_url,
<head> 'zip_size' => (string)$zipSize,
<meta charset="utf-8"> 'max_base64_kb' => (string)RACKET_ZIP_MAX_BASE64_KB,
<title>Racket bootstrap</title> 'max_base64_bytes' => (string)RACKET_ZIP_MAX_BASE64_BYTES,
<link rel="stylesheet" href="/styles.css"> 'binary_chunk_bytes' => (string)RACKET_ZIP_BINARY_CHUNK_BYTES,
</head> 'part_count' => (string)count($parts),
<body class="simple-doc"> 'created_at' => (string)($manifest['created_at'] ?? ''),
'next_id' => $NEXT_ID,
<h1>Racket bootstrap</h1> 'part_rows_html' => $rows,
)));
<p>
<code>racket.zip</code> is vooraf via de configuratiepagina gesplitst naar
parts in de map <code>data</code>.
</p>
<p>
Package index: <a href="' . $pkg_url . '"> Racket package index</a>
</p>
<p>
Bronbestand: <code>config/racket.zip</code><br>
Bronbestand bytes: <code>' . h((string)$zipSize) . '</code><br>
Maximale base64 part-grootte: <code>' . h((string)RACKET_ZIP_MAX_BASE64_KB) . '</code> KiB (<code>' . h((string)RACKET_ZIP_MAX_BASE64_BYTES) . '</code> bytes)<br>
Binaire chunk-grootte: <code>' . h((string)RACKET_ZIP_BINARY_CHUNK_BYTES) . '</code> bytes<br>
Aantal parts: <code>' . h((string)count($parts)) . '</code><br>
Parts gemaakt op: <code>' . h((string)($manifest['created_at'] ?? '')) . '</code><br>
next-id voor alle links op deze pagina: <code>' . h($NEXT_ID) . '</code>
</p>
<p>
Elke link hieronder geeft <strong>text/plain</strong> met de
<strong>base64-representatie van één binair part</strong>.
De URL bevat alleen een nummer, geen bestandsnaam en geen extensie.
</p>
<table>
<thead>
<tr>
<th>#</th>
<th>partnummer</th>
<th>bytes</th>
<th>base64 text/plain URL</th>
</tr>
</thead>
<tbody>
' . $rows . '
</tbody>
</table>
<h2>Reconstructie in de sandbox</h2>
<p>
Decodeer ieder base64-part afzonderlijk naar een binair part. Plak daarna de
binaire parts in numerieke volgorde aan elkaar.
</p>
<pre>
base64 -d part-000001.txt &gt; part-000001
base64 -d part-000002.txt &gt; part-000002
base64 -d part-000003.txt &gt; part-000003
# enzovoort
cat part-* &gt; racket.zip
unzip racket.zip -d /tmp/racket
</pre>
</body>
</html>');
} }
function serve_part() function serve_part()
+17 -51
View File
@@ -28,14 +28,12 @@
* RewriteRule ^racket-pkg-index$ rktpkgs.php [L,QSA] * RewriteRule ^racket-pkg-index$ rktpkgs.php [L,QSA]
*/ */
ini_set('display_errors', '1'); require_once __DIR__ . '/config/reporting.php';
ini_set('display_startup_errors', '1');
ini_set('log_errors', '1');
error_reporting(E_ALL);
require_once __DIR__ . '/private/nexttoken.php'; require_once __DIR__ . '/private/nexttoken.php';
require_once __DIR__ . '/private/lib/catalog-http.php'; require_once __DIR__ . '/private/lib/catalog-http.php';
require_once __DIR__ . '/private/lib/racket-data.php'; require_once __DIR__ . '/private/lib/racket-data.php';
require_once __DIR__ . '/private/Template.php';
$TOKENS = new NextTokenStore(__DIR__ . '/data/racket-sandbox.sqlite'); $TOKENS = new NextTokenStore(__DIR__ . '/data/racket-sandbox.sqlite');
@@ -96,9 +94,10 @@ function html_response($html, $status = 200)
function fail_html($message, $status = 500) function fail_html($message, $status = 500)
{ {
html_response( html_response(
'<!doctype html>' . RacketSandboxTemplate::renderFile('protocol-error.html', array(
'<html><head><meta charset="utf-8"><title>Error</title></head>' . 'title' => 'Error',
'<body><h1>Error</h1><pre>' . h($message) . '</pre></body></html>', 'message' => $message,
)),
$status $status
); );
} }
@@ -135,52 +134,19 @@ function serve_index()
foreach ($names as $i => $name) { foreach ($names as $i => $name) {
$url = make_url('/package', array('name' => $name)); $url = make_url('/package', array('name' => $name));
$rows .= $rows .= RacketSandboxTemplate::renderFile('partials/package-index-row.html', array(
'<tr id="pkg-' . h($name) . '">' . 'id' => $name,
'<td>' . h((string)($i + 1)) . '</td>' . 'index' => (string)($i + 1),
'<td class="pkg-name">' . 'url' => $url,
'<a class="pkg-link" href="' . h($url) . '">' . h($name) . '</a>' . 'name' => $name,
'</td>' . )) . "\n";
'</tr>' . "\n";
} }
html_response('<!doctype html> html_response(RacketSandboxTemplate::renderFile('racket-pkg-index.html', array(
<html lang="nl"> 'package_count' => (string)count($names),
<head> 'next_id' => $NEXT_ID,
<meta charset="utf-8"> 'package_rows_html' => $rows,
<title>Racket package index</title> )));
<link rel="stylesheet" href="/styles.css">
</head>
<body class="simple-doc package-index-page">
<h1>Racket package index</h1>
<p>
Volledige HTML-index van de Racket package catalogus op basis van
<code>pkgs-all</code>. De package-naam is de ophaallink via
<code>rktsndbx.dijkewijk.nl</code>.
</p>
<p>
Aantal packages: <code>' . h((string)count($names)) . '</code><br>
next-id voor alle package-links op deze pagina:
<code>' . h($NEXT_ID) . '</code>
</p>
<table>
<thead>
<tr>
<th>#</th>
<th>package</th>
</tr>
</thead>
<tbody>
' . $rows . '
</tbody>
</table>
</body>
</html>');
} }
$TOKENS->check_valid_next('html'); $TOKENS->check_valid_next('html');
+60 -128
View File
@@ -9,11 +9,9 @@ require_once __DIR__ . '/private/auth.php';
require_once __DIR__ . '/private/header.php'; require_once __DIR__ . '/private/header.php';
require_once __DIR__ . '/private/languagestore.php'; require_once __DIR__ . '/private/languagestore.php';
require_once __DIR__ . '/private/usersettings.php'; require_once __DIR__ . '/private/usersettings.php';
require_once __DIR__ . '/private/viewdata.php';
ini_set('display_errors', '1'); require_once __DIR__ . '/config/reporting.php';
ini_set('display_startup_errors', '1');
ini_set('log_errors', '1');
error_reporting(E_ALL);
$DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite'; $DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite';
@@ -30,11 +28,11 @@ function h($s)
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); 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; global $languageStore, $language;
return $languageStore->translate($key, $language, $fallback); return $languageStore->translateFormat($key, $language, $values, $fallback);
} }
function post_value($name, $default = '') function post_value($name, $default = '')
@@ -77,37 +75,7 @@ $language = resolve_user_language(
$languageStore->supportedLanguages() $languageStore->supportedLanguages()
); );
$languageStore->seedDefaults(array( seed_template_translations($languageStore, 'users.html');
'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'),
));
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = post_value('action'); $action = post_value('action');
@@ -169,20 +137,46 @@ foreach ($languageStore->supportedLanguages() as $lang) {
} }
header('Content-Type: text/html; charset=utf-8'); header('Content-Type: text/html; charset=utf-8');
?> $userRowsHtml = '';
<!doctype html>
<html lang="<?= h($language) ?>">
<head>
<meta charset="utf-8">
<title><?= h(t('app.user_management', 'User management')) ?></title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div class="page"> 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.'),
));
}
<?php $userRowsHtml .= RacketSandboxTemplate::renderFile('partials/user-row.html', array(
render_app_header(array( 'language_url' => 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'), 'title' => t('app.user_management', 'User management'),
'nav_items' => array( 'nav_items' => array(
array('label' => t('app.back_to_sandbox', 'Back to Racket sandbox'), 'url' => '/?lang=' . rawurlencode($language)), 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, 'message' => $message,
'error' => $error, 'error' => $error,
)); ));
?>
<main class="page-main dashboard-main"> echo RacketSandboxTemplate::renderFile('users.html', array(
'language' => $language,
<section class="panel"> 'language_url' => rawurlencode($language),
<h2><?= h(t('app.create_user', 'Create user')) ?></h2> 'title' => t('app.user_management', 'User management'),
'header_html' => $headerHtml,
<form method="post" action="/users?lang=<?= h($language) ?>" class="admin-form-grid"> 'create_user_label' => t('app.create_user', 'Create user'),
<input type="hidden" name="action" value="create_user"> 'full_name_label' => t('app.full_name', 'Full name'),
<label><?= h(t('app.full_name', 'Full name')) ?><br><input type="text" name="full_name" required></label> 'email_label' => t('app.email', 'Email'),
<label><?= h(t('app.email', 'Email')) ?><br><input type="email" name="email" required></label> 'password_label' => t('app.password', 'Password'),
<label><?= h(t('app.password', 'Password')) ?><br><input type="password" name="password" autocomplete="new-password" required></label> 'admin_label' => t('app.admin', 'Admin'),
<label><input type="checkbox" name="is_admin" value="1"> <?= h(t('app.admin', 'Admin')) ?></label> 'enabled_label' => t('app.enabled', 'Enabled'),
<label><input type="checkbox" name="is_enabled" value="1" checked> <?= h(t('app.enabled', 'Enabled')) ?></label> 'user_management_label' => t('app.user_management', 'User management'),
<button type="submit"><?= h(t('app.create_user', 'Create user')) ?></button> 'created_label' => t('app.created', 'Created'),
</form> 'last_login_label' => t('app.last_login', 'Last login'),
</section> 'actions_label' => t('app.actions', 'Actions'),
'user_rows_html' => $userRowsHtml,
<section class="panel"> ));
<h2><?= h(t('app.user_management', 'User management')) ?></h2>
<table>
<thead>
<tr>
<th><?= h(t('app.full_name', 'Full name')) ?></th>
<th><?= h(t('app.email', 'Email')) ?></th>
<th><?= h(t('app.admin', 'Admin')) ?></th>
<th><?= h(t('app.enabled', 'Enabled')) ?></th>
<th><?= h(t('app.created', 'Created')) ?></th>
<th><?= h(t('app.last_login', 'Last login')) ?></th>
<th><?= h(t('app.actions', 'Actions')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $managedUser): ?>
<tr>
<td colspan="7">
<form method="post" action="/users?lang=<?= h($language) ?>" class="user-row-form">
<input type="hidden" name="action" value="update_user">
<input type="hidden" name="user_id" value="<?= h($managedUser->id()) ?>">
<label><?= h(t('app.full_name', 'Full name')) ?><br><input type="text" name="full_name" value="<?= h($managedUser->fullName()) ?>" required></label>
<label><?= h(t('app.email', 'Email')) ?><br><input type="email" name="email" value="<?= h($managedUser->email()) ?>" required></label>
<label><input type="checkbox" name="is_admin" value="1" <?= $managedUser->isAdmin() ? 'checked' : '' ?>> <?= h(t('app.admin', 'Admin')) ?></label>
<label><input type="checkbox" name="is_enabled" value="1" <?= $managedUser->isEnabled() ? 'checked' : '' ?>> <?= h(t('app.enabled', 'Enabled')) ?></label>
<span><?= h(fmt_time($managedUser->createdAt())) ?></span>
<span><?= h(fmt_time($managedUser->lastLoginAt())) ?></span>
<button type="submit"><?= h(t('app.update_user', 'Update user')) ?></button>
</form>
<div class="user-row-actions">
<form method="post" action="/users?lang=<?= h($language) ?>">
<input type="hidden" name="action" value="set_password">
<input type="hidden" name="email" value="<?= h($managedUser->email()) ?>">
<label><?= h(t('app.new_password', 'New password')) ?><br><input type="password" name="password" autocomplete="new-password"></label>
<button type="submit"><?= h(t('app.change_password', 'Change password')) ?></button>
</form>
<?php if ($managedUser->id() !== $currentUser->id()): ?>
<form method="post" action="/users?lang=<?= h($language) ?>"
onsubmit="return confirm('<?= h(t('app.delete_user_confirm', 'Delete user')) ?> <?= h($managedUser->email()) ?>?');">
<input type="hidden" name="action" value="delete_user">
<input type="hidden" name="user_id" value="<?= h($managedUser->id()) ?>">
<button type="submit"><?= h(t('app.delete_user', 'Delete user')) ?></button>
</form>
<?php else: ?>
<p class="small"><?= h(t('app.cannot_delete_self', 'You cannot delete your own account.')) ?></p>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</section>
</main>
</div>
</body>
</html>