Files
racket-chatgpt-bootstrap/config.php
T
2026-05-25 13:47:46 +02:00

350 lines
14 KiB
PHP

<?php
/*
* config.php
*
* Admin application configuration.
*/
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/header.php';
require_once __DIR__ . '/languagestore.php';
require_once __DIR__ . '/nexttoken.php';
require_once __DIR__ . '/usersettings.php';
require_once __DIR__ . '/base64config.php';
require_once __DIR__ . '/racketzip.php';
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
ini_set('log_errors', '1');
error_reporting(E_ALL);
$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)
{
global $languageStore, $language;
return $languageStore->translate($key, $language, $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';
}
$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.'
),
));
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', 'Configuration saved.') . ' ' .
t('app.racket_parts_regenerated', 'Racket installation parts regenerated:') . ' ' .
(int)$manifest['part_count'];
} elseif ($action === 'cleanup_tokens') {
$deleted = $tokens->cleanup();
$message = t('app.expired_tokens_removed', 'Expired next tokens removed:') . ' ' . $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:') . ' ' .
(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__ . '/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(
'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,
));
?>
<main class="page-main dashboard-main">
<section class="panel">
<h2><?= h(t('app.download_settings', 'Download settings')) ?></h2>
<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>
<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>
<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>