475765e31f
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/.
284 lines
11 KiB
PHP
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,
|
|
));
|