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/base64config.php';
require_once __DIR__ . '/private/racketzip.php';
require_once __DIR__ . '/private/viewdata.php';
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
ini_set('log_errors', '1');
error_reporting(E_ALL);
require_once __DIR__ . '/config/reporting.php';
$DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite';
@@ -36,11 +34,11 @@ function h($s)
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function t($key, $fallback = null)
function t($key, $fallback = null, $values = array())
{
global $languageStore, $language;
return $languageStore->translate($key, $language, $fallback);
return $languageStore->translateFormat($key, $language, $values, $fallback);
}
function post_value($name, $default = '')
@@ -84,51 +82,18 @@ function token_status_class($token)
: 'token-expired';
}
function code_value($value)
{
return '<code>' . h($value) . '</code>';
}
$language = resolve_user_language(
$userSettings,
$currentUser->id(),
$languageStore->supportedLanguages()
);
$languageStore->seedDefaults(array(
'app.title' => array('en' => 'Racket sandbox', 'nl' => 'Racket sandbox'),
'app.manage_prompts' => array('en' => 'Manage prompts', 'nl' => 'Prompts beheren'),
'app.user_management' => array('en' => 'User management', 'nl' => 'Gebruikersbeheer'),
'app.configuration' => array('en' => 'Configuration', 'nl' => 'Configuratie'),
'app.logout' => array('en' => 'Logout', 'nl' => 'Uitloggen'),
'app.language' => array('en' => 'Language', 'nl' => 'Taal'),
'app.logged_in_as' => array('en' => 'Logged in as:', 'nl' => 'Ingelogd als:'),
'app.admin' => array('en' => 'Admin', 'nl' => 'Admin'),
'app.back_to_sandbox' => array('en' => 'Back to Racket sandbox', 'nl' => 'Terug naar Racket sandbox'),
'app.download_settings' => array('en' => 'Download settings', 'nl' => 'Downloadinstellingen'),
'app.maintenance' => array('en' => 'Maintenance', 'nl' => 'Onderhoud'),
'app.next_tokens' => array('en' => 'Next tokens', 'nl' => 'Next-tokens'),
'app.current_next_tokens' => array('en' => 'Next tokens', 'nl' => 'Next-tokens'),
'app.token' => array('en' => 'Token', 'nl' => 'Token'),
'app.created_at' => array('en' => 'Created at', 'nl' => 'Aangemaakt op'),
'app.expires_at' => array('en' => 'Expires at', 'nl' => 'Verloopt op'),
'app.no_current_next_tokens' => array('en' => 'No next tokens.', 'nl' => 'Geen next-tokens.'),
'app.remove_expired_tokens' => array('en' => 'Remove expired next tokens', 'nl' => 'Verlopen next-tokens verwijderen'),
'app.racket_zip_chunk_kb' => array('en' => 'Racket installation max base64 chunk size (KiB)', 'nl' => 'Maximale base64-chunkgrootte Racket-installatie (KiB)'),
'app.package_zip_chunk_kb' => array('en' => 'Package/module max base64 chunk size (KiB)', 'nl' => 'Maximale base64-chunkgrootte packages/modules (KiB)'),
'app.save_configuration' => array('en' => 'Save configuration', 'nl' => 'Configuratie opslaan'),
'app.configuration_saved' => array('en' => 'Configuration saved.', 'nl' => 'Configuratie opgeslagen.'),
'app.racket_parts_regenerated' => array('en' => 'Racket installation parts regenerated:', 'nl' => 'Racket-installatie parts opnieuw gemaakt:'),
'app.chunk_size_hint_v2' => array(
'en' => 'Values are maximum base64 payload sizes in KiB. A 6144 KiB binary chunk becomes 8192 KiB base64. Racket installation parts are regenerated when this configuration is saved.',
'nl' => 'Waarden zijn maximale base64-payloadgroottes in KiB. Een binaire chunk van 6144 KiB wordt 8192 KiB base64. Racket-installatie parts worden opnieuw gemaakt wanneer deze configuratie wordt opgeslagen.',
),
'app.effective_binary_chunk' => array('en' => 'Effective binary chunk', 'nl' => 'Effectieve binaire chunk'),
'app.racket_zip_source' => array('en' => 'Racket installation source', 'nl' => 'Bronbestand Racket-installatie'),
'app.racket_parts' => array('en' => 'Racket installation parts', 'nl' => 'Racket-installatie parts'),
'app.racket_parts_current' => array('en' => 'current', 'nl' => 'actueel'),
'app.racket_parts_missing' => array('en' => 'missing or outdated; save configuration to regenerate', 'nl' => 'ontbreken of verouderd; sla configuratie op om opnieuw te maken'),
'app.expired_tokens_removed' => array('en' => 'Expired next tokens removed:', 'nl' => 'Verlopen next-tokens verwijderd:'),
'app.cleanup_help' => array(
'en' => 'Expired links should return an outdated information message to the AI agent. Cleanup only removes old token rows from SQLite.',
'nl' => 'Verlopen links moeten een melding over verouderde informatie aan de AI-agent teruggeven. Opruimen verwijdert alleen oude tokenrijen uit SQLite.'
),
));
seed_template_translations($languageStore, 'config.html');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = post_value('action');
@@ -145,12 +110,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
));
$base64ChunkConfig = load_base64_chunk_config();
$manifest = split_racket_zip_parts($base64ChunkConfig['racket_zip_max_base64_kb']);
$message = t('app.configuration_saved', 'Configuration saved.') . ' ' .
t('app.racket_parts_regenerated', 'Racket installation parts regenerated:') . ' ' .
(int)$manifest['part_count'];
$message = t('app.configuration_saved_with_parts', 'Configuration saved. Racket installation parts regenerated: {{count}}', array(
'count' => (int)$manifest['part_count'],
));
} elseif ($action === 'cleanup_tokens') {
$deleted = $tokens->cleanup();
$message = t('app.expired_tokens_removed', 'Expired next tokens removed:') . ' ' . $deleted;
$message = t('app.expired_tokens_removed', 'Expired next tokens removed: {{count}}', array(
'count' => $deleted,
));
}
} catch (Throwable $e) {
$error = $e->getMessage();
@@ -177,8 +144,9 @@ if (!$racketPartsCurrent && is_file(RACKET_ZIP_FILE) && is_readable(RACKET_ZIP_F
$racketPartsCurrent = true;
if ($message === '') {
$message = t('app.racket_parts_regenerated', 'Racket installation parts regenerated:') . ' ' .
(int)$racketPartsManifest['part_count'];
$message = t('app.racket_parts_regenerated', 'Racket installation parts regenerated: {{count}}', array(
'count' => (int)$racketPartsManifest['part_count'],
));
}
} catch (Throwable $e) {
if ($error === '') {
@@ -203,23 +171,10 @@ foreach ($languageStore->supportedLanguages() as $lang) {
$headerLanguages[$lang] = $languageStore->languageLabel($lang);
}
$styleVersion = @filemtime(__DIR__ . '/styles.css') ?: time();
$styleVersion = @filemtime(__DIR__ . '/css/styles.css') ?: time();
header('Content-Type: text/html; charset=utf-8');
?>
<!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(
$headerHtml = app_header_html(array(
'title' => t('app.configuration', 'Configuration'),
'nav_items' => array(
array('label' => t('app.back_to_sandbox', 'Back to Racket sandbox'), 'url' => '/?lang=' . rawurlencode($language)),
@@ -252,98 +207,77 @@ render_app_header(array(
'message' => $message,
'error' => $error,
));
?>
<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">
<h2><?= h(t('app.download_settings', 'Download settings')) ?></h2>
if (count($currentNextTokens) > 0) {
$tokenRowsHtml = '';
<form method="post" action="/admin-config?lang=<?= h($language) ?>" class="admin-form-grid">
<input type="hidden" name="action" value="update_config">
<label>
<?= h(t('app.racket_zip_chunk_kb', 'Racket installation max base64 chunk size (KiB)')) ?><br>
<input type="number" name="racket_zip_max_base64_kb" min="1" step="1" value="<?= h($base64ChunkConfig['racket_zip_max_base64_kb']) ?>" required>
</label>
<label>
<?= 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>
foreach ($currentNextTokens as $nextToken) {
$tokenRowsHtml .= RacketSandboxTemplate::renderFile('partials/config-token-row.html', array(
'status_class' => token_status_class($nextToken),
'token' => $nextToken['token'] ?? '',
'created_at' => format_token_time($nextToken['created_at'] ?? 0),
'expires_at' => format_token_time($nextToken['expires_at'] ?? 0),
)) . "\n";
}
<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>
<tr>
<th><?= h(t('app.racket_zip_chunk_kb', 'Racket installation max base64 chunk size (KiB)')) ?></th>
<td><code><?= h($base64ChunkConfig['racket_zip_max_base64_kb']) ?></code> KiB</td>
<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>
</tr>
<tr>
<th><?= h(t('app.package_zip_chunk_kb', 'Package/module max base64 chunk size (KiB)')) ?></th>
<td><code><?= h($base64ChunkConfig['package_zip_max_base64_kb']) ?></code> KiB</td>
<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>
</tr>
<tr>
<th><?= h(t('app.racket_zip_source', 'Racket installation source')) ?></th>
<td><code>config/racket.zip</code></td>
<td><code><?= h(is_file(RACKET_ZIP_FILE) ? (string)filesize(RACKET_ZIP_FILE) : '0') ?></code> bytes</td>
</tr>
<tr>
<th><?= h(t('app.racket_parts', 'Racket installation parts')) ?></th>
<td><code><?= h((string)($racketPartsManifest['part_count'] ?? 0)) ?></code></td>
<td>
<?php if ($racketPartsCurrent): ?>
<?= h(t('app.racket_parts_current', 'current')) ?>,
<?= h((string)($racketPartsManifest['created_at'] ?? '')) ?>
<?php else: ?>
<?= h(t('app.racket_parts_missing', 'missing or outdated; save configuration to regenerate')) ?>
<?php endif; ?>
</td>
</tr>
</table>
</section>
<section class="panel">
<h2><?= h(t('app.maintenance', 'Maintenance')) ?></h2>
<fieldset>
<legend><?= h(t('app.next_tokens', 'Next tokens')) ?></legend>
<form method="post" action="/admin-config?lang=<?= h($language) ?>">
<input type="hidden" name="action" value="cleanup_tokens">
<button type="submit"><?= h(t('app.remove_expired_tokens', 'Remove expired next tokens')) ?></button>
</form>
<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>
echo RacketSandboxTemplate::renderFile('config.html', array(
'language' => $language,
'language_url' => rawurlencode($language),
'title' => t('app.configuration', 'Configuration'),
'style_version' => $styleVersion,
'header_html' => $headerHtml,
'download_settings_label' => t('app.download_settings', 'Download settings'),
'racket_zip_chunk_label' => t('app.racket_zip_chunk_kb', 'Racket installation max base64 chunk size (KiB)'),
'package_zip_chunk_label' => t('app.package_zip_chunk_kb', 'Package/module max base64 chunk size (KiB)'),
'racket_zip_max_base64_kb' => $base64ChunkConfig['racket_zip_max_base64_kb'],
'package_zip_max_base64_kb' => $base64ChunkConfig['package_zip_max_base64_kb'],
'save_configuration_label' => t('app.save_configuration', 'Save configuration'),
'chunk_size_hint' => t('app.chunk_size_hint_v2', 'Values are maximum base64 payload sizes in KiB. A {{chunk_size}} KiB binary chunk becomes {{base64_chunk_size}} KiB base64. Racket installation parts are regenerated when this configuration is saved.', array(
'chunk_size' => (string)intdiv(base64_binary_chunk_bytes_for_kib($base64ChunkConfig['racket_zip_max_base64_kb']), 1024),
'base64_chunk_size' => (string)$base64ChunkConfig['racket_zip_max_base64_kb'],
)),
'racket_zip_base64_chunk_size' => t('app.base64_chunk_size_kib', '{{size}} KiB', array(
'size' => code_value($base64ChunkConfig['racket_zip_max_base64_kb']),
)),
'package_zip_base64_chunk_size' => t('app.base64_chunk_size_kib', '{{size}} KiB', array(
'size' => code_value($base64ChunkConfig['package_zip_max_base64_kb']),
)),
'racket_zip_effective_binary_chunk' => t('app.effective_binary_chunk_bytes', 'Effective binary chunk: {{bytes}} bytes', array(
'bytes' => code_value(base64_binary_chunk_bytes_for_kib($base64ChunkConfig['racket_zip_max_base64_kb'])),
)),
'package_zip_effective_binary_chunk' => t('app.effective_binary_chunk_bytes', 'Effective binary chunk: {{bytes}} bytes', array(
'bytes' => code_value(base64_binary_chunk_bytes_for_kib($base64ChunkConfig['package_zip_max_base64_kb'])),
)),
'racket_zip_source_label' => t('app.racket_zip_source', 'Racket installation source'),
'racket_zip_file_size' => t('app.file_size_bytes', '{{bytes}} bytes', array(
'bytes' => code_value(is_file(RACKET_ZIP_FILE) ? (string)filesize(RACKET_ZIP_FILE) : '0'),
)),
'racket_parts_label' => t('app.racket_parts', 'Racket installation parts'),
'racket_part_count' => (string)($racketPartsManifest['part_count'] ?? 0),
'racket_parts_status' => $racketPartsStatus,
'maintenance_label' => t('app.maintenance', 'Maintenance'),
'next_tokens_label' => t('app.next_tokens', 'Next tokens'),
'remove_expired_tokens_label' => t('app.remove_expired_tokens', 'Remove expired next tokens'),
'cleanup_help' => t('app.cleanup_help', 'Expired links should return an outdated information message to the AI agent. Cleanup only removes old token rows from SQLite.'),
'current_next_tokens_label' => t('app.current_next_tokens', 'Current next tokens'),
'current_tokens_html' => $currentTokensHtml,
));