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 '' . h($value) . ''; } $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, ));