Files
www-data 475765e31f 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/.
2026-05-26 12:50:26 +02:00

284 lines
11 KiB
PHP

<?php
/*
* config.php
*
* Admin application configuration.
*/
require_once __DIR__ . '/private/auth.php';
require_once __DIR__ . '/private/header.php';
require_once __DIR__ . '/private/languagestore.php';
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';
require_once __DIR__ . '/config/reporting.php';
$DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite';
$auth = new RacketSandboxAuth($DB_FILE);
$languageStore = new LanguageStore($DB_FILE);
$tokens = new NextTokenStore($DB_FILE);
$userSettings = new UserSettingsStore($DB_FILE);
$currentUser = $auth->requireAdminHtml();
$message = '';
$error = '';
$base64ChunkConfig = load_base64_chunk_config();
$localTimezone = new DateTimeZone('Europe/Amsterdam');
function h($s)
{
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function t($key, $fallback = null, $values = array())
{
global $languageStore, $language;
return $languageStore->translateFormat($key, $language, $values, $fallback);
}
function post_value($name, $default = '')
{
return $_POST[$name] ?? $default;
}
function resolve_user_language($userSettings, $userId, $allowedLanguages)
{
$language = isset($_GET['lang'])
? (string)$_GET['lang']
: (string)$userSettings->get($userId, 'language', 'en');
if (!in_array($language, $allowedLanguages, true)) {
$language = 'en';
}
$userSettings->set($userId, 'language', $language);
return $language;
}
function base64_binary_chunk_bytes_for_kib($kib)
{
return racket_zip_binary_chunk_bytes_for_base64_kib($kib);
}
function format_token_time($timestamp)
{
global $localTimezone;
$time = (new DateTimeImmutable('@' . (int)$timestamp))->setTimezone($localTimezone);
return $time->format('Y-m-d H:i:s T');
}
function token_status_class($token)
{
return time() <= (int)($token['expires_at'] ?? 0)
? 'token-valid'
: 'token-expired';
}
function code_value($value)
{
return '<code>' . h($value) . '</code>';
}
$language = resolve_user_language(
$userSettings,
$currentUser->id(),
$languageStore->supportedLanguages()
);
seed_template_translations($languageStore, 'config.html');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = post_value('action');
try {
if ($action === 'logout') {
$auth->logout();
header('Location: /login.php');
exit;
} elseif ($action === 'update_config') {
save_base64_chunk_config(array(
'racket_zip_max_base64_kb' => post_value('racket_zip_max_base64_kb'),
'package_zip_max_base64_kb' => post_value('package_zip_max_base64_kb'),
));
$base64ChunkConfig = load_base64_chunk_config();
$manifest = split_racket_zip_parts($base64ChunkConfig['racket_zip_max_base64_kb']);
$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: {{count}}', array(
'count' => $deleted,
));
}
} catch (Throwable $e) {
$error = $e->getMessage();
}
}
$racketPartsManifest = null;
$racketPartsCurrent = false;
try {
$racketPartsManifest = load_racket_zip_parts_manifest();
$racketPartsCurrent = racket_zip_parts_current(
$racketPartsManifest,
$base64ChunkConfig['racket_zip_max_base64_kb']
);
} catch (Throwable $e) {
$racketPartsManifest = null;
$racketPartsCurrent = false;
}
if (!$racketPartsCurrent && is_file(RACKET_ZIP_FILE) && is_readable(RACKET_ZIP_FILE)) {
try {
$racketPartsManifest = split_racket_zip_parts($base64ChunkConfig['racket_zip_max_base64_kb']);
$racketPartsCurrent = true;
if ($message === '') {
$message = t('app.racket_parts_regenerated', 'Racket installation parts regenerated: {{count}}', array(
'count' => (int)$racketPartsManifest['part_count'],
));
}
} catch (Throwable $e) {
if ($error === '') {
$error = $e->getMessage();
}
}
}
$headerLanguages = array();
$currentNextTokens = array();
try {
$currentNextTokens = $tokens->tokens();
} catch (Throwable $e) {
if ($error === '') {
$error = $e->getMessage();
}
}
foreach ($languageStore->supportedLanguages() as $lang) {
$headerLanguages[$lang] = $languageStore->languageLabel($lang);
}
$styleVersion = @filemtime(__DIR__ . '/css/styles.css') ?: time();
header('Content-Type: text/html; charset=utf-8');
$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)),
array(
'label' => t('app.manage_prompts', 'Manage prompts'),
'url' => '/prompts?lang=' . rawurlencode($language),
'separator_before' => true,
),
array(
'label' => t('app.user_management', 'User management'),
'url' => '/users?lang=' . rawurlencode($language),
'separator_before' => true,
),
array(
'label' => t('app.configuration', 'Configuration'),
'url' => '/admin-config?lang=' . rawurlencode($language),
'active' => true,
'separator_before' => true,
),
),
'user' => $currentUser,
'user_prefix' => t('app.logged_in_as', 'Logged in as:'),
'admin_label' => t('app.admin', 'Admin'),
'language_label' => t('app.language', 'Language'),
'language' => $language,
'languages' => $headerLanguages,
'language_action' => '/admin-config',
'logout_action' => '/admin-config?lang=' . rawurlencode($language),
'logout_label' => t('app.logout', 'Logout'),
'message' => $message,
'error' => $error,
));
$racketPartsStatus = $racketPartsCurrent
? t('app.racket_parts_current_with_date', 'current, {{created_at}}', array(
'created_at' => (string)($racketPartsManifest['created_at'] ?? ''),
))
: t('app.racket_parts_missing', 'missing or outdated; save configuration to regenerate');
if (count($currentNextTokens) > 0) {
$tokenRowsHtml = '';
foreach ($currentNextTokens as $nextToken) {
$tokenRowsHtml .= RacketSandboxTemplate::renderFile('partials/config-token-row.html', array(
'status_class' => token_status_class($nextToken),
'token' => $nextToken['token'] ?? '',
'created_at' => format_token_time($nextToken['created_at'] ?? 0),
'expires_at' => format_token_time($nextToken['expires_at'] ?? 0),
)) . "\n";
}
$currentTokensHtml = RacketSandboxTemplate::renderFile('partials/config-token-table.html', array(
'token_label' => t('app.token', 'Token'),
'created_at_label' => t('app.created_at', 'Created at'),
'expires_at_label' => t('app.expires_at', 'Expires at'),
'token_rows_html' => $tokenRowsHtml,
));
} else {
$currentTokensHtml = RacketSandboxTemplate::renderFile('partials/paragraph.html', array(
'class' => 'small',
'text' => t('app.no_current_next_tokens', 'No current next tokens.'),
));
}
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,
));