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:
+90
-156
@@ -12,11 +12,9 @@ require_once __DIR__ . '/private/nexttoken.php';
|
|||||||
require_once __DIR__ . '/private/usersettings.php';
|
require_once __DIR__ . '/private/usersettings.php';
|
||||||
require_once __DIR__ . '/private/base64config.php';
|
require_once __DIR__ . '/private/base64config.php';
|
||||||
require_once __DIR__ . '/private/racketzip.php';
|
require_once __DIR__ . '/private/racketzip.php';
|
||||||
|
require_once __DIR__ . '/private/viewdata.php';
|
||||||
|
|
||||||
ini_set('display_errors', '1');
|
require_once __DIR__ . '/config/reporting.php';
|
||||||
ini_set('display_startup_errors', '1');
|
|
||||||
ini_set('log_errors', '1');
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
$DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite';
|
$DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite';
|
||||||
|
|
||||||
@@ -36,11 +34,11 @@ function h($s)
|
|||||||
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
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;
|
global $languageStore, $language;
|
||||||
|
|
||||||
return $languageStore->translate($key, $language, $fallback);
|
return $languageStore->translateFormat($key, $language, $values, $fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function post_value($name, $default = '')
|
function post_value($name, $default = '')
|
||||||
@@ -84,51 +82,18 @@ function token_status_class($token)
|
|||||||
: 'token-expired';
|
: 'token-expired';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function code_value($value)
|
||||||
|
{
|
||||||
|
return '<code>' . h($value) . '</code>';
|
||||||
|
}
|
||||||
|
|
||||||
$language = resolve_user_language(
|
$language = resolve_user_language(
|
||||||
$userSettings,
|
$userSettings,
|
||||||
$currentUser->id(),
|
$currentUser->id(),
|
||||||
$languageStore->supportedLanguages()
|
$languageStore->supportedLanguages()
|
||||||
);
|
);
|
||||||
|
|
||||||
$languageStore->seedDefaults(array(
|
seed_template_translations($languageStore, 'config.html');
|
||||||
'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') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$action = post_value('action');
|
$action = post_value('action');
|
||||||
@@ -145,12 +110,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
));
|
));
|
||||||
$base64ChunkConfig = load_base64_chunk_config();
|
$base64ChunkConfig = load_base64_chunk_config();
|
||||||
$manifest = split_racket_zip_parts($base64ChunkConfig['racket_zip_max_base64_kb']);
|
$manifest = split_racket_zip_parts($base64ChunkConfig['racket_zip_max_base64_kb']);
|
||||||
$message = t('app.configuration_saved', 'Configuration saved.') . ' ' .
|
$message = t('app.configuration_saved_with_parts', 'Configuration saved. Racket installation parts regenerated: {{count}}', array(
|
||||||
t('app.racket_parts_regenerated', 'Racket installation parts regenerated:') . ' ' .
|
'count' => (int)$manifest['part_count'],
|
||||||
(int)$manifest['part_count'];
|
));
|
||||||
} elseif ($action === 'cleanup_tokens') {
|
} elseif ($action === 'cleanup_tokens') {
|
||||||
$deleted = $tokens->cleanup();
|
$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) {
|
} catch (Throwable $e) {
|
||||||
$error = $e->getMessage();
|
$error = $e->getMessage();
|
||||||
@@ -177,8 +144,9 @@ if (!$racketPartsCurrent && is_file(RACKET_ZIP_FILE) && is_readable(RACKET_ZIP_F
|
|||||||
$racketPartsCurrent = true;
|
$racketPartsCurrent = true;
|
||||||
|
|
||||||
if ($message === '') {
|
if ($message === '') {
|
||||||
$message = t('app.racket_parts_regenerated', 'Racket installation parts regenerated:') . ' ' .
|
$message = t('app.racket_parts_regenerated', 'Racket installation parts regenerated: {{count}}', array(
|
||||||
(int)$racketPartsManifest['part_count'];
|
'count' => (int)$racketPartsManifest['part_count'],
|
||||||
|
));
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
if ($error === '') {
|
if ($error === '') {
|
||||||
@@ -203,23 +171,10 @@ foreach ($languageStore->supportedLanguages() as $lang) {
|
|||||||
$headerLanguages[$lang] = $languageStore->languageLabel($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');
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
?>
|
$headerHtml = app_header_html(array(
|
||||||
<!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'),
|
'title' => t('app.configuration', 'Configuration'),
|
||||||
'nav_items' => array(
|
'nav_items' => array(
|
||||||
array('label' => t('app.back_to_sandbox', 'Back to Racket sandbox'), 'url' => '/?lang=' . rawurlencode($language)),
|
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,
|
'message' => $message,
|
||||||
'error' => $error,
|
'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">
|
if (count($currentNextTokens) > 0) {
|
||||||
<h2><?= h(t('app.download_settings', 'Download settings')) ?></h2>
|
$tokenRowsHtml = '';
|
||||||
|
|
||||||
<form method="post" action="/admin-config?lang=<?= h($language) ?>" class="admin-form-grid">
|
foreach ($currentNextTokens as $nextToken) {
|
||||||
<input type="hidden" name="action" value="update_config">
|
$tokenRowsHtml .= RacketSandboxTemplate::renderFile('partials/config-token-row.html', array(
|
||||||
<label>
|
'status_class' => token_status_class($nextToken),
|
||||||
<?= h(t('app.racket_zip_chunk_kb', 'Racket installation max base64 chunk size (KiB)')) ?><br>
|
'token' => $nextToken['token'] ?? '',
|
||||||
<input type="number" name="racket_zip_max_base64_kb" min="1" step="1" value="<?= h($base64ChunkConfig['racket_zip_max_base64_kb']) ?>" required>
|
'created_at' => format_token_time($nextToken['created_at'] ?? 0),
|
||||||
</label>
|
'expires_at' => format_token_time($nextToken['expires_at'] ?? 0),
|
||||||
<label>
|
)) . "\n";
|
||||||
<?= 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>
|
$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>
|
echo RacketSandboxTemplate::renderFile('config.html', array(
|
||||||
<tr>
|
'language' => $language,
|
||||||
<th><?= h(t('app.racket_zip_chunk_kb', 'Racket installation max base64 chunk size (KiB)')) ?></th>
|
'language_url' => rawurlencode($language),
|
||||||
<td><code><?= h($base64ChunkConfig['racket_zip_max_base64_kb']) ?></code> KiB</td>
|
'title' => t('app.configuration', 'Configuration'),
|
||||||
<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>
|
'style_version' => $styleVersion,
|
||||||
</tr>
|
'header_html' => $headerHtml,
|
||||||
<tr>
|
'download_settings_label' => t('app.download_settings', 'Download settings'),
|
||||||
<th><?= h(t('app.package_zip_chunk_kb', 'Package/module max base64 chunk size (KiB)')) ?></th>
|
'racket_zip_chunk_label' => t('app.racket_zip_chunk_kb', 'Racket installation max base64 chunk size (KiB)'),
|
||||||
<td><code><?= h($base64ChunkConfig['package_zip_max_base64_kb']) ?></code> KiB</td>
|
'package_zip_chunk_label' => t('app.package_zip_chunk_kb', 'Package/module max base64 chunk size (KiB)'),
|
||||||
<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>
|
'racket_zip_max_base64_kb' => $base64ChunkConfig['racket_zip_max_base64_kb'],
|
||||||
</tr>
|
'package_zip_max_base64_kb' => $base64ChunkConfig['package_zip_max_base64_kb'],
|
||||||
<tr>
|
'save_configuration_label' => t('app.save_configuration', 'Save configuration'),
|
||||||
<th><?= h(t('app.racket_zip_source', 'Racket installation source')) ?></th>
|
'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(
|
||||||
<td><code>config/racket.zip</code></td>
|
'chunk_size' => (string)intdiv(base64_binary_chunk_bytes_for_kib($base64ChunkConfig['racket_zip_max_base64_kb']), 1024),
|
||||||
<td><code><?= h(is_file(RACKET_ZIP_FILE) ? (string)filesize(RACKET_ZIP_FILE) : '0') ?></code> bytes</td>
|
'base64_chunk_size' => (string)$base64ChunkConfig['racket_zip_max_base64_kb'],
|
||||||
</tr>
|
)),
|
||||||
<tr>
|
'racket_zip_base64_chunk_size' => t('app.base64_chunk_size_kib', '{{size}} KiB', array(
|
||||||
<th><?= h(t('app.racket_parts', 'Racket installation parts')) ?></th>
|
'size' => code_value($base64ChunkConfig['racket_zip_max_base64_kb']),
|
||||||
<td><code><?= h((string)($racketPartsManifest['part_count'] ?? 0)) ?></code></td>
|
)),
|
||||||
<td>
|
'package_zip_base64_chunk_size' => t('app.base64_chunk_size_kib', '{{size}} KiB', array(
|
||||||
<?php if ($racketPartsCurrent): ?>
|
'size' => code_value($base64ChunkConfig['package_zip_max_base64_kb']),
|
||||||
<?= h(t('app.racket_parts_current', 'current')) ?>,
|
)),
|
||||||
<?= h((string)($racketPartsManifest['created_at'] ?? '')) ?>
|
'racket_zip_effective_binary_chunk' => t('app.effective_binary_chunk_bytes', 'Effective binary chunk: {{bytes}} bytes', array(
|
||||||
<?php else: ?>
|
'bytes' => code_value(base64_binary_chunk_bytes_for_kib($base64ChunkConfig['racket_zip_max_base64_kb'])),
|
||||||
<?= h(t('app.racket_parts_missing', 'missing or outdated; save configuration to regenerate')) ?>
|
)),
|
||||||
<?php endif; ?>
|
'package_zip_effective_binary_chunk' => t('app.effective_binary_chunk_bytes', 'Effective binary chunk: {{bytes}} bytes', array(
|
||||||
</td>
|
'bytes' => code_value(base64_binary_chunk_bytes_for_kib($base64ChunkConfig['package_zip_max_base64_kb'])),
|
||||||
</tr>
|
)),
|
||||||
</table>
|
'racket_zip_source_label' => t('app.racket_zip_source', 'Racket installation source'),
|
||||||
</section>
|
'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'),
|
||||||
<section class="panel">
|
)),
|
||||||
<h2><?= h(t('app.maintenance', 'Maintenance')) ?></h2>
|
'racket_parts_label' => t('app.racket_parts', 'Racket installation parts'),
|
||||||
|
'racket_part_count' => (string)($racketPartsManifest['part_count'] ?? 0),
|
||||||
<fieldset>
|
'racket_parts_status' => $racketPartsStatus,
|
||||||
<legend><?= h(t('app.next_tokens', 'Next tokens')) ?></legend>
|
'maintenance_label' => t('app.maintenance', 'Maintenance'),
|
||||||
|
'next_tokens_label' => t('app.next_tokens', 'Next tokens'),
|
||||||
<form method="post" action="/admin-config?lang=<?= h($language) ?>">
|
'remove_expired_tokens_label' => t('app.remove_expired_tokens', 'Remove expired next tokens'),
|
||||||
<input type="hidden" name="action" value="cleanup_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.'),
|
||||||
<button type="submit"><?= h(t('app.remove_expired_tokens', 'Remove expired next tokens')) ?></button>
|
'current_next_tokens_label' => t('app.current_next_tokens', 'Current next tokens'),
|
||||||
</form>
|
'current_tokens_html' => $currentTokensHtml,
|
||||||
|
));
|
||||||
<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>
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* Shared runtime error reporting settings for public entrypoints.
|
||||||
|
*/
|
||||||
|
|
||||||
|
ini_set('display_errors', '1');
|
||||||
|
ini_set('display_startup_errors', '1');
|
||||||
|
ini_set('log_errors', '1');
|
||||||
|
error_reporting(E_ALL);
|
||||||
@@ -24,11 +24,9 @@ require_once __DIR__ . '/private/languagestore.php';
|
|||||||
require_once __DIR__ . '/private/nexttoken.php';
|
require_once __DIR__ . '/private/nexttoken.php';
|
||||||
require_once __DIR__ . '/private/promptstore.php';
|
require_once __DIR__ . '/private/promptstore.php';
|
||||||
require_once __DIR__ . '/private/usersettings.php';
|
require_once __DIR__ . '/private/usersettings.php';
|
||||||
|
require_once __DIR__ . '/private/viewdata.php';
|
||||||
|
|
||||||
ini_set('display_errors', '1');
|
require_once __DIR__ . '/config/reporting.php';
|
||||||
ini_set('display_startup_errors', '1');
|
|
||||||
ini_set('log_errors', '1');
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
$DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite';
|
$DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite';
|
||||||
|
|
||||||
@@ -57,11 +55,11 @@ function get_value($name, $default = '')
|
|||||||
return $_GET[$name] ?? $default;
|
return $_GET[$name] ?? $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
function t($key, $fallback = null)
|
function t($key, $fallback = null, $values = array())
|
||||||
{
|
{
|
||||||
global $languageStore, $language;
|
global $languageStore, $language;
|
||||||
|
|
||||||
return $languageStore->translate($key, $language, $fallback);
|
return $languageStore->translateFormat($key, $language, $values, $fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function current_scheme()
|
function current_scheme()
|
||||||
@@ -154,64 +152,7 @@ $language = resolve_user_language(
|
|||||||
$languageStore->supportedLanguages()
|
$languageStore->supportedLanguages()
|
||||||
);
|
);
|
||||||
|
|
||||||
$languageStore->seedDefaults(array(
|
seed_template_translations($languageStore, 'index.html');
|
||||||
'app.title' => array('en' => 'Racket sandbox', 'nl' => 'Racket sandbox'),
|
|
||||||
'app.manage_prompts' => array('en' => 'Manage prompts', 'nl' => 'Prompts beheren'),
|
|
||||||
'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.bootstrap_link' => array('en' => 'Bootstrap link', 'nl' => 'Bootstraplink'),
|
|
||||||
'app.generate_bootstrap_link' => array('en' => 'Generate bootstrap link', 'nl' => 'Bootstraplink genereren'),
|
|
||||||
'app.ttl_minutes' => array('en' => 'TTL in minutes', 'nl' => 'TTL in minuten'),
|
|
||||||
'app.ttl_range_help' => array('en' => 'Allowed range: 30 minutes to 8 hours.', 'nl' => 'Toegestaan bereik: 30 minuten tot 8 uur.'),
|
|
||||||
'app.generated_link' => array('en' => 'Generated link', 'nl' => 'Gegenereerde link'),
|
|
||||||
'app.copy' => array('en' => 'Copy', 'nl' => 'Kopieer'),
|
|
||||||
'app.copied' => array('en' => 'Copied', 'nl' => 'Gekopieerd'),
|
|
||||||
'app.generated_link_help' => array(
|
|
||||||
'en' => 'Give this link to the AI agent. The agent should start from this link and then only follow links from the generated HTML pages.',
|
|
||||||
'nl' => 'Geef deze link aan de AI-agent. De agent moet vanaf deze link starten en daarna alleen links volgen vanuit de gegenereerde HTML-paginas.'
|
|
||||||
),
|
|
||||||
'app.select_prompt' => array('en' => 'Select prompt', 'nl' => 'Prompt selecteren'),
|
|
||||||
'app.copy_full_prompt' => array('en' => 'Copy full prompt', 'nl' => 'Volledige prompt kopieren'),
|
|
||||||
'app.bootstrap_prompt_help' => array(
|
|
||||||
'en' => 'Choose one of your prompts. The placeholder {{bootstrap-racket-link}} is replaced by the generated bootstrap link.',
|
|
||||||
'nl' => 'Kies een van je prompts. De placeholder {{bootstrap-racket-link}} wordt vervangen door de gegenereerde bootstraplink.'
|
|
||||||
),
|
|
||||||
'app.no_bootstrap_prompts' => array(
|
|
||||||
'en' => 'No personal prompts are available for this language. Copy a default prompt first from prompt management.',
|
|
||||||
'nl' => 'Er zijn geen persoonlijke prompts beschikbaar voor deze taal. Kopieer eerst een standaardprompt vanuit promptbeheer.'
|
|
||||||
),
|
|
||||||
'app.user_management' => array('en' => 'User management', 'nl' => 'Gebruikersbeheer'),
|
|
||||||
'app.configuration' => array('en' => 'Configuration', 'nl' => 'Configuratie'),
|
|
||||||
'app.user_management_help' => array(
|
|
||||||
'en' => 'Users are registered manually by email address, full name and password. This page only manages existing users.',
|
|
||||||
'nl' => 'Gebruikers worden handmatig geregistreerd met e-mailadres, volledige naam en wachtwoord. Deze pagina beheert alleen bestaande gebruikers.'
|
|
||||||
),
|
|
||||||
'app.id' => array('en' => 'ID', 'nl' => 'ID'),
|
|
||||||
'app.full_name' => array('en' => 'Full name', 'nl' => 'Volledige naam'),
|
|
||||||
'app.email' => array('en' => 'Email', 'nl' => 'E-mail'),
|
|
||||||
'app.enabled' => array('en' => 'Enabled', 'nl' => 'Ingeschakeld'),
|
|
||||||
'app.created' => array('en' => 'Created', 'nl' => 'Gemaakt'),
|
|
||||||
'app.last_login' => array('en' => 'Last login', 'nl' => 'Laatste login'),
|
|
||||||
'app.actions' => array('en' => 'Actions', 'nl' => 'Acties'),
|
|
||||||
'app.yes' => array('en' => 'yes', 'nl' => 'ja'),
|
|
||||||
'app.no' => array('en' => 'no', 'nl' => 'nee'),
|
|
||||||
'app.save_flags' => array('en' => 'Save flags', 'nl' => 'Vlaggen opslaan'),
|
|
||||||
'app.new_password' => array('en' => 'New password', 'nl' => 'Nieuw wachtwoord'),
|
|
||||||
'app.change_password' => array('en' => 'Change password', 'nl' => 'Wachtwoord wijzigen'),
|
|
||||||
'app.delete_user' => array('en' => 'Delete user', 'nl' => 'Gebruiker verwijderen'),
|
|
||||||
'app.delete_user_confirm' => array('en' => 'Delete user', 'nl' => 'Gebruiker verwijderen'),
|
|
||||||
'app.cannot_delete_self' => array('en' => 'You cannot delete your own account.', 'nl' => 'Je kunt je eigen account niet verwijderen.'),
|
|
||||||
'app.bootstrap_link_issued' => array('en' => 'Bootstrap link issued.', 'nl' => 'Bootstraplink aangemaakt.'),
|
|
||||||
'app.password_changed_for' => array('en' => 'Password changed for:', 'nl' => 'Wachtwoord gewijzigd voor:'),
|
|
||||||
'app.user_flags_updated' => array('en' => 'User flags updated.', 'nl' => 'Gebruikersvlaggen bijgewerkt.'),
|
|
||||||
'app.user_deleted' => array('en' => 'User deleted.', 'nl' => 'Gebruiker verwijderd.'),
|
|
||||||
'app.admin_rights_required' => array('en' => 'Admin rights required.', 'nl' => 'Adminrechten vereist.'),
|
|
||||||
'app.cannot_remove_own_admin' => array('en' => 'You cannot remove your own admin rights.', 'nl' => 'Je kunt je eigen adminrechten niet verwijderen.'),
|
|
||||||
'app.cannot_disable_self' => array('en' => 'You cannot disable your own account.', 'nl' => 'Je kunt je eigen account niet uitschakelen.'),
|
|
||||||
'app.unknown_action' => array('en' => 'Unknown action:', 'nl' => 'Onbekende actie:'),
|
|
||||||
));
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$action = post_value('action');
|
$action = post_value('action');
|
||||||
@@ -260,7 +201,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$password = (string)post_value('password');
|
$password = (string)post_value('password');
|
||||||
|
|
||||||
$auth->setPassword($email, $password);
|
$auth->setPassword($email, $password);
|
||||||
$message = t('app.password_changed_for', 'Password changed for:') . ' ' . $email;
|
$message = t('app.password_changed_for', 'Password changed for: {{email}}', array('email' => $email));
|
||||||
} elseif ($action === 'set_flags') {
|
} elseif ($action === 'set_flags') {
|
||||||
$userId = (int)post_value('user_id');
|
$userId = (int)post_value('user_id');
|
||||||
$isAdmin = post_bool('is_admin');
|
$isAdmin = post_bool('is_admin');
|
||||||
@@ -281,13 +222,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$userId = (int)post_value('user_id');
|
$userId = (int)post_value('user_id');
|
||||||
|
|
||||||
if ($userId === $currentUser->id()) {
|
if ($userId === $currentUser->id()) {
|
||||||
throw new Exception('You cannot delete your own account.');
|
throw new Exception(t('app.cannot_delete_self', 'You cannot delete your own account.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$auth->deleteUser($userId);
|
$auth->deleteUser($userId);
|
||||||
$message = t('app.user_deleted', 'User deleted.');
|
$message = t('app.user_deleted', 'User deleted.');
|
||||||
} elseif ($action !== '') {
|
} elseif ($action !== '') {
|
||||||
throw new Exception(t('app.unknown_action', 'Unknown action:') . ' ' . $action);
|
throw new Exception(t('app.unknown_action', 'Unknown action: {{action}}', array('action' => $action)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
@@ -345,20 +286,59 @@ if ($currentUser->isAdmin()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
header('Content-Type: text/html; charset=utf-8');
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
?>
|
$generatedLinkHtml = '';
|
||||||
<!doctype html>
|
|
||||||
<html lang="<?= h($language) ?>">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title><?= h(t('app.title', 'Racket sandbox')) ?></title>
|
|
||||||
<link rel="stylesheet" href="/styles.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="page">
|
if ($issuedLink !== '') {
|
||||||
|
$generatedLinkHtml = RacketSandboxTemplate::renderFile('partials/index-generated-link.html', array(
|
||||||
|
'generated_link_label' => t('app.generated_link', 'Generated link'),
|
||||||
|
'issued_link' => $issuedLink,
|
||||||
|
'copy_label' => t('app.copy', 'Copy'),
|
||||||
|
'copied_label' => t('app.copied', 'Copied'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
<?php
|
$promptPanelHtml = '';
|
||||||
render_app_header(array(
|
|
||||||
|
if ($issuedLink !== '') {
|
||||||
|
$promptToolHtml = '';
|
||||||
|
|
||||||
|
if (count($bootstrapPrompts) > 0) {
|
||||||
|
$promptOptionsHtml = '';
|
||||||
|
|
||||||
|
foreach ($bootstrapPrompts as $prompt) {
|
||||||
|
$promptOptionsHtml .= RacketSandboxTemplate::renderFile('partials/select-option.html', array(
|
||||||
|
'value' => $prompt['id'],
|
||||||
|
'selected' => '',
|
||||||
|
'label' => $prompt['name'],
|
||||||
|
)) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$promptToolHtml = RacketSandboxTemplate::renderFile('partials/index-prompt-select.html', array(
|
||||||
|
'select_prompt_label' => t('app.select_prompt', 'Select prompt'),
|
||||||
|
'prompt_options_html' => $promptOptionsHtml,
|
||||||
|
'copy_label' => t('app.copy', 'Copy'),
|
||||||
|
'copied_label' => t('app.copied', 'Copied'),
|
||||||
|
'copy_full_prompt_label' => t('app.copy_full_prompt', 'Copy full prompt'),
|
||||||
|
'bootstrap_prompt_json' => json_encode(array(
|
||||||
|
'link' => $issuedLink,
|
||||||
|
'prompts' => $bootstrapPrompts,
|
||||||
|
), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$promptToolHtml = RacketSandboxTemplate::renderFile('partials/paragraph.html', array(
|
||||||
|
'class' => 'small',
|
||||||
|
'text' => t('app.no_bootstrap_prompts', 'No personal prompts are available for this language. Copy a default prompt first from prompt management.'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$promptPanelHtml = RacketSandboxTemplate::renderFile('partials/index-prompt-tool.html', array(
|
||||||
|
'copy_full_prompt_label' => t('app.copy_full_prompt', 'Copy full prompt'),
|
||||||
|
'bootstrap_prompt_help' => t('app.bootstrap_prompt_help', 'Choose one of your prompts. The placeholder {{bootstrap-racket-link}} is replaced by the generated bootstrap link.'),
|
||||||
|
'prompt_tool_html' => $promptToolHtml,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$headerHtml = app_header_html(array(
|
||||||
'title' => t('app.title', 'Racket sandbox'),
|
'title' => t('app.title', 'Racket sandbox'),
|
||||||
'nav_items' => $headerNavItems,
|
'nav_items' => $headerNavItems,
|
||||||
'user' => $currentUser,
|
'user' => $currentUser,
|
||||||
@@ -373,98 +353,20 @@ render_app_header(array(
|
|||||||
'message' => $message,
|
'message' => $message,
|
||||||
'error' => $error,
|
'error' => $error,
|
||||||
));
|
));
|
||||||
?>
|
|
||||||
|
|
||||||
<main class="page-main dashboard-main">
|
echo RacketSandboxTemplate::renderFile('index.html', array(
|
||||||
|
'language' => $language,
|
||||||
<section class="panel">
|
'language_url' => rawurlencode($language),
|
||||||
<h2><?= h(t('app.bootstrap_link', 'Bootstrap link')) ?></h2>
|
'title' => t('app.title', 'Racket sandbox'),
|
||||||
|
'header_html' => $headerHtml,
|
||||||
<fieldset>
|
'bootstrap_link_label' => t('app.bootstrap_link', 'Bootstrap link'),
|
||||||
<legend><?= h(t('app.generate_bootstrap_link', 'Generate bootstrap link')) ?></legend>
|
'generate_bootstrap_link_label' => t('app.generate_bootstrap_link', 'Generate bootstrap link'),
|
||||||
|
'bootstrap_result_class' => $issuedLink !== '' ? 'has-prompt' : '',
|
||||||
<div class="bootstrap-result-grid <?= $issuedLink !== '' ? 'has-prompt' : '' ?>">
|
'ttl_minutes_label' => t('app.ttl_minutes', 'TTL in minutes'),
|
||||||
<div>
|
'bootstrap_ttl_minutes' => $bootstrapTtlMinutes,
|
||||||
<form method="post" action="/?lang=<?= h($language) ?>">
|
'bootstrap_ttl_min_minutes' => BOOTSTRAP_TTL_MIN_MINUTES,
|
||||||
<input type="hidden" name="action" value="issue_bootstrap">
|
'bootstrap_ttl_max_minutes' => BOOTSTRAP_TTL_MAX_MINUTES,
|
||||||
|
'ttl_range_help' => t('app.ttl_range_help', 'Allowed range: 30 minutes to 8 hours.'),
|
||||||
<label>
|
'generated_link_html' => $generatedLinkHtml,
|
||||||
<?= h(t('app.ttl_minutes', 'TTL in minutes')) ?><br>
|
'prompt_panel_html' => $promptPanelHtml,
|
||||||
<input type="number"
|
));
|
||||||
name="ttl_minutes"
|
|
||||||
value="<?= h($bootstrapTtlMinutes) ?>"
|
|
||||||
min="<?= h(BOOTSTRAP_TTL_MIN_MINUTES) ?>"
|
|
||||||
max="<?= h(BOOTSTRAP_TTL_MAX_MINUTES) ?>">
|
|
||||||
</label>
|
|
||||||
<p class="small"><?= h(t('app.ttl_range_help', 'Allowed range: 30 minutes to 8 hours.')) ?></p>
|
|
||||||
|
|
||||||
<button type="submit"><?= h(t('app.generate_bootstrap_link', 'Generate bootstrap link')) ?></button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<?php if ($issuedLink !== ''): ?>
|
|
||||||
<h3><?= h(t('app.generated_link', 'Generated link')) ?></h3>
|
|
||||||
<div class="generated-link-row">
|
|
||||||
<a href="<?= h($issuedLink) ?>" target="_blank" rel="noopener noreferrer"><?= h($issuedLink) ?></a>
|
|
||||||
<button type="button"
|
|
||||||
class="js-copy-button"
|
|
||||||
data-copy-text="<?= h($issuedLink) ?>"
|
|
||||||
data-copy-label="<?= h(t('app.copy', 'Copy')) ?>"
|
|
||||||
data-copied-label="<?= h(t('app.copied', 'Copied')) ?>">
|
|
||||||
<?= h(t('app.copy', 'Copy')) ?>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($issuedLink !== ''): ?>
|
|
||||||
<div>
|
|
||||||
<h3><?= h(t('app.copy_full_prompt', 'Copy full prompt')) ?></h3>
|
|
||||||
<p>
|
|
||||||
<?= h(t('app.bootstrap_prompt_help', 'Choose one of your prompts. The placeholder {{bootstrap-racket-link}} is replaced by the generated bootstrap link.')) ?>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<?php if (count($bootstrapPrompts) > 0): ?>
|
|
||||||
<div class="bootstrap-prompt-tool">
|
|
||||||
<label>
|
|
||||||
<?= h(t('app.select_prompt', 'Select prompt')) ?><br>
|
|
||||||
<select id="bootstrapPromptSelect">
|
|
||||||
<?php foreach ($bootstrapPrompts as $prompt): ?>
|
|
||||||
<option value="<?= h($prompt['id']) ?>"><?= h($prompt['name']) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<textarea id="bootstrapPromptOutput" readonly rows="12"></textarea>
|
|
||||||
|
|
||||||
<button type="button"
|
|
||||||
class="js-copy-button"
|
|
||||||
data-copy-target="bootstrapPromptOutput"
|
|
||||||
data-copy-label="<?= h(t('app.copy', 'Copy')) ?>"
|
|
||||||
data-copied-label="<?= h(t('app.copied', 'Copied')) ?>">
|
|
||||||
<?= h(t('app.copy_full_prompt', 'Copy full prompt')) ?>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<script type="application/json" id="bootstrapPromptData">
|
|
||||||
<?= json_encode(array(
|
|
||||||
'link' => $issuedLink,
|
|
||||||
'prompts' => $bootstrapPrompts,
|
|
||||||
), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>
|
|
||||||
</script>
|
|
||||||
<?php else: ?>
|
|
||||||
<p class="small"><?= h(t('app.no_bootstrap_prompts', 'No personal prompts are available for this language. Copy a default prompt first from prompt management.')) ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</fieldset>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/js/clipboard.js" defer></script>
|
|
||||||
<script src="/js/bootstrap-prompt.js" defer></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@@ -4,21 +4,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/private/auth.php';
|
require_once __DIR__ . '/private/auth.php';
|
||||||
|
require_once __DIR__ . '/private/Template.php';
|
||||||
|
|
||||||
ini_set('display_errors', '1');
|
require_once __DIR__ . '/config/reporting.php';
|
||||||
ini_set('display_startup_errors', '1');
|
|
||||||
ini_set('log_errors', '1');
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
$auth = new RacketSandboxAuth(__DIR__ . '/data/racket-sandbox.sqlite');
|
$auth = new RacketSandboxAuth(__DIR__ . '/data/racket-sandbox.sqlite');
|
||||||
|
|
||||||
$error = '';
|
$error = '';
|
||||||
|
|
||||||
function h($s)
|
|
||||||
{
|
|
||||||
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
function detect_login_language($supported, $fallback)
|
function detect_login_language($supported, $fallback)
|
||||||
{
|
{
|
||||||
$header = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
|
$header = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
|
||||||
@@ -61,26 +54,10 @@ function detect_login_language($supported, $fallback)
|
|||||||
}
|
}
|
||||||
|
|
||||||
$pageTitle = 'Racket ChatGPT Agent Sandbox Creator';
|
$pageTitle = 'Racket ChatGPT Agent Sandbox Creator';
|
||||||
$texts = array(
|
$templateData = RacketSandboxTemplate::dataFile('login.html');
|
||||||
'en' => array(
|
$texts = $templateData['translations'] ?? array();
|
||||||
'email' => 'Email address',
|
|
||||||
'password' => 'Password',
|
|
||||||
'login' => 'Login',
|
|
||||||
'account_title' => 'Want to try it?',
|
|
||||||
'account_text' => 'If you would like an account to try the sandbox, please request one from Hans Dijkema through the Racket Discourse pages.',
|
|
||||||
'account_link' => 'Go to Racket Discourse',
|
|
||||||
),
|
|
||||||
'nl' => array(
|
|
||||||
'email' => 'E-mailadres',
|
|
||||||
'password' => 'Wachtwoord',
|
|
||||||
'login' => 'Inloggen',
|
|
||||||
'account_title' => 'Wil je het eens proberen?',
|
|
||||||
'account_text' => 'Als je een account wilt om de sandbox eens uit te proberen, doe dan een verzoek aan Hans Dijkema via de Racket Discourse-pagina\'s.',
|
|
||||||
'account_link' => 'Naar Racket Discourse',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
$language = detect_login_language($texts, 'en');
|
$language = detect_login_language($texts, 'en');
|
||||||
$styleVersion = @filemtime(__DIR__ . '/styles.css') ?: time();
|
$styleVersion = @filemtime(__DIR__ . '/css/styles.css') ?: time();
|
||||||
|
|
||||||
if ($auth->currentUser() !== null && $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
if ($auth->currentUser() !== null && $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
header('Location: /');
|
header('Location: /');
|
||||||
@@ -98,46 +75,24 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
header('Content-Type: text/html; charset=utf-8');
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="<?= h($language) ?>">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title><?= h($pageTitle) ?></title>
|
|
||||||
<link rel="stylesheet" href="/styles.css?v=<?= h($styleVersion) ?>">
|
|
||||||
</head>
|
|
||||||
<body class="simple-doc login-page">
|
|
||||||
|
|
||||||
<main class="login-layout">
|
$loginText = $texts[$language];
|
||||||
<section class="login-panel">
|
$errorHtml = $error === ''
|
||||||
<h1><?= h($pageTitle) ?></h1>
|
? ''
|
||||||
|
: RacketSandboxTemplate::renderFile('partials/message.html', array(
|
||||||
|
'class' => 'error',
|
||||||
|
'message' => $error,
|
||||||
|
));
|
||||||
|
|
||||||
<?php if ($error !== ''): ?>
|
echo RacketSandboxTemplate::renderFile('login.html', array(
|
||||||
<div class="error"><?= h($error) ?></div>
|
'language' => $language,
|
||||||
<?php endif; ?>
|
'page_title' => $pageTitle,
|
||||||
|
'style_version' => $styleVersion,
|
||||||
<form method="post" action="/login.php">
|
'error_html' => $errorHtml,
|
||||||
|
'email_label' => $loginText['email'],
|
||||||
<label>
|
'password_label' => $loginText['password'],
|
||||||
<?= h($texts[$language]['email']) ?><br>
|
'login_label' => $loginText['login'],
|
||||||
<input type="email" name="email" autocomplete="username" required>
|
'account_title' => $loginText['account_title'],
|
||||||
</label>
|
'account_text' => $loginText['account_text'],
|
||||||
|
'account_link' => $loginText['account_link'],
|
||||||
<label>
|
));
|
||||||
<?= h($texts[$language]['password']) ?><br>
|
|
||||||
<input type="password" name="password" autocomplete="current-password" required>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button type="submit"><?= h($texts[$language]['login']) ?></button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<aside class="login-request-panel">
|
|
||||||
<h2><?= h($texts[$language]['account_title']) ?></h2>
|
|
||||||
<p><?= h($texts[$language]['account_text']) ?></p>
|
|
||||||
<p><a href="https://racket.discourse.group/"><?= h($texts[$language]['account_link']) ?></a></p>
|
|
||||||
</aside>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
+41
-77
@@ -23,10 +23,7 @@
|
|||||||
* - Eén next-id per gegenereerde HTML-pagina.
|
* - Eén next-id per gegenereerde HTML-pagina.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ini_set('display_errors', '1');
|
require_once __DIR__ . '/config/reporting.php';
|
||||||
ini_set('display_startup_errors', '1');
|
|
||||||
ini_set('log_errors', '1');
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
require_once __DIR__ . '/private/nexttoken.php';
|
require_once __DIR__ . '/private/nexttoken.php';
|
||||||
|
|
||||||
@@ -40,6 +37,7 @@ require_once __DIR__ . '/private/b64parts.php';
|
|||||||
require_once __DIR__ . '/private/base64config.php';
|
require_once __DIR__ . '/private/base64config.php';
|
||||||
require_once __DIR__ . '/private/lib/catalog-http.php';
|
require_once __DIR__ . '/private/lib/catalog-http.php';
|
||||||
require_once __DIR__ . '/private/lib/racket-data.php';
|
require_once __DIR__ . '/private/lib/racket-data.php';
|
||||||
|
require_once __DIR__ . '/private/Template.php';
|
||||||
|
|
||||||
define('DATA_DIR', __DIR__ . '/data');
|
define('DATA_DIR', __DIR__ . '/data');
|
||||||
define('CATALOG_PACKAGE_BASE', 'https://pkgs.racket-lang.org/pkg/');
|
define('CATALOG_PACKAGE_BASE', 'https://pkgs.racket-lang.org/pkg/');
|
||||||
@@ -123,11 +121,10 @@ function text_response($text, $status = 200)
|
|||||||
function fail_html($message, $status = 500)
|
function fail_html($message, $status = 500)
|
||||||
{
|
{
|
||||||
html_response(
|
html_response(
|
||||||
'<!doctype html><html><head><meta charset="utf-8">' .
|
RacketSandboxTemplate::renderFile('protocol-error.html', array(
|
||||||
'<title>Package error</title></head><body>' .
|
'title' => 'Package error',
|
||||||
'<h1>Package error</h1>' .
|
'message' => $message,
|
||||||
'<pre>' . h($message) . '</pre>' .
|
)),
|
||||||
'</body></html>',
|
|
||||||
$status
|
$status
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -362,79 +359,46 @@ function serve_package_page()
|
|||||||
'part' => $n,
|
'part' => $n,
|
||||||
));
|
));
|
||||||
|
|
||||||
$rows .=
|
$rows .= RacketSandboxTemplate::renderFile('partials/package-part-row.html', array(
|
||||||
'<tr>' .
|
'number' => $n,
|
||||||
'<td>' . h($n) . '</td>' .
|
'base64_bytes' => (string)$part['base64_bytes'],
|
||||||
'<td>' . h((string)$part['base64_bytes']) . '</td>' .
|
'url' => $url,
|
||||||
'<td><a href="' . h($url) . '">' . h($url) . '</a></td>' .
|
)) . "\n";
|
||||||
'</tr>' . "\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html_response('<!doctype html>
|
$sourceRows = '';
|
||||||
<html lang="nl">
|
$sourceValues = array(
|
||||||
<head>
|
'catalog source' => $info['source'],
|
||||||
<meta charset="utf-8">
|
'repo url' => $info['repo_url'],
|
||||||
<title>Package ' . h($package) . '</title>
|
'fetch status' => $zipInfo['status'] ?? '',
|
||||||
<link rel="stylesheet" href="/styles.css">
|
'default branch' => $zipInfo['default_branch'] ?? '',
|
||||||
</head>
|
'head sha' => $zipInfo['head_sha'] ?? '',
|
||||||
<body class="simple-doc">
|
'zip file' => $zipInfo['zip_file'] ?? '',
|
||||||
|
'zip bytes' => (string)($zipInfo['zip_bytes'] ?? ''),
|
||||||
|
'zip sha256' => $zipInfo['zip_sha256'] ?? '',
|
||||||
|
'parts status' => $manifest['parts_status'] ?? '',
|
||||||
|
'max base64 part size' => '<code>' . h((string)PACKAGE_ZIP_MAX_BASE64_KB) . '</code> KiB (<code>' . h((string)PACKAGE_ZIP_MAX_BASE64_BYTES) . '</code> bytes)',
|
||||||
|
'binary chunk size' => '<code>' . h((string)($manifest['binary_chunk_bytes'] ?? '')) . '</code> bytes',
|
||||||
|
'part count' => (string)$manifest['part_count'],
|
||||||
|
'next id' => $NEXT_ID,
|
||||||
|
);
|
||||||
|
|
||||||
<h1>Package ' . h($package) . '</h1>
|
foreach ($sourceValues as $label => $value) {
|
||||||
|
$valueHtml = strpos((string)$value, '<code>') !== false
|
||||||
|
? (string)$value
|
||||||
|
: '<code>' . h((string)$value) . '</code>';
|
||||||
|
|
||||||
<p>
|
$sourceRows .= RacketSandboxTemplate::renderFile('partials/package-source-row.html', array(
|
||||||
Deze pagina is HTML. Alle part-links hieronder geven <code>text/plain</code>
|
'label' => $label,
|
||||||
met base64-inhoud terug. Dezelfde <code>next</code> wordt gebruikt voor alle
|
'value_html' => $valueHtml,
|
||||||
part-links op deze pagina.
|
)) . "\n";
|
||||||
</p>
|
}
|
||||||
|
|
||||||
<h2>Bron</h2>
|
html_response(RacketSandboxTemplate::renderFile('package.html', array(
|
||||||
|
'package' => $package,
|
||||||
<table>
|
'source_rows_html' => $sourceRows,
|
||||||
<tr><th>catalog source</th><td><code>' . h($info['source']) . '</code></td></tr>
|
'part_rows_html' => $rows,
|
||||||
<tr><th>repo url</th><td><code>' . h($info['repo_url']) . '</code></td></tr>
|
)));
|
||||||
<tr><th>fetch status</th><td><code>' . h($zipInfo['status'] ?? '') . '</code></td></tr>
|
|
||||||
<tr><th>default branch</th><td><code>' . h($zipInfo['default_branch'] ?? '') . '</code></td></tr>
|
|
||||||
<tr><th>head sha</th><td><code>' . h($zipInfo['head_sha'] ?? '') . '</code></td></tr>
|
|
||||||
<tr><th>zip file</th><td><code>' . h($zipInfo['zip_file'] ?? '') . '</code></td></tr>
|
|
||||||
<tr><th>zip bytes</th><td><code>' . h((string)($zipInfo['zip_bytes'] ?? '')) . '</code></td></tr>
|
|
||||||
<tr><th>zip sha256</th><td><code>' . h($zipInfo['zip_sha256'] ?? '') . '</code></td></tr>
|
|
||||||
<tr><th>parts status</th><td><code>' . h($manifest['parts_status'] ?? '') . '</code></td></tr>
|
|
||||||
<tr><th>max base64 part size</th><td><code>' . h((string)PACKAGE_ZIP_MAX_BASE64_KB) . '</code> KiB (<code>' . h((string)PACKAGE_ZIP_MAX_BASE64_BYTES) . '</code> bytes)</td></tr>
|
|
||||||
<tr><th>binary chunk size</th><td><code>' . h((string)($manifest['binary_chunk_bytes'] ?? '')) . '</code> bytes</td></tr>
|
|
||||||
<tr><th>part count</th><td><code>' . h((string)$manifest['part_count']) . '</code></td></tr>
|
|
||||||
<tr><th>next id</th><td><code>' . h($NEXT_ID) . '</code></td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h2>Base64 parts</h2>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>part</th>
|
|
||||||
<th>base64 bytes</th>
|
|
||||||
<th>text/plain URL</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
' . $rows . '
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h2>Reconstructie in de sandbox</h2>
|
|
||||||
|
|
||||||
<pre>
|
|
||||||
# download alle links als:
|
|
||||||
# ' . h($package) . '.part.000001.b64
|
|
||||||
# ' . h($package) . '.part.000002.b64
|
|
||||||
# enz.
|
|
||||||
|
|
||||||
cat ' . h($package) . '.part.*.b64 > ' . h($package) . '.zip.b64
|
|
||||||
base64 -d ' . h($package) . '.zip.b64 > ' . h($package) . '.zip
|
|
||||||
raco pkg install --auto ./' . h($package) . '.zip
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function serve_package_part()
|
function serve_package_part()
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* Small explicit template renderer.
|
||||||
|
*
|
||||||
|
* Templates use escaped {{name}} placeholders and raw {{{name}}} placeholders.
|
||||||
|
* Values are provided as an array, so templates do not depend on globals.
|
||||||
|
*/
|
||||||
|
|
||||||
|
final class RacketSandboxTemplate
|
||||||
|
{
|
||||||
|
const DATA_DELIMITER = "\n===\n";
|
||||||
|
|
||||||
|
public static function renderFile($template, $values)
|
||||||
|
{
|
||||||
|
$parts = self::readFile($template);
|
||||||
|
|
||||||
|
return self::renderString($parts['html'], $values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function dataFile($template)
|
||||||
|
{
|
||||||
|
$parts = self::readFile($template);
|
||||||
|
$json = trim($parts['data']);
|
||||||
|
|
||||||
|
if ($json === '') {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($json, true);
|
||||||
|
|
||||||
|
if (!is_array($data)) {
|
||||||
|
throw new RuntimeException('Template data is invalid JSON: ' . $template);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function translationsFile($template)
|
||||||
|
{
|
||||||
|
$data = self::dataFile($template);
|
||||||
|
$translations = $data['translations'] ?? array();
|
||||||
|
|
||||||
|
if (!is_array($translations)) {
|
||||||
|
throw new RuntimeException('Template translations must be an object: ' . $template);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $translations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function renderString($source, $values)
|
||||||
|
{
|
||||||
|
$source = preg_replace_callback('/\{\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}\}/', function ($match) use ($values) {
|
||||||
|
return self::value($values, $match[1]);
|
||||||
|
}, $source);
|
||||||
|
|
||||||
|
return preg_replace_callback('/\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/', function ($match) use ($values) {
|
||||||
|
return self::escape(self::value($values, $match[1]));
|
||||||
|
}, $source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function escape($value)
|
||||||
|
{
|
||||||
|
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function value($values, $name)
|
||||||
|
{
|
||||||
|
if (!is_array($values) || !array_key_exists($name, $values)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string)$values[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function readFile($template)
|
||||||
|
{
|
||||||
|
$file = __DIR__ . '/views/' . ltrim((string)$template, '/');
|
||||||
|
|
||||||
|
if (!is_file($file)) {
|
||||||
|
throw new RuntimeException('Template not found: ' . $template);
|
||||||
|
}
|
||||||
|
|
||||||
|
$source = file_get_contents($file);
|
||||||
|
|
||||||
|
if ($source === false) {
|
||||||
|
throw new RuntimeException('Template could not be read: ' . $template);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strpos($source, "===\n") === 0) {
|
||||||
|
$chunks = array('', substr($source, 4));
|
||||||
|
} else {
|
||||||
|
$chunks = explode(self::DATA_DELIMITER, $source, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'html' => $chunks[0],
|
||||||
|
'data' => $chunks[1] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-13
@@ -14,6 +14,8 @@
|
|||||||
* Outside code should not inspect DB rows directly.
|
* Outside code should not inspect DB rows directly.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/Template.php';
|
||||||
|
|
||||||
class RacketSandboxAuthException extends Exception
|
class RacketSandboxAuthException extends Exception
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -696,7 +698,7 @@ class RacketSandboxAuth
|
|||||||
$this->messageHtml(
|
$this->messageHtml(
|
||||||
'Login required',
|
'Login required',
|
||||||
'Please log in to continue.',
|
'Please log in to continue.',
|
||||||
'<p><a href="/login.php">Login</a></p>'
|
RacketSandboxTemplate::renderFile('partials/login-link.html', array('login_label' => 'Login'))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,18 +707,12 @@ class RacketSandboxAuth
|
|||||||
http_response_code(200);
|
http_response_code(200);
|
||||||
header('Content-Type: text/html; charset=utf-8');
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
|
||||||
echo '<!doctype html>
|
echo RacketSandboxTemplate::renderFile('simple-message.html', array(
|
||||||
<html lang="en">
|
'language' => 'en',
|
||||||
<head>
|
'title' => $title,
|
||||||
<meta charset="utf-8">
|
'message' => $message,
|
||||||
<title>' . htmlspecialchars($title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</title>
|
'extra_html' => $extra,
|
||||||
</head>
|
));
|
||||||
<body>
|
|
||||||
<h1>' . htmlspecialchars($title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</h1>
|
|
||||||
<p>' . htmlspecialchars($message, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</p>
|
|
||||||
' . $extra . '
|
|
||||||
</body>
|
|
||||||
</html>';
|
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+117
-65
@@ -5,12 +5,19 @@
|
|||||||
* Shared page header renderer for logged-in application pages.
|
* Shared page header renderer for logged-in application pages.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/Template.php';
|
||||||
|
|
||||||
function app_header_h($s)
|
function app_header_h($s)
|
||||||
{
|
{
|
||||||
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
return RacketSandboxTemplate::escape($s);
|
||||||
}
|
}
|
||||||
|
|
||||||
function render_app_header($options)
|
function render_app_header($options)
|
||||||
|
{
|
||||||
|
echo app_header_html($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function app_header_html($options)
|
||||||
{
|
{
|
||||||
$title = (string)($options['title'] ?? '');
|
$title = (string)($options['title'] ?? '');
|
||||||
$navItems = $options['nav_items'] ?? array();
|
$navItems = $options['nav_items'] ?? array();
|
||||||
@@ -26,69 +33,114 @@ function render_app_header($options)
|
|||||||
$logoutLabel = (string)($options['logout_label'] ?? '');
|
$logoutLabel = (string)($options['logout_label'] ?? '');
|
||||||
$message = (string)($options['message'] ?? '');
|
$message = (string)($options['message'] ?? '');
|
||||||
$error = (string)($options['error'] ?? '');
|
$error = (string)($options['error'] ?? '');
|
||||||
?>
|
|
||||||
<header class="page-header">
|
|
||||||
<div class="page-titlebar">
|
|
||||||
<h1><?= app_header_h($title) ?></h1>
|
|
||||||
|
|
||||||
<nav class="header-nav" aria-label="<?= app_header_h($title) ?> navigation">
|
return RacketSandboxTemplate::renderFile('app-header.html', array(
|
||||||
<?php foreach ($navItems as $item): ?>
|
'title' => $title,
|
||||||
<?php if (!empty($item['separator_before'])): ?>
|
'nav_html' => app_header_nav_html($navItems),
|
||||||
<span class="nav-separator" aria-hidden="true">|</span>
|
'logout_html' => app_header_logout_html($logoutAction, $logoutLabel),
|
||||||
<?php endif; ?>
|
'user_html' => app_header_user_html($user, $userPrefix, $adminLabel),
|
||||||
<?php if (!empty($item['active'])): ?>
|
'language_html' => app_header_language_html($languageLabel, $language, $languages, $languageAction, $languageHidden),
|
||||||
<strong><?= app_header_h($item['label'] ?? '') ?></strong>
|
'message_html' => app_header_status_html('message', $message),
|
||||||
<?php else: ?>
|
'error_html' => app_header_status_html('error', $error),
|
||||||
<a href="<?= app_header_h($item['url'] ?? '#') ?>"><?= app_header_h($item['label'] ?? '') ?></a>
|
));
|
||||||
<?php endif; ?>
|
}
|
||||||
<?php endforeach; ?>
|
|
||||||
|
function app_header_status_html($class, $message)
|
||||||
<?php if ($logoutAction !== '' && $logoutLabel !== ''): ?>
|
{
|
||||||
<span class="nav-separator" aria-hidden="true">|</span>
|
if ($message === '') {
|
||||||
<form class="header-action-form" method="post" action="<?= app_header_h($logoutAction) ?>">
|
return '';
|
||||||
<input type="hidden" name="action" value="logout">
|
}
|
||||||
<button type="submit"><?= app_header_h($logoutLabel) ?></button>
|
|
||||||
</form>
|
return RacketSandboxTemplate::renderFile('partials/message.html', array(
|
||||||
<?php endif; ?>
|
'class' => $class,
|
||||||
|
'message' => $message,
|
||||||
<?php if ($user !== null): ?>
|
));
|
||||||
<span class="nav-separator" aria-hidden="true">|</span>
|
}
|
||||||
<span class="nav-user">
|
|
||||||
<?php if ($userPrefix !== ''): ?>
|
function app_header_nav_html($navItems)
|
||||||
<?= app_header_h($userPrefix) ?>
|
{
|
||||||
<?php endif; ?>
|
$html = '';
|
||||||
<?= app_header_h($user->displayName()) ?>
|
|
||||||
<?php if ($user->isAdmin()): ?>
|
foreach ($navItems as $item) {
|
||||||
<span class="small">(<?= app_header_h($adminLabel) ?>)</span>
|
if (!empty($item['separator_before'])) {
|
||||||
<?php endif; ?>
|
$html .= RacketSandboxTemplate::renderFile('partials/header-separator.html', array()) . "\n";
|
||||||
</span>
|
}
|
||||||
<?php endif; ?>
|
|
||||||
|
if (!empty($item['active'])) {
|
||||||
<form class="header-language-form" method="get" action="<?= app_header_h($languageAction) ?>">
|
$html .= RacketSandboxTemplate::renderFile('partials/header-nav-active.html', array(
|
||||||
<?php foreach ($languageHidden as $name => $value): ?>
|
'label' => $item['label'] ?? '',
|
||||||
<input type="hidden" name="<?= app_header_h($name) ?>" value="<?= app_header_h($value) ?>">
|
)) . "\n";
|
||||||
<?php endforeach; ?>
|
continue;
|
||||||
<label>
|
}
|
||||||
<?= app_header_h($languageLabel) ?>
|
|
||||||
<select name="lang" onchange="this.form.submit()">
|
$html .= RacketSandboxTemplate::renderFile('partials/header-nav-link.html', array(
|
||||||
<?php foreach ($languages as $lang => $label): ?>
|
'url' => $item['url'] ?? '#',
|
||||||
<option value="<?= app_header_h($lang) ?>" <?= $lang === $language ? 'selected' : '' ?>>
|
'label' => $item['label'] ?? '',
|
||||||
<?= app_header_h($label) ?>
|
)) . "\n";
|
||||||
</option>
|
}
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
return $html;
|
||||||
</label>
|
}
|
||||||
</form>
|
|
||||||
</nav>
|
function app_header_logout_html($logoutAction, $logoutLabel)
|
||||||
</div>
|
{
|
||||||
|
if ($logoutAction === '' || $logoutLabel === '') {
|
||||||
<?php if ($message !== ''): ?>
|
return '';
|
||||||
<div class="message"><?= app_header_h($message) ?></div>
|
}
|
||||||
<?php endif; ?>
|
|
||||||
|
return RacketSandboxTemplate::renderFile('partials/header-logout.html', array(
|
||||||
<?php if ($error !== ''): ?>
|
'logout_action' => $logoutAction,
|
||||||
<div class="error"><?= app_header_h($error) ?></div>
|
'logout_label' => $logoutLabel,
|
||||||
<?php endif; ?>
|
)) . "\n";
|
||||||
</header>
|
}
|
||||||
<?php
|
|
||||||
|
function app_header_user_html($user, $userPrefix, $adminLabel)
|
||||||
|
{
|
||||||
|
if ($user === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$adminHtml = '';
|
||||||
|
|
||||||
|
if ($user->isAdmin()) {
|
||||||
|
$adminHtml = RacketSandboxTemplate::renderFile('partials/header-admin-badge.html', array(
|
||||||
|
'admin_label' => $adminLabel,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return RacketSandboxTemplate::renderFile('partials/header-user.html', array(
|
||||||
|
'user_prefix' => $userPrefix,
|
||||||
|
'display_name' => $user->displayName(),
|
||||||
|
'admin_html' => $adminHtml,
|
||||||
|
)) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
function app_header_language_html($languageLabel, $language, $languages, $languageAction, $languageHidden)
|
||||||
|
{
|
||||||
|
$hiddenInputsHtml = '';
|
||||||
|
|
||||||
|
foreach ($languageHidden as $name => $value) {
|
||||||
|
$hiddenInputsHtml .= RacketSandboxTemplate::renderFile('partials/header-hidden-input.html', array(
|
||||||
|
'name' => $name,
|
||||||
|
'value' => $value,
|
||||||
|
)) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$languageOptionsHtml = '';
|
||||||
|
|
||||||
|
foreach ($languages as $lang => $label) {
|
||||||
|
$selected = $lang === $language ? ' selected' : '';
|
||||||
|
$languageOptionsHtml .= RacketSandboxTemplate::renderFile('partials/select-option.html', array(
|
||||||
|
'value' => $lang,
|
||||||
|
'selected' => $selected,
|
||||||
|
'label' => $label,
|
||||||
|
)) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return RacketSandboxTemplate::renderFile('partials/header-language.html', array(
|
||||||
|
'language_action' => $languageAction,
|
||||||
|
'hidden_inputs_html' => $hiddenInputsHtml,
|
||||||
|
'language_label' => $languageLabel,
|
||||||
|
'language_options_html' => $languageOptionsHtml,
|
||||||
|
)) . "\n";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,43 @@ class LanguageStore
|
|||||||
return $fallback !== null ? (string)$fallback : $key;
|
return $fallback !== null ? (string)$fallback : $key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function translateFormat($key, $language, $values = array(), $fallback = null)
|
||||||
|
{
|
||||||
|
return self::formatText($this->translate($key, $language, $fallback), $values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function formatText($text, $values, $maxDepth = 8)
|
||||||
|
{
|
||||||
|
if (!is_array($values) || count($values) === 0) {
|
||||||
|
return (string)$text;
|
||||||
|
}
|
||||||
|
|
||||||
|
$text = (string)$text;
|
||||||
|
|
||||||
|
for ($depth = 0; $depth < $maxDepth; $depth++) {
|
||||||
|
$changed = false;
|
||||||
|
|
||||||
|
$next = preg_replace_callback('/\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/', function ($match) use ($values, &$changed) {
|
||||||
|
$name = $match[1];
|
||||||
|
|
||||||
|
if (!array_key_exists($name, $values)) {
|
||||||
|
return $match[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$changed = true;
|
||||||
|
return (string)$values[$name];
|
||||||
|
}, $text);
|
||||||
|
|
||||||
|
$text = $next;
|
||||||
|
|
||||||
|
if (!$changed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
public function setTranslation($key, $language, $text)
|
public function setTranslation($key, $language, $text)
|
||||||
{
|
{
|
||||||
$key = $this->safeKey($key);
|
$key = $this->safeKey($key);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* Helpers for reading non-rendered data from view templates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/Template.php';
|
||||||
|
|
||||||
|
function seed_template_translations($languageStore, $template)
|
||||||
|
{
|
||||||
|
$languageStore->seedDefaults(RacketSandboxTemplate::translationsFile($template));
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<header class="page-header">
|
||||||
|
<div class="page-titlebar">
|
||||||
|
<h1>{{title}}</h1>
|
||||||
|
|
||||||
|
<nav class="header-nav" aria-label="{{title}} navigation">
|
||||||
|
{{{nav_html}}}
|
||||||
|
{{{logout_html}}}
|
||||||
|
{{{user_html}}}
|
||||||
|
{{{language_html}}}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{{message_html}}}
|
||||||
|
{{{error_html}}}
|
||||||
|
</header>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Racket bootstrap</title>
|
||||||
|
<link rel="stylesheet" href="/css/styles.css">
|
||||||
|
</head>
|
||||||
|
<body class="simple-doc">
|
||||||
|
|
||||||
|
<h1>Racket bootstrap</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<code>racket.zip</code> is vooraf via de configuratiepagina gesplitst naar
|
||||||
|
parts in de map <code>data</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Package index: <a href="{{pkg_url}}">Racket package index</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Bronbestand: <code>config/racket.zip</code><br>
|
||||||
|
Bronbestand bytes: <code>{{zip_size}}</code><br>
|
||||||
|
Maximale base64 part-grootte: <code>{{max_base64_kb}}</code> KiB (<code>{{max_base64_bytes}}</code> bytes)<br>
|
||||||
|
Binaire chunk-grootte: <code>{{binary_chunk_bytes}}</code> bytes<br>
|
||||||
|
Aantal parts: <code>{{part_count}}</code><br>
|
||||||
|
Parts gemaakt op: <code>{{created_at}}</code><br>
|
||||||
|
next-id voor alle links op deze pagina: <code>{{next_id}}</code>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Elke link hieronder geeft <strong>text/plain</strong> met de
|
||||||
|
<strong>base64-representatie van een binair part</strong>.
|
||||||
|
De URL bevat alleen een nummer, geen bestandsnaam en geen extensie.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>partnummer</th>
|
||||||
|
<th>bytes</th>
|
||||||
|
<th>base64 text/plain URL</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{{part_rows_html}}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Reconstructie in de sandbox</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Decodeer ieder base64-part afzonderlijk naar een binair part. Plak daarna de
|
||||||
|
binaire parts in numerieke volgorde aan elkaar.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
base64 -d part-000001.txt > part-000001
|
||||||
|
base64 -d part-000002.txt > part-000002
|
||||||
|
base64 -d part-000003.txt > part-000003
|
||||||
|
# enzovoort
|
||||||
|
|
||||||
|
cat part-* > racket.zip
|
||||||
|
unzip racket.zip -d /tmp/racket
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="{{language}}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{title}}</title>
|
||||||
|
<link rel="stylesheet" href="/css/styles.css?v={{style_version}}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
{{{header_html}}}
|
||||||
|
|
||||||
|
<main class="page-main dashboard-main">
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>{{download_settings_label}}</h2>
|
||||||
|
|
||||||
|
<form method="post" action="/admin-config?lang={{language_url}}" class="admin-form-grid">
|
||||||
|
<input type="hidden" name="action" value="update_config">
|
||||||
|
<label>
|
||||||
|
{{racket_zip_chunk_label}}<br>
|
||||||
|
<input type="number" name="racket_zip_max_base64_kb" min="1" step="1" value="{{racket_zip_max_base64_kb}}" required>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
{{package_zip_chunk_label}}<br>
|
||||||
|
<input type="number" name="package_zip_max_base64_kb" min="1" step="1" value="{{package_zip_max_base64_kb}}" required>
|
||||||
|
</label>
|
||||||
|
<button type="submit">{{save_configuration_label}}</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="small">{{chunk_size_hint}}</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>{{racket_zip_chunk_label}}</th>
|
||||||
|
<td>{{{racket_zip_base64_chunk_size}}}</td>
|
||||||
|
<td>{{{racket_zip_effective_binary_chunk}}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{{package_zip_chunk_label}}</th>
|
||||||
|
<td>{{{package_zip_base64_chunk_size}}}</td>
|
||||||
|
<td>{{{package_zip_effective_binary_chunk}}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{{racket_zip_source_label}}</th>
|
||||||
|
<td><code>config/racket.zip</code></td>
|
||||||
|
<td>{{{racket_zip_file_size}}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{{racket_parts_label}}</th>
|
||||||
|
<td><code>{{racket_part_count}}</code></td>
|
||||||
|
<td>{{racket_parts_status}}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>{{maintenance_label}}</h2>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>{{next_tokens_label}}</legend>
|
||||||
|
|
||||||
|
<form method="post" action="/admin-config?lang={{language_url}}">
|
||||||
|
<input type="hidden" name="action" value="cleanup_tokens">
|
||||||
|
<button type="submit">{{remove_expired_tokens_label}}</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="small">{{cleanup_help}}</p>
|
||||||
|
|
||||||
|
<h3>{{current_next_tokens_label}}</h3>
|
||||||
|
|
||||||
|
{{{current_tokens_html}}}
|
||||||
|
</fieldset>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
===
|
||||||
|
{
|
||||||
|
"translations": {
|
||||||
|
"app.title": {
|
||||||
|
"en": "Racket sandbox",
|
||||||
|
"nl": "Racket sandbox"
|
||||||
|
},
|
||||||
|
"app.manage_prompts": {
|
||||||
|
"en": "Manage prompts",
|
||||||
|
"nl": "Prompts beheren"
|
||||||
|
},
|
||||||
|
"app.user_management": {
|
||||||
|
"en": "User management",
|
||||||
|
"nl": "Gebruikersbeheer"
|
||||||
|
},
|
||||||
|
"app.configuration": {
|
||||||
|
"en": "Configuration",
|
||||||
|
"nl": "Configuratie"
|
||||||
|
},
|
||||||
|
"app.logout": {
|
||||||
|
"en": "Logout",
|
||||||
|
"nl": "Uitloggen"
|
||||||
|
},
|
||||||
|
"app.language": {
|
||||||
|
"en": "Language",
|
||||||
|
"nl": "Taal"
|
||||||
|
},
|
||||||
|
"app.logged_in_as": {
|
||||||
|
"en": "Logged in as:",
|
||||||
|
"nl": "Ingelogd als:"
|
||||||
|
},
|
||||||
|
"app.admin": {
|
||||||
|
"en": "Admin",
|
||||||
|
"nl": "Admin"
|
||||||
|
},
|
||||||
|
"app.back_to_sandbox": {
|
||||||
|
"en": "Back to Racket sandbox",
|
||||||
|
"nl": "Terug naar Racket sandbox"
|
||||||
|
},
|
||||||
|
"app.download_settings": {
|
||||||
|
"en": "Download settings",
|
||||||
|
"nl": "Downloadinstellingen"
|
||||||
|
},
|
||||||
|
"app.maintenance": {
|
||||||
|
"en": "Maintenance",
|
||||||
|
"nl": "Onderhoud"
|
||||||
|
},
|
||||||
|
"app.next_tokens": {
|
||||||
|
"en": "Next tokens",
|
||||||
|
"nl": "Next-tokens"
|
||||||
|
},
|
||||||
|
"app.current_next_tokens": {
|
||||||
|
"en": "Next tokens",
|
||||||
|
"nl": "Next-tokens"
|
||||||
|
},
|
||||||
|
"app.token": {
|
||||||
|
"en": "Token",
|
||||||
|
"nl": "Token"
|
||||||
|
},
|
||||||
|
"app.created_at": {
|
||||||
|
"en": "Created at",
|
||||||
|
"nl": "Aangemaakt op"
|
||||||
|
},
|
||||||
|
"app.expires_at": {
|
||||||
|
"en": "Expires at",
|
||||||
|
"nl": "Verloopt op"
|
||||||
|
},
|
||||||
|
"app.no_current_next_tokens": {
|
||||||
|
"en": "No next tokens.",
|
||||||
|
"nl": "Geen next-tokens."
|
||||||
|
},
|
||||||
|
"app.remove_expired_tokens": {
|
||||||
|
"en": "Remove expired next tokens",
|
||||||
|
"nl": "Verlopen next-tokens verwijderen"
|
||||||
|
},
|
||||||
|
"app.racket_zip_chunk_kb": {
|
||||||
|
"en": "Racket installation max base64 chunk size (KiB)",
|
||||||
|
"nl": "Maximale base64-chunkgrootte Racket-installatie (KiB)"
|
||||||
|
},
|
||||||
|
"app.package_zip_chunk_kb": {
|
||||||
|
"en": "Package/module max base64 chunk size (KiB)",
|
||||||
|
"nl": "Maximale base64-chunkgrootte packages/modules (KiB)"
|
||||||
|
},
|
||||||
|
"app.save_configuration": {
|
||||||
|
"en": "Save configuration",
|
||||||
|
"nl": "Configuratie opslaan"
|
||||||
|
},
|
||||||
|
"app.configuration_saved": {
|
||||||
|
"en": "Configuration saved.",
|
||||||
|
"nl": "Configuratie opgeslagen."
|
||||||
|
},
|
||||||
|
"app.configuration_saved_with_parts": {
|
||||||
|
"en": "Configuration saved. Racket installation parts regenerated: {{count}}",
|
||||||
|
"nl": "Configuratie opgeslagen. Racket-installatie parts opnieuw gemaakt: {{count}}"
|
||||||
|
},
|
||||||
|
"app.racket_parts_regenerated": {
|
||||||
|
"en": "Racket installation parts regenerated: {{count}}",
|
||||||
|
"nl": "Racket-installatie parts opnieuw gemaakt: {{count}}"
|
||||||
|
},
|
||||||
|
"app.chunk_size_hint_v2": {
|
||||||
|
"en": "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.",
|
||||||
|
"nl": "Waarden zijn maximale base64-payloadgroottes in KiB. Een binaire chunk van {{chunk_size}} KiB wordt {{base64_chunk_size}} KiB base64. Racket-installatie parts worden opnieuw gemaakt wanneer deze configuratie wordt opgeslagen."
|
||||||
|
},
|
||||||
|
"app.effective_binary_chunk": {
|
||||||
|
"en": "Effective binary chunk",
|
||||||
|
"nl": "Effectieve binaire chunk"
|
||||||
|
},
|
||||||
|
"app.effective_binary_chunk_bytes": {
|
||||||
|
"en": "Effective binary chunk: {{bytes}} bytes",
|
||||||
|
"nl": "Effectieve binaire chunk: {{bytes}} bytes"
|
||||||
|
},
|
||||||
|
"app.base64_chunk_size_kib": {
|
||||||
|
"en": "{{size}} KiB",
|
||||||
|
"nl": "{{size}} KiB"
|
||||||
|
},
|
||||||
|
"app.file_size_bytes": {
|
||||||
|
"en": "{{bytes}} bytes",
|
||||||
|
"nl": "{{bytes}} bytes"
|
||||||
|
},
|
||||||
|
"app.racket_zip_source": {
|
||||||
|
"en": "Racket installation source",
|
||||||
|
"nl": "Bronbestand Racket-installatie"
|
||||||
|
},
|
||||||
|
"app.racket_parts": {
|
||||||
|
"en": "Racket installation parts",
|
||||||
|
"nl": "Racket-installatie parts"
|
||||||
|
},
|
||||||
|
"app.racket_parts_current": {
|
||||||
|
"en": "current",
|
||||||
|
"nl": "actueel"
|
||||||
|
},
|
||||||
|
"app.racket_parts_current_with_date": {
|
||||||
|
"en": "current, {{created_at}}",
|
||||||
|
"nl": "actueel, {{created_at}}"
|
||||||
|
},
|
||||||
|
"app.racket_parts_missing": {
|
||||||
|
"en": "missing or outdated; save configuration to regenerate",
|
||||||
|
"nl": "ontbreken of verouderd; sla configuratie op om opnieuw te maken"
|
||||||
|
},
|
||||||
|
"app.expired_tokens_removed": {
|
||||||
|
"en": "Expired next tokens removed: {{count}}",
|
||||||
|
"nl": "Verlopen next-tokens verwijderd: {{count}}"
|
||||||
|
},
|
||||||
|
"app.cleanup_help": {
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="{{language}}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{title}}</title>
|
||||||
|
<link rel="stylesheet" href="/css/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
{{{header_html}}}
|
||||||
|
|
||||||
|
<main class="page-main dashboard-main">
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>{{bootstrap_link_label}}</h2>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>{{generate_bootstrap_link_label}}</legend>
|
||||||
|
|
||||||
|
<div class="bootstrap-result-grid {{bootstrap_result_class}}">
|
||||||
|
<div>
|
||||||
|
<form method="post" action="/?lang={{language_url}}">
|
||||||
|
<input type="hidden" name="action" value="issue_bootstrap">
|
||||||
|
|
||||||
|
<label>
|
||||||
|
{{ttl_minutes_label}}<br>
|
||||||
|
<input type="number"
|
||||||
|
name="ttl_minutes"
|
||||||
|
value="{{bootstrap_ttl_minutes}}"
|
||||||
|
min="{{bootstrap_ttl_min_minutes}}"
|
||||||
|
max="{{bootstrap_ttl_max_minutes}}">
|
||||||
|
</label>
|
||||||
|
<p class="small">{{ttl_range_help}}</p>
|
||||||
|
|
||||||
|
<button type="submit">{{generate_bootstrap_link_label}}</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{{generated_link_html}}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{{prompt_panel_html}}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/js/clipboard.js" defer></script>
|
||||||
|
<script src="/js/bootstrap-prompt.js" defer></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
===
|
||||||
|
{
|
||||||
|
"translations": {
|
||||||
|
"app.title": {
|
||||||
|
"en": "Racket sandbox",
|
||||||
|
"nl": "Racket sandbox"
|
||||||
|
},
|
||||||
|
"app.manage_prompts": {
|
||||||
|
"en": "Manage prompts",
|
||||||
|
"nl": "Prompts beheren"
|
||||||
|
},
|
||||||
|
"app.logout": {
|
||||||
|
"en": "Logout",
|
||||||
|
"nl": "Uitloggen"
|
||||||
|
},
|
||||||
|
"app.language": {
|
||||||
|
"en": "Language",
|
||||||
|
"nl": "Taal"
|
||||||
|
},
|
||||||
|
"app.logged_in_as": {
|
||||||
|
"en": "Logged in as:",
|
||||||
|
"nl": "Ingelogd als:"
|
||||||
|
},
|
||||||
|
"app.admin": {
|
||||||
|
"en": "admin",
|
||||||
|
"nl": "admin"
|
||||||
|
},
|
||||||
|
"app.bootstrap_link": {
|
||||||
|
"en": "Bootstrap link",
|
||||||
|
"nl": "Bootstraplink"
|
||||||
|
},
|
||||||
|
"app.generate_bootstrap_link": {
|
||||||
|
"en": "Generate bootstrap link",
|
||||||
|
"nl": "Bootstraplink genereren"
|
||||||
|
},
|
||||||
|
"app.ttl_minutes": {
|
||||||
|
"en": "TTL in minutes",
|
||||||
|
"nl": "TTL in minuten"
|
||||||
|
},
|
||||||
|
"app.ttl_range_help": {
|
||||||
|
"en": "Allowed range: 30 minutes to 8 hours.",
|
||||||
|
"nl": "Toegestaan bereik: 30 minuten tot 8 uur."
|
||||||
|
},
|
||||||
|
"app.generated_link": {
|
||||||
|
"en": "Generated link",
|
||||||
|
"nl": "Gegenereerde link"
|
||||||
|
},
|
||||||
|
"app.copy": {
|
||||||
|
"en": "Copy",
|
||||||
|
"nl": "Kopieer"
|
||||||
|
},
|
||||||
|
"app.copied": {
|
||||||
|
"en": "Copied",
|
||||||
|
"nl": "Gekopieerd"
|
||||||
|
},
|
||||||
|
"app.generated_link_help": {
|
||||||
|
"en": "Give this link to the AI agent. The agent should start from this link and then only follow links from the generated HTML pages.",
|
||||||
|
"nl": "Geef deze link aan de AI-agent. De agent moet vanaf deze link starten en daarna alleen links volgen vanuit de gegenereerde HTML-paginas."
|
||||||
|
},
|
||||||
|
"app.select_prompt": {
|
||||||
|
"en": "Select prompt",
|
||||||
|
"nl": "Prompt selecteren"
|
||||||
|
},
|
||||||
|
"app.copy_full_prompt": {
|
||||||
|
"en": "Copy full prompt",
|
||||||
|
"nl": "Volledige prompt kopieren"
|
||||||
|
},
|
||||||
|
"app.bootstrap_prompt_help": {
|
||||||
|
"en": "Choose one of your prompts. The placeholder {{bootstrap-racket-link}} is replaced by the generated bootstrap link.",
|
||||||
|
"nl": "Kies een van je prompts. De placeholder {{bootstrap-racket-link}} wordt vervangen door de gegenereerde bootstraplink."
|
||||||
|
},
|
||||||
|
"app.no_bootstrap_prompts": {
|
||||||
|
"en": "No personal prompts are available for this language. Copy a default prompt first from prompt management.",
|
||||||
|
"nl": "Er zijn geen persoonlijke prompts beschikbaar voor deze taal. Kopieer eerst een standaardprompt vanuit promptbeheer."
|
||||||
|
},
|
||||||
|
"app.user_management": {
|
||||||
|
"en": "User management",
|
||||||
|
"nl": "Gebruikersbeheer"
|
||||||
|
},
|
||||||
|
"app.configuration": {
|
||||||
|
"en": "Configuration",
|
||||||
|
"nl": "Configuratie"
|
||||||
|
},
|
||||||
|
"app.user_management_help": {
|
||||||
|
"en": "Users are registered manually by email address, full name and password. This page only manages existing users.",
|
||||||
|
"nl": "Gebruikers worden handmatig geregistreerd met e-mailadres, volledige naam en wachtwoord. Deze pagina beheert alleen bestaande gebruikers."
|
||||||
|
},
|
||||||
|
"app.id": {
|
||||||
|
"en": "ID",
|
||||||
|
"nl": "ID"
|
||||||
|
},
|
||||||
|
"app.full_name": {
|
||||||
|
"en": "Full name",
|
||||||
|
"nl": "Volledige naam"
|
||||||
|
},
|
||||||
|
"app.email": {
|
||||||
|
"en": "Email",
|
||||||
|
"nl": "E-mail"
|
||||||
|
},
|
||||||
|
"app.enabled": {
|
||||||
|
"en": "Enabled",
|
||||||
|
"nl": "Ingeschakeld"
|
||||||
|
},
|
||||||
|
"app.created": {
|
||||||
|
"en": "Created",
|
||||||
|
"nl": "Gemaakt"
|
||||||
|
},
|
||||||
|
"app.last_login": {
|
||||||
|
"en": "Last login",
|
||||||
|
"nl": "Laatste login"
|
||||||
|
},
|
||||||
|
"app.actions": {
|
||||||
|
"en": "Actions",
|
||||||
|
"nl": "Acties"
|
||||||
|
},
|
||||||
|
"app.yes": {
|
||||||
|
"en": "yes",
|
||||||
|
"nl": "ja"
|
||||||
|
},
|
||||||
|
"app.no": {
|
||||||
|
"en": "no",
|
||||||
|
"nl": "nee"
|
||||||
|
},
|
||||||
|
"app.save_flags": {
|
||||||
|
"en": "Save flags",
|
||||||
|
"nl": "Vlaggen opslaan"
|
||||||
|
},
|
||||||
|
"app.new_password": {
|
||||||
|
"en": "New password",
|
||||||
|
"nl": "Nieuw wachtwoord"
|
||||||
|
},
|
||||||
|
"app.change_password": {
|
||||||
|
"en": "Change password",
|
||||||
|
"nl": "Wachtwoord wijzigen"
|
||||||
|
},
|
||||||
|
"app.delete_user": {
|
||||||
|
"en": "Delete user",
|
||||||
|
"nl": "Gebruiker verwijderen"
|
||||||
|
},
|
||||||
|
"app.delete_user_confirm": {
|
||||||
|
"en": "Delete user",
|
||||||
|
"nl": "Gebruiker verwijderen"
|
||||||
|
},
|
||||||
|
"app.cannot_delete_self": {
|
||||||
|
"en": "You cannot delete your own account.",
|
||||||
|
"nl": "Je kunt je eigen account niet verwijderen."
|
||||||
|
},
|
||||||
|
"app.bootstrap_link_issued": {
|
||||||
|
"en": "Bootstrap link issued.",
|
||||||
|
"nl": "Bootstraplink aangemaakt."
|
||||||
|
},
|
||||||
|
"app.password_changed_for": {
|
||||||
|
"en": "Password changed for: {{email}}",
|
||||||
|
"nl": "Wachtwoord gewijzigd voor: {{email}}"
|
||||||
|
},
|
||||||
|
"app.user_flags_updated": {
|
||||||
|
"en": "User flags updated.",
|
||||||
|
"nl": "Gebruikersvlaggen bijgewerkt."
|
||||||
|
},
|
||||||
|
"app.user_deleted": {
|
||||||
|
"en": "User deleted.",
|
||||||
|
"nl": "Gebruiker verwijderd."
|
||||||
|
},
|
||||||
|
"app.admin_rights_required": {
|
||||||
|
"en": "Admin rights required.",
|
||||||
|
"nl": "Adminrechten vereist."
|
||||||
|
},
|
||||||
|
"app.cannot_remove_own_admin": {
|
||||||
|
"en": "You cannot remove your own admin rights.",
|
||||||
|
"nl": "Je kunt je eigen adminrechten niet verwijderen."
|
||||||
|
},
|
||||||
|
"app.cannot_disable_self": {
|
||||||
|
"en": "You cannot disable your own account.",
|
||||||
|
"nl": "Je kunt je eigen account niet uitschakelen."
|
||||||
|
},
|
||||||
|
"app.unknown_action": {
|
||||||
|
"en": "Unknown action: {{action}}",
|
||||||
|
"nl": "Onbekende actie: {{action}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="{{language}}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{page_title}}</title>
|
||||||
|
<link rel="stylesheet" href="/css/styles.css?v={{style_version}}">
|
||||||
|
</head>
|
||||||
|
<body class="simple-doc login-page">
|
||||||
|
|
||||||
|
<main class="login-layout">
|
||||||
|
<section class="login-panel">
|
||||||
|
<h1>{{page_title}}</h1>
|
||||||
|
|
||||||
|
{{{error_html}}}
|
||||||
|
|
||||||
|
<form method="post" action="/login.php">
|
||||||
|
|
||||||
|
<label>
|
||||||
|
{{email_label}}<br>
|
||||||
|
<input type="email" name="email" autocomplete="username" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
{{password_label}}<br>
|
||||||
|
<input type="password" name="password" autocomplete="current-password" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button type="submit">{{login_label}}</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="login-request-panel">
|
||||||
|
<h2>{{account_title}}</h2>
|
||||||
|
<p>{{account_text}}</p>
|
||||||
|
<p><a href="https://racket.discourse.group/">{{account_link}}</a></p>
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
===
|
||||||
|
{
|
||||||
|
"translations": {
|
||||||
|
"en": {
|
||||||
|
"email": "Email address",
|
||||||
|
"password": "Password",
|
||||||
|
"login": "Login",
|
||||||
|
"account_title": "Want to try it?",
|
||||||
|
"account_text": "If you would like an account to try the sandbox, please request one from Hans Dijkema through the Racket Discourse pages.",
|
||||||
|
"account_link": "Go to Racket Discourse"
|
||||||
|
},
|
||||||
|
"nl": {
|
||||||
|
"email": "E-mailadres",
|
||||||
|
"password": "Wachtwoord",
|
||||||
|
"login": "Inloggen",
|
||||||
|
"account_title": "Wil je het eens proberen?",
|
||||||
|
"account_text": "Als je een account wilt om de sandbox eens uit te proberen, doe dan een verzoek aan Hans Dijkema via de Racket Discourse-pagina's.",
|
||||||
|
"account_link": "Naar Racket Discourse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Package {{package}}</title>
|
||||||
|
<link rel="stylesheet" href="/css/styles.css">
|
||||||
|
</head>
|
||||||
|
<body class="simple-doc">
|
||||||
|
|
||||||
|
<h1>Package {{package}}</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Deze pagina is HTML. Alle part-links hieronder geven <code>text/plain</code>
|
||||||
|
met base64-inhoud terug. Dezelfde <code>next</code> wordt gebruikt voor alle
|
||||||
|
part-links op deze pagina.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Bron</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
{{{source_rows_html}}}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Base64 parts</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>part</th>
|
||||||
|
<th>base64 bytes</th>
|
||||||
|
<th>text/plain URL</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{{part_rows_html}}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Reconstructie in de sandbox</h2>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
# download alle links als:
|
||||||
|
# {{package}}.part.000001.b64
|
||||||
|
# {{package}}.part.000002.b64
|
||||||
|
# enz.
|
||||||
|
|
||||||
|
cat {{package}}.part.*.b64 > {{package}}.zip.b64
|
||||||
|
base64 -d {{package}}.zip.b64 > {{package}}.zip
|
||||||
|
raco pkg install --auto ./{{package}}.zip
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<tr class="{{status_class}}">
|
||||||
|
<td><code>{{token}}</code></td>
|
||||||
|
<td>{{created_at}}</td>
|
||||||
|
<td>{{expires_at}}</td>
|
||||||
|
</tr>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>{{token_label}}</th>
|
||||||
|
<th>{{created_at_label}}</th>
|
||||||
|
<th>{{expires_at_label}}</th>
|
||||||
|
</tr>
|
||||||
|
{{{token_rows_html}}}
|
||||||
|
</table>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<span class="small">({{admin_label}})</span>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<input type="hidden" name="{{name}}" value="{{value}}">
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<form class="header-language-form" method="get" action="{{language_action}}">
|
||||||
|
{{{hidden_inputs_html}}}
|
||||||
|
<label>
|
||||||
|
{{language_label}}
|
||||||
|
<select name="lang" onchange="this.form.submit()">
|
||||||
|
{{{language_options_html}}}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<span class="nav-separator" aria-hidden="true">|</span>
|
||||||
|
<form class="header-action-form" method="post" action="{{logout_action}}">
|
||||||
|
<input type="hidden" name="action" value="logout">
|
||||||
|
<button type="submit">{{logout_label}}</button>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<strong>{{label}}</strong>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<a href="{{url}}">{{label}}</a>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<span class="nav-separator" aria-hidden="true">|</span>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<span class="nav-separator" aria-hidden="true">|</span>
|
||||||
|
<span class="nav-user">
|
||||||
|
{{user_prefix}}
|
||||||
|
{{display_name}}
|
||||||
|
{{{admin_html}}}
|
||||||
|
</span>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<h3>{{generated_link_label}}</h3>
|
||||||
|
<div class="generated-link-row">
|
||||||
|
<a href="{{issued_link}}" target="_blank" rel="noopener noreferrer">{{issued_link}}</a>
|
||||||
|
<button type="button"
|
||||||
|
class="js-copy-button"
|
||||||
|
data-copy-text="{{issued_link}}"
|
||||||
|
data-copy-label="{{copy_label}}"
|
||||||
|
data-copied-label="{{copied_label}}">
|
||||||
|
{{copy_label}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<div class="bootstrap-prompt-tool">
|
||||||
|
<label>
|
||||||
|
{{select_prompt_label}}<br>
|
||||||
|
<select id="bootstrapPromptSelect">
|
||||||
|
{{{prompt_options_html}}}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<textarea id="bootstrapPromptOutput" readonly rows="12"></textarea>
|
||||||
|
|
||||||
|
<button type="button"
|
||||||
|
class="js-copy-button"
|
||||||
|
data-copy-target="bootstrapPromptOutput"
|
||||||
|
data-copy-label="{{copy_label}}"
|
||||||
|
data-copied-label="{{copied_label}}">
|
||||||
|
{{copy_full_prompt_label}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<script type="application/json" id="bootstrapPromptData">
|
||||||
|
{{{bootstrap_prompt_json}}}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<div>
|
||||||
|
<h3>{{copy_full_prompt_label}}</h3>
|
||||||
|
<p>{{bootstrap_prompt_help}}</p>
|
||||||
|
|
||||||
|
{{{prompt_tool_html}}}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<p><a href="/login.php">{{login_label}}</a></p>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<div class="{{class}}">{{message}}</div>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<tr id="pkg-{{id}}">
|
||||||
|
<td>{{index}}</td>
|
||||||
|
<td class="pkg-name">
|
||||||
|
<a class="pkg-link" href="{{url}}">{{name}}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<tr>
|
||||||
|
<td>{{number}}</td>
|
||||||
|
<td>{{base64_bytes}}</td>
|
||||||
|
<td><a href="{{url}}">{{url}}</a></td>
|
||||||
|
</tr>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<tr><th>{{label}}</th><td>{{{value_html}}}</td></tr>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<p class="{{class}}">{{text}}</p>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<div class="default-admin-notice">
|
||||||
|
<strong>{{badge}}</strong>
|
||||||
|
<span>{{hint}}</span>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<details class="create-drawer">
|
||||||
|
<summary>{{create_default_label}}</summary>
|
||||||
|
<form method="post" action="/prompts?lang={{language_url}}&mode=defaults">
|
||||||
|
<input type="hidden" name="action" value="create_default">
|
||||||
|
<label>{{default_key_label}}<br><input type="text" name="default_key" placeholder="bootstrap-racket"></label>
|
||||||
|
<label>{{name_label}}<br><input type="text" name="name"></label>
|
||||||
|
<label>{{language_label}}<br>
|
||||||
|
<select name="language">
|
||||||
|
{{{language_options_html}}}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>{{prompt_content_label}}<br><textarea name="content" rows="7"></textarea></label>
|
||||||
|
<button type="submit">{{create_default_label}}</button>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<form method="post" action="/prompts?lang={{language_url}}&mode=defaults"
|
||||||
|
onsubmit="return confirm({{{confirm_json}}});">
|
||||||
|
<input type="hidden" name="action" value="delete_default">
|
||||||
|
<input type="hidden" name="default_id" value="{{id}}">
|
||||||
|
<button type="submit">{{delete_label}}</button>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<div class="prompt-list-item default-prompt-item">
|
||||||
|
<button type="button"
|
||||||
|
class="prompt-select js-open-prompt"
|
||||||
|
data-kind="default"
|
||||||
|
data-id="{{id}}">
|
||||||
|
<span class="prompt-name">{{name}}</span>
|
||||||
|
<span class="prompt-subline">{{{metadata}}}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<form method="post" action="/prompts?lang={{language_url}}&mode=personal">
|
||||||
|
<input type="hidden" name="action" value="copy_default">
|
||||||
|
<input type="hidden" name="default_id" value="{{id}}">
|
||||||
|
<button type="submit">{{copy_label}}</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{{admin_delete_html}}}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<div class="prompt-list-item">
|
||||||
|
<button type="button"
|
||||||
|
class="prompt-select js-open-prompt"
|
||||||
|
data-kind="personal"
|
||||||
|
data-id="{{id}}">
|
||||||
|
<span class="prompt-name">{{name}}</span>
|
||||||
|
<span class="prompt-subline">{{metadata}}</span>
|
||||||
|
</button>
|
||||||
|
<form method="post" action="/prompts?lang={{language_url}}&mode=personal"
|
||||||
|
onsubmit="return confirm({{{confirm_json}}});">
|
||||||
|
<input type="hidden" name="action" value="delete_prompt">
|
||||||
|
<input type="hidden" name="prompt_id" value="{{id}}">
|
||||||
|
<button type="submit">{{delete_label}}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<tr>
|
||||||
|
<td>{{index}}</td>
|
||||||
|
<td>{{number}}</td>
|
||||||
|
<td>{{size}}</td>
|
||||||
|
<td><a href="{{url}}">{{url}}</a></td>
|
||||||
|
</tr>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<option value="{{value}}"{{{selected}}}>{{label}}</option>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<form method="post" action="/users?lang={{language_url}}"
|
||||||
|
onsubmit="return confirm({{{confirm_json}}});">
|
||||||
|
<input type="hidden" name="action" value="delete_user">
|
||||||
|
<input type="hidden" name="user_id" value="{{user_id}}">
|
||||||
|
<button type="submit">{{delete_user_label}}</button>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<tr>
|
||||||
|
<td colspan="7">
|
||||||
|
<form method="post" action="/users?lang={{language_url}}" class="user-row-form">
|
||||||
|
<input type="hidden" name="action" value="update_user">
|
||||||
|
<input type="hidden" name="user_id" value="{{user_id}}">
|
||||||
|
|
||||||
|
<label>{{full_name_label}}<br><input type="text" name="full_name" value="{{full_name}}" required></label>
|
||||||
|
<label>{{email_label}}<br><input type="email" name="email" value="{{email}}" required></label>
|
||||||
|
<label><input type="checkbox" name="is_admin" value="1"{{{is_admin_checked}}}> {{admin_label}}</label>
|
||||||
|
<label><input type="checkbox" name="is_enabled" value="1"{{{is_enabled_checked}}}> {{enabled_label}}</label>
|
||||||
|
<span>{{created_at}}</span>
|
||||||
|
<span>{{last_login_at}}</span>
|
||||||
|
<button type="submit">{{update_user_label}}</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="user-row-actions">
|
||||||
|
<form method="post" action="/users?lang={{language_url}}">
|
||||||
|
<input type="hidden" name="action" value="set_password">
|
||||||
|
<input type="hidden" name="email" value="{{email}}">
|
||||||
|
<label>{{new_password_label}}<br><input type="password" name="password" autocomplete="new-password"></label>
|
||||||
|
<button type="submit">{{change_password_label}}</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{{delete_html}}}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<p class="small">{{cannot_delete_self}}</p>
|
||||||
@@ -0,0 +1,462 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="{{language}}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{title}}</title>
|
||||||
|
<link rel="stylesheet" href="/css/styles.css?v={{style_version}}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
{{{header_html}}}
|
||||||
|
|
||||||
|
<main class="page-main prompt-workbench">
|
||||||
|
|
||||||
|
<aside class="prompt-sidebar panel">
|
||||||
|
<div class="prompt-tabs" role="tablist" aria-label="Prompt lists">
|
||||||
|
<button type="button" class="prompt-tab {{defaults_tab_active_class}}" data-tab="defaults" role="tab" aria-selected="{{defaults_tab_selected}}">
|
||||||
|
{{available_defaults_label}}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="prompt-tab {{personal_tab_active_class}}" data-tab="personal" role="tab" aria-selected="{{personal_tab_selected}}">
|
||||||
|
{{your_prompts_label}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="prompt-tab-panel {{defaults_tab_active_class}}" id="tab-defaults" role="tabpanel">
|
||||||
|
{{{default_admin_notice_html}}}
|
||||||
|
|
||||||
|
<div class="sidebar-actions">
|
||||||
|
<form method="post" action="/prompts?lang={{language_url}}&mode=personal">
|
||||||
|
<input type="hidden" name="action" value="copy_all_defaults">
|
||||||
|
<input type="hidden" name="language" value="{{language}}">
|
||||||
|
<button type="submit">{{copy_all_label}}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="prompt-list">
|
||||||
|
{{{default_prompts_html}}}
|
||||||
|
{{{no_defaults_html}}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{{create_default_html}}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="prompt-tab-panel {{personal_tab_active_class}}" id="tab-personal" role="tabpanel">
|
||||||
|
<div class="prompt-list">
|
||||||
|
{{{personal_prompts_html}}}
|
||||||
|
{{{no_personal_html}}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details class="create-drawer">
|
||||||
|
<summary>{{create_personal_label}}</summary>
|
||||||
|
<form method="post" action="/prompts?lang={{language_url}}&mode=personal">
|
||||||
|
<input type="hidden" name="action" value="create_prompt">
|
||||||
|
<label>{{name_label}}<br><input type="text" name="name"></label>
|
||||||
|
<label>{{language_label}}<br>
|
||||||
|
<select name="language">
|
||||||
|
{{{language_options_html}}}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>{{prompt_content_label}}<br><textarea name="content" rows="7"></textarea></label>
|
||||||
|
<button type="submit">{{create_personal_label}}</button>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section id="promptViewer" class="prompt-viewer panel is-empty">
|
||||||
|
<div class="viewer-empty">
|
||||||
|
{{select_prompt_label}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="viewer-shell">
|
||||||
|
<div class="viewer-header">
|
||||||
|
<div>
|
||||||
|
<div class="viewer-title" id="viewerTitle">{{prompt_label}}</div>
|
||||||
|
<div class="version-meta" id="viewerMeta"></div>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="editPromptButton">{{edit_label}}</button>
|
||||||
|
</div>
|
||||||
|
<pre id="viewerContent" class="viewer-content"></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="page-footer">
|
||||||
|
<a href="/">{{back_label}}</a>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="promptModalBackdrop" class="modal-backdrop">
|
||||||
|
<div class="prompt-modal">
|
||||||
|
|
||||||
|
<form id="promptModalForm" class="edit-mode" method="post" action="/prompts?lang={{language_url}}&mode={{mode_url}}">
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<div>
|
||||||
|
<div class="modal-title" id="modalTitle">{{prompt_label}}</div>
|
||||||
|
<div class="version-meta" id="modalMeta"></div>
|
||||||
|
</div>
|
||||||
|
<button type="button" id="cancelEditButton">{{close_label}}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-toolbar">
|
||||||
|
<button type="button" id="versionNewerButton">{{newer_previous_label}}</button>
|
||||||
|
<button type="button" id="versionOlderButton">{{older_previous_label}}</button>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
{{diff_view_label}}
|
||||||
|
<select id="diffMode">
|
||||||
|
<option value="plain">{{diff_plain_label}}</option>
|
||||||
|
<option value="all" selected>{{diff_all_label}}</option>
|
||||||
|
<option value="same">{{diff_same_label}}</option>
|
||||||
|
<option value="added">{{diff_added_label}}</option>
|
||||||
|
<option value="deleted">{{diff_deleted_label}}</option>
|
||||||
|
<option value="changed">{{diff_changed_label}}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<span id="versionIndicator" class="version-meta"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<div class="edit-pane">
|
||||||
|
<div class="modal-form-grid">
|
||||||
|
<label>
|
||||||
|
{{name_label}}<br>
|
||||||
|
<input type="text" id="modalName" name="name">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
{{language_label}}<br>
|
||||||
|
<select id="modalLanguage" name="language">
|
||||||
|
{{{language_options_plain_html}}}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="defaultKeyRow" class="default-key-row">
|
||||||
|
<label>
|
||||||
|
{{default_key_label}}<br>
|
||||||
|
<input type="text" id="modalDefaultKey" name="default_key">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="content-label">
|
||||||
|
<span>{{prompt_content_label}}</span>
|
||||||
|
<textarea id="modalContent" name="content"></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="version-pane">
|
||||||
|
<div>
|
||||||
|
<strong>{{previous_version_label}}</strong>
|
||||||
|
<div id="selectedVersionMeta" class="version-meta"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="version-content" id="versionContent"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<input type="hidden" id="modalAction" name="action" value="">
|
||||||
|
<input type="hidden" id="modalPromptId" name="prompt_id" value="">
|
||||||
|
<input type="hidden" id="modalDefaultId" name="default_id" value="">
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="create_version" value="1" checked>
|
||||||
|
{{store_version_label}}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
{{version_note_label}}
|
||||||
|
<input type="text" name="version_note" value="editor edit">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button type="submit">{{save_label}}</button>
|
||||||
|
<button type="button" id="snapshotButton">{{store_snapshot_label}}</button>
|
||||||
|
<button type="button" id="restoreVersionButton">{{restore_version_label}}</button>
|
||||||
|
<button type="button" id="deleteVersionButton">{{delete_version_label}}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="modalAuxForm" method="post" action="/prompts?lang={{language_url}}&mode={{mode_url}}" class="hidden">
|
||||||
|
<input type="hidden" id="auxAction" name="action" value="">
|
||||||
|
<input type="hidden" id="auxPromptId" name="prompt_id" value="">
|
||||||
|
<input type="hidden" id="auxDefaultId" name="default_id" value="">
|
||||||
|
<input type="hidden" id="auxVersionNo" name="version_no" value="">
|
||||||
|
<input type="hidden" id="auxVersionNote" name="version_note" value="">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script type="application/json" id="promptDataJson">
|
||||||
|
{{{prompt_data_json}}}
|
||||||
|
</script>
|
||||||
|
<script type="application/json" id="promptTextJson">
|
||||||
|
{{{prompt_text_json}}}
|
||||||
|
</script>
|
||||||
|
<script src="/js/prompt-editor.js?v={{prompt_editor_version}}" defer></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
===
|
||||||
|
{
|
||||||
|
"translations": {
|
||||||
|
"prompts.title": {
|
||||||
|
"en": "Prompt administration",
|
||||||
|
"nl": "Promptbeheer"
|
||||||
|
},
|
||||||
|
"prompts.back": {
|
||||||
|
"en": "Back to Racket sandbox",
|
||||||
|
"nl": "Terug naar Racket sandbox"
|
||||||
|
},
|
||||||
|
"prompts.your_prompts": {
|
||||||
|
"en": "Your prompts",
|
||||||
|
"nl": "Jouw prompts"
|
||||||
|
},
|
||||||
|
"prompts.default_admin": {
|
||||||
|
"en": "Default prompt administration",
|
||||||
|
"nl": "Standaardpromptbeheer"
|
||||||
|
},
|
||||||
|
"prompts.language": {
|
||||||
|
"en": "Language",
|
||||||
|
"nl": "Taal"
|
||||||
|
},
|
||||||
|
"prompts.available_defaults": {
|
||||||
|
"en": "Available default prompts",
|
||||||
|
"nl": "Beschikbare standaardprompts"
|
||||||
|
},
|
||||||
|
"prompts.default_admin_badge": {
|
||||||
|
"en": "Admin default prompts",
|
||||||
|
"nl": "Admin standaardprompts"
|
||||||
|
},
|
||||||
|
"prompts.default_admin_hint": {
|
||||||
|
"en": "Defaults are shared with every user and can be copied into personal prompts.",
|
||||||
|
"nl": "Standaarden worden gedeeld met alle gebruikers en kunnen naar persoonlijke prompts worden gekopieerd."
|
||||||
|
},
|
||||||
|
"prompts.copy_all": {
|
||||||
|
"en": "Copy all",
|
||||||
|
"nl": "Alles kopieren"
|
||||||
|
},
|
||||||
|
"prompts.copy": {
|
||||||
|
"en": "copy",
|
||||||
|
"nl": "kopieer"
|
||||||
|
},
|
||||||
|
"prompts.delete": {
|
||||||
|
"en": "delete",
|
||||||
|
"nl": "verwijder"
|
||||||
|
},
|
||||||
|
"prompts.create_default": {
|
||||||
|
"en": "Create default prompt",
|
||||||
|
"nl": "Standaardprompt maken"
|
||||||
|
},
|
||||||
|
"prompts.create_personal": {
|
||||||
|
"en": "Create personal prompt",
|
||||||
|
"nl": "Persoonlijke prompt maken"
|
||||||
|
},
|
||||||
|
"prompts.name": {
|
||||||
|
"en": "Name",
|
||||||
|
"nl": "Naam"
|
||||||
|
},
|
||||||
|
"prompts.default_key": {
|
||||||
|
"en": "Default key",
|
||||||
|
"nl": "Standaardsleutel"
|
||||||
|
},
|
||||||
|
"prompts.prompt_content": {
|
||||||
|
"en": "Prompt content",
|
||||||
|
"nl": "Promptinhoud"
|
||||||
|
},
|
||||||
|
"prompts.no_defaults": {
|
||||||
|
"en": "No default prompts for this language yet.",
|
||||||
|
"nl": "Nog geen standaardprompts voor deze taal."
|
||||||
|
},
|
||||||
|
"prompts.no_personal": {
|
||||||
|
"en": "No personal prompts yet for this language.",
|
||||||
|
"nl": "Nog geen persoonlijke prompts voor deze taal."
|
||||||
|
},
|
||||||
|
"prompts.select_prompt": {
|
||||||
|
"en": "Select a prompt on the left to view it.",
|
||||||
|
"nl": "Selecteer links een prompt om deze te bekijken."
|
||||||
|
},
|
||||||
|
"prompts.edit": {
|
||||||
|
"en": "Edit",
|
||||||
|
"nl": "Bewerk"
|
||||||
|
},
|
||||||
|
"prompts.close": {
|
||||||
|
"en": "Close",
|
||||||
|
"nl": "Sluiten"
|
||||||
|
},
|
||||||
|
"prompts.newer_previous": {
|
||||||
|
"en": "newer previous version",
|
||||||
|
"nl": "nieuwere vorige versie"
|
||||||
|
},
|
||||||
|
"prompts.older_previous": {
|
||||||
|
"en": "older previous version",
|
||||||
|
"nl": "oudere vorige versie"
|
||||||
|
},
|
||||||
|
"prompts.diff_view": {
|
||||||
|
"en": "Diff view:",
|
||||||
|
"nl": "Verschilweergave:"
|
||||||
|
},
|
||||||
|
"prompts.diff_plain": {
|
||||||
|
"en": "text, no diff",
|
||||||
|
"nl": "tekst, geen verschil"
|
||||||
|
},
|
||||||
|
"prompts.diff_all": {
|
||||||
|
"en": "all diff",
|
||||||
|
"nl": "alle verschillen"
|
||||||
|
},
|
||||||
|
"prompts.diff_same": {
|
||||||
|
"en": "unchanged only",
|
||||||
|
"nl": "alleen ongewijzigd"
|
||||||
|
},
|
||||||
|
"prompts.diff_added": {
|
||||||
|
"en": "additions only",
|
||||||
|
"nl": "alleen toevoegingen"
|
||||||
|
},
|
||||||
|
"prompts.diff_deleted": {
|
||||||
|
"en": "deletions only",
|
||||||
|
"nl": "alleen verwijderingen"
|
||||||
|
},
|
||||||
|
"prompts.diff_changed": {
|
||||||
|
"en": "changes only",
|
||||||
|
"nl": "alleen wijzigingen"
|
||||||
|
},
|
||||||
|
"prompts.previous_version": {
|
||||||
|
"en": "Previous version",
|
||||||
|
"nl": "Vorige versie"
|
||||||
|
},
|
||||||
|
"prompts.store_version": {
|
||||||
|
"en": "store this edit as a new version",
|
||||||
|
"nl": "bewaar deze bewerking als nieuwe versie"
|
||||||
|
},
|
||||||
|
"prompts.version_note": {
|
||||||
|
"en": "Version note:",
|
||||||
|
"nl": "Versienotitie:"
|
||||||
|
},
|
||||||
|
"prompts.save": {
|
||||||
|
"en": "Save",
|
||||||
|
"nl": "Opslaan"
|
||||||
|
},
|
||||||
|
"prompts.store_snapshot": {
|
||||||
|
"en": "Store snapshot",
|
||||||
|
"nl": "Snapshot bewaren"
|
||||||
|
},
|
||||||
|
"prompts.restore_version": {
|
||||||
|
"en": "Restore selected version",
|
||||||
|
"nl": "Geselecteerde versie herstellen"
|
||||||
|
},
|
||||||
|
"prompts.delete_version": {
|
||||||
|
"en": "Delete selected version",
|
||||||
|
"nl": "Geselecteerde versie verwijderen"
|
||||||
|
},
|
||||||
|
"prompts.prompt": {
|
||||||
|
"en": "Prompt",
|
||||||
|
"nl": "Prompt"
|
||||||
|
},
|
||||||
|
"prompts.prompt_not_found": {
|
||||||
|
"en": "Prompt not found",
|
||||||
|
"nl": "Prompt niet gevonden"
|
||||||
|
},
|
||||||
|
"prompts.no_previous_versions": {
|
||||||
|
"en": "No previous versions stored.",
|
||||||
|
"nl": "Geen vorige versies opgeslagen."
|
||||||
|
},
|
||||||
|
"prompts.no_lines_for_view": {
|
||||||
|
"en": "No lines for this view.",
|
||||||
|
"nl": "Geen regels voor deze weergave."
|
||||||
|
},
|
||||||
|
"prompts.restore_version_confirm": {
|
||||||
|
"en": "Restore version",
|
||||||
|
"nl": "Versie herstellen"
|
||||||
|
},
|
||||||
|
"prompts.delete_version_confirm": {
|
||||||
|
"en": "Delete version",
|
||||||
|
"nl": "Versie verwijderen"
|
||||||
|
},
|
||||||
|
"prompts.delete_default_confirm": {
|
||||||
|
"en": "Delete default prompt {{name}}?",
|
||||||
|
"nl": "Standaardprompt {{name}} verwijderen?"
|
||||||
|
},
|
||||||
|
"prompts.delete_prompt_confirm": {
|
||||||
|
"en": "Delete prompt {{name}}?",
|
||||||
|
"nl": "Prompt {{name}} verwijderen?"
|
||||||
|
},
|
||||||
|
"prompts.default_metadata": {
|
||||||
|
"en": "{{default_key}} · {{updated_at}}",
|
||||||
|
"nl": "{{default_key}} · {{updated_at}}"
|
||||||
|
},
|
||||||
|
"prompts.personal_metadata": {
|
||||||
|
"en": "{{language}} · {{updated_at}}",
|
||||||
|
"nl": "{{language}} · {{updated_at}}"
|
||||||
|
},
|
||||||
|
"prompts.default_prompt_prefix": {
|
||||||
|
"en": "Default prompt: ",
|
||||||
|
"nl": "Standaardprompt: "
|
||||||
|
},
|
||||||
|
"prompts.prompt_prefix": {
|
||||||
|
"en": "Prompt: ",
|
||||||
|
"nl": "Prompt: "
|
||||||
|
},
|
||||||
|
"prompts.created": {
|
||||||
|
"en": "created",
|
||||||
|
"nl": "gemaakt"
|
||||||
|
},
|
||||||
|
"prompts.updated": {
|
||||||
|
"en": "updated",
|
||||||
|
"nl": "bijgewerkt"
|
||||||
|
},
|
||||||
|
"prompts.default_prompt": {
|
||||||
|
"en": "default prompt",
|
||||||
|
"nl": "standaardprompt"
|
||||||
|
},
|
||||||
|
"prompts.version": {
|
||||||
|
"en": "version",
|
||||||
|
"nl": "versie"
|
||||||
|
},
|
||||||
|
"prompts.showing_version": {
|
||||||
|
"en": "showing version",
|
||||||
|
"nl": "toont versie"
|
||||||
|
},
|
||||||
|
"prompts.of": {
|
||||||
|
"en": "of",
|
||||||
|
"nl": "van"
|
||||||
|
},
|
||||||
|
"prompts.old": {
|
||||||
|
"en": "old",
|
||||||
|
"nl": "oud"
|
||||||
|
},
|
||||||
|
"prompts.new": {
|
||||||
|
"en": "new",
|
||||||
|
"nl": "nieuw"
|
||||||
|
},
|
||||||
|
"prompts.unknown_action": {
|
||||||
|
"en": "Unknown action: {{action}}",
|
||||||
|
"nl": "Onbekende actie: {{action}}"
|
||||||
|
},
|
||||||
|
"app.admin": {
|
||||||
|
"en": "admin",
|
||||||
|
"nl": "admin"
|
||||||
|
},
|
||||||
|
"app.user_management": {
|
||||||
|
"en": "User management",
|
||||||
|
"nl": "Gebruikersbeheer"
|
||||||
|
},
|
||||||
|
"app.configuration": {
|
||||||
|
"en": "Configuration",
|
||||||
|
"nl": "Configuratie"
|
||||||
|
},
|
||||||
|
"app.logout": {
|
||||||
|
"en": "Logout",
|
||||||
|
"nl": "Uitloggen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{title}}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{title}}</h1>
|
||||||
|
<pre>{{message}}</pre>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="nl">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Racket package index</title>
|
||||||
|
<link rel="stylesheet" href="/css/styles.css">
|
||||||
|
</head>
|
||||||
|
<body class="simple-doc package-index-page">
|
||||||
|
|
||||||
|
<h1>Racket package index</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Volledige HTML-index van de Racket package catalogus op basis van
|
||||||
|
<code>pkgs-all</code>. De package-naam is de ophaallink via
|
||||||
|
<code>rktsndbx.dijkewijk.nl</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Aantal packages: <code>{{package_count}}</code><br>
|
||||||
|
next-id voor alle package-links op deze pagina:
|
||||||
|
<code>{{next_id}}</code>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>package</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{{package_rows_html}}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="{{language}}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{title}}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{title}}</h1>
|
||||||
|
<p>{{message}}</p>
|
||||||
|
{{{extra_html}}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="{{language}}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{title}}</title>
|
||||||
|
<link rel="stylesheet" href="/css/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
{{{header_html}}}
|
||||||
|
|
||||||
|
<main class="page-main dashboard-main">
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>{{create_user_label}}</h2>
|
||||||
|
|
||||||
|
<form method="post" action="/users?lang={{language_url}}" class="admin-form-grid">
|
||||||
|
<input type="hidden" name="action" value="create_user">
|
||||||
|
<label>{{full_name_label}}<br><input type="text" name="full_name" required></label>
|
||||||
|
<label>{{email_label}}<br><input type="email" name="email" required></label>
|
||||||
|
<label>{{password_label}}<br><input type="password" name="password" autocomplete="new-password" required></label>
|
||||||
|
<label><input type="checkbox" name="is_admin" value="1"> {{admin_label}}</label>
|
||||||
|
<label><input type="checkbox" name="is_enabled" value="1" checked> {{enabled_label}}</label>
|
||||||
|
<button type="submit">{{create_user_label}}</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>{{user_management_label}}</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{full_name_label}}</th>
|
||||||
|
<th>{{email_label}}</th>
|
||||||
|
<th>{{admin_label}}</th>
|
||||||
|
<th>{{enabled_label}}</th>
|
||||||
|
<th>{{created_label}}</th>
|
||||||
|
<th>{{last_login_label}}</th>
|
||||||
|
<th>{{actions_label}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{{user_rows_html}}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
===
|
||||||
|
{
|
||||||
|
"translations": {
|
||||||
|
"app.title": {
|
||||||
|
"en": "Racket sandbox",
|
||||||
|
"nl": "Racket sandbox"
|
||||||
|
},
|
||||||
|
"app.manage_prompts": {
|
||||||
|
"en": "Manage prompts",
|
||||||
|
"nl": "Prompts beheren"
|
||||||
|
},
|
||||||
|
"app.user_management": {
|
||||||
|
"en": "User management",
|
||||||
|
"nl": "Gebruikersbeheer"
|
||||||
|
},
|
||||||
|
"app.logout": {
|
||||||
|
"en": "Logout",
|
||||||
|
"nl": "Uitloggen"
|
||||||
|
},
|
||||||
|
"app.language": {
|
||||||
|
"en": "Language",
|
||||||
|
"nl": "Taal"
|
||||||
|
},
|
||||||
|
"app.logged_in_as": {
|
||||||
|
"en": "Logged in as:",
|
||||||
|
"nl": "Ingelogd als:"
|
||||||
|
},
|
||||||
|
"app.admin": {
|
||||||
|
"en": "Admin",
|
||||||
|
"nl": "Admin"
|
||||||
|
},
|
||||||
|
"app.enabled": {
|
||||||
|
"en": "Enabled",
|
||||||
|
"nl": "Ingeschakeld"
|
||||||
|
},
|
||||||
|
"app.full_name": {
|
||||||
|
"en": "Full name",
|
||||||
|
"nl": "Volledige naam"
|
||||||
|
},
|
||||||
|
"app.email": {
|
||||||
|
"en": "Email",
|
||||||
|
"nl": "E-mail"
|
||||||
|
},
|
||||||
|
"app.password": {
|
||||||
|
"en": "Password",
|
||||||
|
"nl": "Wachtwoord"
|
||||||
|
},
|
||||||
|
"app.new_password": {
|
||||||
|
"en": "New password",
|
||||||
|
"nl": "Nieuw wachtwoord"
|
||||||
|
},
|
||||||
|
"app.created": {
|
||||||
|
"en": "Created",
|
||||||
|
"nl": "Gemaakt"
|
||||||
|
},
|
||||||
|
"app.last_login": {
|
||||||
|
"en": "Last login",
|
||||||
|
"nl": "Laatste login"
|
||||||
|
},
|
||||||
|
"app.actions": {
|
||||||
|
"en": "Actions",
|
||||||
|
"nl": "Acties"
|
||||||
|
},
|
||||||
|
"app.create_user": {
|
||||||
|
"en": "Create user",
|
||||||
|
"nl": "Gebruiker aanmaken"
|
||||||
|
},
|
||||||
|
"app.update_user": {
|
||||||
|
"en": "Update user",
|
||||||
|
"nl": "Gebruiker aanpassen"
|
||||||
|
},
|
||||||
|
"app.change_password": {
|
||||||
|
"en": "Change password",
|
||||||
|
"nl": "Wachtwoord wijzigen"
|
||||||
|
},
|
||||||
|
"app.delete_user": {
|
||||||
|
"en": "Delete user",
|
||||||
|
"nl": "Gebruiker verwijderen"
|
||||||
|
},
|
||||||
|
"app.delete_user_confirm": {
|
||||||
|
"en": "Delete user {{email}}?",
|
||||||
|
"nl": "Gebruiker {{email}} verwijderen?"
|
||||||
|
},
|
||||||
|
"app.cannot_delete_self": {
|
||||||
|
"en": "You cannot delete your own account.",
|
||||||
|
"nl": "Je kunt je eigen account niet verwijderen."
|
||||||
|
},
|
||||||
|
"app.cannot_disable_self": {
|
||||||
|
"en": "You cannot disable your own account.",
|
||||||
|
"nl": "Je kunt je eigen account niet uitschakelen."
|
||||||
|
},
|
||||||
|
"app.cannot_remove_own_admin": {
|
||||||
|
"en": "You cannot remove your own admin rights.",
|
||||||
|
"nl": "Je kunt je eigen adminrechten niet verwijderen."
|
||||||
|
},
|
||||||
|
"app.user_created": {
|
||||||
|
"en": "User created.",
|
||||||
|
"nl": "Gebruiker aangemaakt."
|
||||||
|
},
|
||||||
|
"app.user_updated": {
|
||||||
|
"en": "User updated.",
|
||||||
|
"nl": "Gebruiker aangepast."
|
||||||
|
},
|
||||||
|
"app.password_changed": {
|
||||||
|
"en": "Password changed.",
|
||||||
|
"nl": "Wachtwoord gewijzigd."
|
||||||
|
},
|
||||||
|
"app.user_deleted": {
|
||||||
|
"en": "User deleted.",
|
||||||
|
"nl": "Gebruiker verwijderd."
|
||||||
|
},
|
||||||
|
"app.back_to_sandbox": {
|
||||||
|
"en": "Back to Racket sandbox",
|
||||||
|
"nl": "Terug naar Racket sandbox"
|
||||||
|
},
|
||||||
|
"app.configuration": {
|
||||||
|
"en": "Configuration",
|
||||||
|
"nl": "Configuratie"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+174
-369
@@ -18,11 +18,9 @@ require_once __DIR__ . '/private/header.php';
|
|||||||
require_once __DIR__ . '/private/languagestore.php';
|
require_once __DIR__ . '/private/languagestore.php';
|
||||||
require_once __DIR__ . '/private/promptstore.php';
|
require_once __DIR__ . '/private/promptstore.php';
|
||||||
require_once __DIR__ . '/private/usersettings.php';
|
require_once __DIR__ . '/private/usersettings.php';
|
||||||
|
require_once __DIR__ . '/private/viewdata.php';
|
||||||
|
|
||||||
ini_set('display_errors', '1');
|
require_once __DIR__ . '/config/reporting.php';
|
||||||
ini_set('display_startup_errors', '1');
|
|
||||||
ini_set('log_errors', '1');
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
$DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite';
|
$DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite';
|
||||||
|
|
||||||
@@ -41,11 +39,11 @@ function h($s)
|
|||||||
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
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;
|
global $languageStore, $language;
|
||||||
|
|
||||||
return $languageStore->translate($key, $language, $fallback);
|
return $languageStore->translateFormat($key, $language, $values, $fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function post_value($name, $default = '')
|
function post_value($name, $default = '')
|
||||||
@@ -116,70 +114,7 @@ $language = resolve_user_language(
|
|||||||
$store->supportedLanguages()
|
$store->supportedLanguages()
|
||||||
);
|
);
|
||||||
|
|
||||||
$languageStore->seedDefaults(array(
|
seed_template_translations($languageStore, 'prompts.html');
|
||||||
'prompts.title' => array('en' => 'Prompt administration', 'nl' => 'Promptbeheer'),
|
|
||||||
'prompts.back' => array('en' => 'Back to Racket sandbox', 'nl' => 'Terug naar Racket sandbox'),
|
|
||||||
'prompts.your_prompts' => array('en' => 'Your prompts', 'nl' => 'Jouw prompts'),
|
|
||||||
'prompts.default_admin' => array('en' => 'Default prompt administration', 'nl' => 'Standaardpromptbeheer'),
|
|
||||||
'prompts.language' => array('en' => 'Language', 'nl' => 'Taal'),
|
|
||||||
'prompts.available_defaults' => array('en' => 'Available default prompts', 'nl' => 'Beschikbare standaardprompts'),
|
|
||||||
'prompts.default_admin_badge' => array('en' => 'Admin default prompts', 'nl' => 'Admin standaardprompts'),
|
|
||||||
'prompts.default_admin_hint' => array(
|
|
||||||
'en' => 'You are editing global default prompts. Users can copy these to their own prompts.',
|
|
||||||
'nl' => 'Je bewerkt globale standaardprompts. Gebruikers kunnen deze naar hun eigen prompts kopieren.'
|
|
||||||
),
|
|
||||||
'prompts.copy_all' => array('en' => 'Copy all', 'nl' => 'Alles kopieren'),
|
|
||||||
'prompts.copy' => array('en' => 'copy', 'nl' => 'kopieer'),
|
|
||||||
'prompts.delete' => array('en' => 'delete', 'nl' => 'verwijder'),
|
|
||||||
'prompts.create_default' => array('en' => 'Create default prompt', 'nl' => 'Standaardprompt maken'),
|
|
||||||
'prompts.create_personal' => array('en' => 'Create personal prompt', 'nl' => 'Persoonlijke prompt maken'),
|
|
||||||
'prompts.name' => array('en' => 'Name', 'nl' => 'Naam'),
|
|
||||||
'prompts.default_key' => array('en' => 'Default key', 'nl' => 'Standaardsleutel'),
|
|
||||||
'prompts.prompt_content' => array('en' => 'Prompt content', 'nl' => 'Promptinhoud'),
|
|
||||||
'prompts.no_defaults' => array('en' => 'No default prompts for this language yet.', 'nl' => 'Nog geen standaardprompts voor deze taal.'),
|
|
||||||
'prompts.no_personal' => array('en' => 'No personal prompts yet for this language.', 'nl' => 'Nog geen persoonlijke prompts voor deze taal.'),
|
|
||||||
'prompts.select_prompt' => array('en' => 'Select a prompt on the left to view it.', 'nl' => 'Selecteer links een prompt om deze te bekijken.'),
|
|
||||||
'prompts.edit' => array('en' => 'Edit', 'nl' => 'Bewerk'),
|
|
||||||
'prompts.close' => array('en' => 'Close', 'nl' => 'Sluiten'),
|
|
||||||
'prompts.newer_previous' => array('en' => 'newer previous version', 'nl' => 'nieuwere vorige versie'),
|
|
||||||
'prompts.older_previous' => array('en' => 'older previous version', 'nl' => 'oudere vorige versie'),
|
|
||||||
'prompts.diff_view' => array('en' => 'Diff view:', 'nl' => 'Verschilweergave:'),
|
|
||||||
'prompts.diff_plain' => array('en' => 'text, no diff', 'nl' => 'tekst, geen verschil'),
|
|
||||||
'prompts.diff_all' => array('en' => 'all diff', 'nl' => 'alle verschillen'),
|
|
||||||
'prompts.diff_same' => array('en' => 'unchanged only', 'nl' => 'alleen ongewijzigd'),
|
|
||||||
'prompts.diff_added' => array('en' => 'additions only', 'nl' => 'alleen toevoegingen'),
|
|
||||||
'prompts.diff_deleted' => array('en' => 'deletions only', 'nl' => 'alleen verwijderingen'),
|
|
||||||
'prompts.diff_changed' => array('en' => 'changes only', 'nl' => 'alleen wijzigingen'),
|
|
||||||
'prompts.previous_version' => array('en' => 'Previous version', 'nl' => 'Vorige versie'),
|
|
||||||
'prompts.store_version' => array('en' => 'store this edit as a new version', 'nl' => 'bewaar deze bewerking als nieuwe versie'),
|
|
||||||
'prompts.version_note' => array('en' => 'Version note:', 'nl' => 'Versienotitie:'),
|
|
||||||
'prompts.save' => array('en' => 'Save', 'nl' => 'Opslaan'),
|
|
||||||
'prompts.store_snapshot' => array('en' => 'Store snapshot', 'nl' => 'Snapshot bewaren'),
|
|
||||||
'prompts.restore_version' => array('en' => 'Restore selected version', 'nl' => 'Geselecteerde versie herstellen'),
|
|
||||||
'prompts.delete_version' => array('en' => 'Delete selected version', 'nl' => 'Geselecteerde versie verwijderen'),
|
|
||||||
'prompts.prompt' => array('en' => 'Prompt', 'nl' => 'Prompt'),
|
|
||||||
'prompts.prompt_not_found' => array('en' => 'Prompt not found', 'nl' => 'Prompt niet gevonden'),
|
|
||||||
'prompts.no_previous_versions' => array('en' => 'No previous versions stored.', 'nl' => 'Geen vorige versies opgeslagen.'),
|
|
||||||
'prompts.no_lines_for_view' => array('en' => 'No lines for this view.', 'nl' => 'Geen regels voor deze weergave.'),
|
|
||||||
'prompts.restore_version_confirm' => array('en' => 'Restore version', 'nl' => 'Versie herstellen'),
|
|
||||||
'prompts.delete_version_confirm' => array('en' => 'Delete version', 'nl' => 'Versie verwijderen'),
|
|
||||||
'prompts.delete_default_confirm' => array('en' => 'Delete default prompt', 'nl' => 'Standaardprompt verwijderen'),
|
|
||||||
'prompts.delete_prompt_confirm' => array('en' => 'Delete prompt', 'nl' => 'Prompt verwijderen'),
|
|
||||||
'prompts.default_prompt_prefix' => array('en' => 'Default prompt: ', 'nl' => 'Standaardprompt: '),
|
|
||||||
'prompts.prompt_prefix' => array('en' => 'Prompt: ', 'nl' => 'Prompt: '),
|
|
||||||
'prompts.created' => array('en' => 'created', 'nl' => 'gemaakt'),
|
|
||||||
'prompts.updated' => array('en' => 'updated', 'nl' => 'bijgewerkt'),
|
|
||||||
'prompts.default_prompt' => array('en' => 'default prompt', 'nl' => 'standaardprompt'),
|
|
||||||
'prompts.version' => array('en' => 'version', 'nl' => 'versie'),
|
|
||||||
'prompts.showing_version' => array('en' => 'showing version', 'nl' => 'toont versie'),
|
|
||||||
'prompts.of' => array('en' => 'of', 'nl' => 'van'),
|
|
||||||
'prompts.old' => array('en' => 'old', 'nl' => 'oud'),
|
|
||||||
'prompts.new' => array('en' => 'new', 'nl' => 'nieuw'),
|
|
||||||
'app.admin' => array('en' => 'admin', 'nl' => 'admin'),
|
|
||||||
'app.user_management' => array('en' => 'User management', 'nl' => 'Gebruikersbeheer'),
|
|
||||||
'app.configuration' => array('en' => 'Configuration', 'nl' => 'Configuratie'),
|
|
||||||
'app.logout' => array('en' => 'Logout', 'nl' => 'Uitloggen'),
|
|
||||||
));
|
|
||||||
|
|
||||||
$mode = get_value('mode', 'personal');
|
$mode = get_value('mode', 'personal');
|
||||||
|
|
||||||
@@ -326,7 +261,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$message = 'Default prompt deleted.';
|
$message = 'Default prompt deleted.';
|
||||||
}
|
}
|
||||||
} elseif ($action !== '') {
|
} elseif ($action !== '') {
|
||||||
throw new Exception('Unknown action: ' . $action);
|
throw new Exception(t('prompts.unknown_action', 'Unknown action: {{action}}', array('action' => $action)));
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$error = $e->getMessage();
|
$error = $e->getMessage();
|
||||||
@@ -394,309 +329,115 @@ if ($user->isAdmin()) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$styleVersion = @filemtime(__DIR__ . '/styles.css') ?: time();
|
$styleVersion = @filemtime(__DIR__ . '/css/styles.css') ?: time();
|
||||||
$promptEditorVersion = @filemtime(__DIR__ . '/js/prompt-editor.js') ?: time();
|
$promptEditorVersion = @filemtime(__DIR__ . '/js/prompt-editor.js') ?: time();
|
||||||
|
|
||||||
header('Content-Type: text/html; charset=utf-8');
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="<?= h($language) ?>">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title><?= h(t('prompts.title', 'Prompt administration')) ?></title>
|
|
||||||
<link rel="stylesheet" href="/styles.css?v=<?= h($styleVersion) ?>">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="page">
|
$languageOptionsHtml = '';
|
||||||
|
$languageOptionsPlainHtml = '';
|
||||||
|
|
||||||
<?php
|
foreach ($store->supportedLanguages() as $lang) {
|
||||||
render_app_header(array(
|
$languageOptionsHtml .= RacketSandboxTemplate::renderFile('partials/select-option.html', array(
|
||||||
'title' => t('prompts.title', 'Prompt administration'),
|
'value' => $lang,
|
||||||
'nav_items' => $headerNavItems,
|
'selected' => $lang === $language ? ' selected' : '',
|
||||||
'user' => $user,
|
'label' => $store->languageLabel($lang),
|
||||||
'admin_label' => t('app.admin', 'admin'),
|
)) . "\n";
|
||||||
|
|
||||||
|
$languageOptionsPlainHtml .= RacketSandboxTemplate::renderFile('partials/select-option.html', array(
|
||||||
|
'value' => $lang,
|
||||||
|
'selected' => '',
|
||||||
|
'label' => $store->languageLabel($lang),
|
||||||
|
)) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultAdminNoticeHtml = '';
|
||||||
|
|
||||||
|
if ($user->isAdmin()) {
|
||||||
|
$defaultAdminNoticeHtml = RacketSandboxTemplate::renderFile('partials/prompt-admin-notice.html', array(
|
||||||
|
'badge' => t('prompts.default_admin_badge', 'Admin default prompts'),
|
||||||
|
'hint' => t('prompts.default_admin_hint', 'You are editing global default prompts. Users can copy these to their own prompts.'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultPromptsHtml = '';
|
||||||
|
|
||||||
|
foreach ($defaultPrompts as $default) {
|
||||||
|
$adminDeleteHtml = '';
|
||||||
|
|
||||||
|
if ($user->isAdmin()) {
|
||||||
|
$adminDeleteHtml = RacketSandboxTemplate::renderFile('partials/prompt-default-delete.html', array(
|
||||||
|
'language_url' => rawurlencode($language),
|
||||||
|
'confirm_json' => json_encode(t('prompts.delete_default_confirm', 'Delete default prompt {{name}}?', array(
|
||||||
|
'name' => $default->name(),
|
||||||
|
)), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT),
|
||||||
|
'name' => $default->name(),
|
||||||
|
'id' => $default->id(),
|
||||||
|
'delete_label' => t('prompts.delete', 'delete'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultPromptsHtml .= RacketSandboxTemplate::renderFile('partials/prompt-default-item.html', array(
|
||||||
|
'language_url' => rawurlencode($language),
|
||||||
|
'id' => $default->id(),
|
||||||
|
'name' => $default->name(),
|
||||||
|
'default_key' => $default->defaultKey(),
|
||||||
|
'metadata' => t('prompts.default_metadata', '{{default_key}} · {{updated_at}}', array(
|
||||||
|
'default_key' => '<code>' . h($default->defaultKey()) . '</code>',
|
||||||
|
'updated_at' => fmt_time($default->updatedAt()),
|
||||||
|
)),
|
||||||
|
'copy_label' => t('prompts.copy', 'copy'),
|
||||||
|
'admin_delete_html' => $adminDeleteHtml,
|
||||||
|
)) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$noDefaultsHtml = count($defaultPrompts) === 0
|
||||||
|
? RacketSandboxTemplate::renderFile('partials/paragraph.html', array(
|
||||||
|
'class' => 'empty-state',
|
||||||
|
'text' => t('prompts.no_defaults', 'No default prompts for this language yet.'),
|
||||||
|
))
|
||||||
|
: '';
|
||||||
|
|
||||||
|
$createDefaultHtml = '';
|
||||||
|
|
||||||
|
if ($user->isAdmin()) {
|
||||||
|
$createDefaultHtml = RacketSandboxTemplate::renderFile('partials/prompt-create-default.html', array(
|
||||||
|
'language_url' => rawurlencode($language),
|
||||||
|
'create_default_label' => t('prompts.create_default', 'Create default prompt'),
|
||||||
|
'default_key_label' => t('prompts.default_key', 'Default key'),
|
||||||
|
'name_label' => t('prompts.name', 'Name'),
|
||||||
'language_label' => t('prompts.language', 'Language'),
|
'language_label' => t('prompts.language', 'Language'),
|
||||||
'language' => $language,
|
'language_options_html' => $languageOptionsHtml,
|
||||||
'languages' => $headerLanguages,
|
'prompt_content_label' => t('prompts.prompt_content', 'Prompt content'),
|
||||||
'language_action' => '/prompts',
|
));
|
||||||
'language_hidden' => array('mode' => $mode),
|
}
|
||||||
'logout_action' => '/prompts?lang=' . rawurlencode($language) . '&mode=' . rawurlencode($mode),
|
|
||||||
'logout_label' => t('app.logout', 'Logout'),
|
|
||||||
'message' => $message,
|
|
||||||
'error' => $error,
|
|
||||||
));
|
|
||||||
?>
|
|
||||||
|
|
||||||
<main class="page-main prompt-workbench">
|
$personalPromptsHtml = '';
|
||||||
|
|
||||||
<aside class="prompt-sidebar panel">
|
foreach ($personalPrompts as $prompt) {
|
||||||
<div class="prompt-tabs" role="tablist" aria-label="Prompt lists">
|
$personalPromptsHtml .= RacketSandboxTemplate::renderFile('partials/prompt-personal-item.html', array(
|
||||||
<button type="button" class="prompt-tab <?= $mode === 'defaults' ? 'active' : '' ?>" data-tab="defaults" role="tab" aria-selected="<?= $mode === 'defaults' ? 'true' : 'false' ?>">
|
'language_url' => rawurlencode($language),
|
||||||
<?= h(t('prompts.available_defaults', 'Available default prompts')) ?>
|
'id' => $prompt->id(),
|
||||||
</button>
|
'name' => $prompt->name(),
|
||||||
<button type="button" class="prompt-tab <?= $mode !== 'defaults' ? 'active' : '' ?>" data-tab="personal" role="tab" aria-selected="<?= $mode !== 'defaults' ? 'true' : 'false' ?>">
|
'metadata' => t('prompts.personal_metadata', '{{language}} · {{updated_at}}', array(
|
||||||
<?= h(t('prompts.your_prompts', 'Your prompts')) ?>
|
'language' => $store->languageLabel($prompt->language()),
|
||||||
</button>
|
'updated_at' => fmt_time($prompt->updatedAt()),
|
||||||
</div>
|
)),
|
||||||
|
'confirm_json' => json_encode(t('prompts.delete_prompt_confirm', 'Delete prompt {{name}}?', array(
|
||||||
|
'name' => $prompt->name(),
|
||||||
|
)), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT),
|
||||||
|
'delete_label' => t('prompts.delete', 'delete'),
|
||||||
|
)) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
<section class="prompt-tab-panel <?= $mode === 'defaults' ? 'active' : '' ?>" id="tab-defaults" role="tabpanel">
|
$noPersonalHtml = count($personalPrompts) === 0
|
||||||
<?php if ($user->isAdmin()): ?>
|
? RacketSandboxTemplate::renderFile('partials/paragraph.html', array(
|
||||||
<div class="default-admin-notice">
|
'class' => 'empty-state',
|
||||||
<strong><?= h(t('prompts.default_admin_badge', 'Admin default prompts')) ?></strong>
|
'text' => t('prompts.no_personal', 'No personal prompts yet for this language.'),
|
||||||
<span><?= h(t('prompts.default_admin_hint', 'You are editing global default prompts. Users can copy these to their own prompts.')) ?></span>
|
))
|
||||||
</div>
|
: '';
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="sidebar-actions">
|
$promptTextJson = json_encode(array(
|
||||||
<form method="post" action="/prompts?lang=<?= h($language) ?>&mode=personal">
|
|
||||||
<input type="hidden" name="action" value="copy_all_defaults">
|
|
||||||
<input type="hidden" name="language" value="<?= h($language) ?>">
|
|
||||||
<button type="submit"><?= h(t('prompts.copy_all', 'Copy all')) ?></button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="prompt-list">
|
|
||||||
<?php foreach ($defaultPrompts as $default): ?>
|
|
||||||
<div class="prompt-list-item default-prompt-item">
|
|
||||||
<button type="button"
|
|
||||||
class="prompt-select js-open-prompt"
|
|
||||||
data-kind="default"
|
|
||||||
data-id="<?= h($default->id()) ?>">
|
|
||||||
<span class="prompt-name"><?= h($default->name()) ?></span>
|
|
||||||
<span class="prompt-subline"><code><?= h($default->defaultKey()) ?></code> · <?= h(fmt_time($default->updatedAt())) ?></span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<form method="post" action="/prompts?lang=<?= h($language) ?>&mode=personal">
|
|
||||||
<input type="hidden" name="action" value="copy_default">
|
|
||||||
<input type="hidden" name="default_id" value="<?= h($default->id()) ?>">
|
|
||||||
<button type="submit"><?= h(t('prompts.copy', 'copy')) ?></button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<?php if ($user->isAdmin()): ?>
|
|
||||||
<form method="post" action="/prompts?lang=<?= h($language) ?>&mode=defaults"
|
|
||||||
onsubmit="return confirm('<?= h(t('prompts.delete_default_confirm', 'Delete default prompt')) ?> <?= h($default->name()) ?>?');">
|
|
||||||
<input type="hidden" name="action" value="delete_default">
|
|
||||||
<input type="hidden" name="default_id" value="<?= h($default->id()) ?>">
|
|
||||||
<button type="submit"><?= h(t('prompts.delete', 'delete')) ?></button>
|
|
||||||
</form>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
<?php if (count($defaultPrompts) === 0): ?>
|
|
||||||
<p class="empty-state"><?= h(t('prompts.no_defaults', 'No default prompts for this language yet.')) ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($user->isAdmin()): ?>
|
|
||||||
<details class="create-drawer">
|
|
||||||
<summary><?= h(t('prompts.create_default', 'Create default prompt')) ?></summary>
|
|
||||||
<form method="post" action="/prompts?lang=<?= h($language) ?>&mode=defaults">
|
|
||||||
<input type="hidden" name="action" value="create_default">
|
|
||||||
<label><?= h(t('prompts.default_key', 'Default key')) ?><br><input type="text" name="default_key" placeholder="bootstrap-racket"></label>
|
|
||||||
<label><?= h(t('prompts.name', 'Name')) ?><br><input type="text" name="name"></label>
|
|
||||||
<label><?= h(t('prompts.language', 'Language')) ?><br>
|
|
||||||
<select name="language">
|
|
||||||
<?php foreach ($store->supportedLanguages() as $lang): ?>
|
|
||||||
<option value="<?= h($lang) ?>" <?= $lang === $language ? 'selected' : '' ?>>
|
|
||||||
<?= h($store->languageLabel($lang)) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label><?= h(t('prompts.prompt_content', 'Prompt content')) ?><br><textarea name="content" rows="7"></textarea></label>
|
|
||||||
<button type="submit"><?= h(t('prompts.create_default', 'Create default prompt')) ?></button>
|
|
||||||
</form>
|
|
||||||
</details>
|
|
||||||
<?php endif; ?>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="prompt-tab-panel <?= $mode !== 'defaults' ? 'active' : '' ?>" id="tab-personal" role="tabpanel">
|
|
||||||
<div class="prompt-list">
|
|
||||||
<?php foreach ($personalPrompts as $prompt): ?>
|
|
||||||
<div class="prompt-list-item">
|
|
||||||
<button type="button"
|
|
||||||
class="prompt-select js-open-prompt"
|
|
||||||
data-kind="personal"
|
|
||||||
data-id="<?= h($prompt->id()) ?>">
|
|
||||||
<span class="prompt-name"><?= h($prompt->name()) ?></span>
|
|
||||||
<span class="prompt-subline"><?= h($store->languageLabel($prompt->language())) ?> · <?= h(fmt_time($prompt->updatedAt())) ?></span>
|
|
||||||
</button>
|
|
||||||
<form method="post" action="/prompts?lang=<?= h($language) ?>&mode=personal"
|
|
||||||
onsubmit="return confirm('<?= h(t('prompts.delete_prompt_confirm', 'Delete prompt')) ?> <?= h($prompt->name()) ?>?');">
|
|
||||||
<input type="hidden" name="action" value="delete_prompt">
|
|
||||||
<input type="hidden" name="prompt_id" value="<?= h($prompt->id()) ?>">
|
|
||||||
<button type="submit"><?= h(t('prompts.delete', 'delete')) ?></button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
<?php if (count($personalPrompts) === 0): ?>
|
|
||||||
<p class="empty-state"><?= h(t('prompts.no_personal', 'No personal prompts yet for this language.')) ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<details class="create-drawer">
|
|
||||||
<summary><?= h(t('prompts.create_personal', 'Create personal prompt')) ?></summary>
|
|
||||||
<form method="post" action="/prompts?lang=<?= h($language) ?>&mode=personal">
|
|
||||||
<input type="hidden" name="action" value="create_prompt">
|
|
||||||
<label><?= h(t('prompts.name', 'Name')) ?><br><input type="text" name="name"></label>
|
|
||||||
<label><?= h(t('prompts.language', 'Language')) ?><br>
|
|
||||||
<select name="language">
|
|
||||||
<?php foreach ($store->supportedLanguages() as $lang): ?>
|
|
||||||
<option value="<?= h($lang) ?>" <?= $lang === $language ? 'selected' : '' ?>>
|
|
||||||
<?= h($store->languageLabel($lang)) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label><?= h(t('prompts.prompt_content', 'Prompt content')) ?><br><textarea name="content" rows="7"></textarea></label>
|
|
||||||
<button type="submit"><?= h(t('prompts.create_personal', 'Create prompt')) ?></button>
|
|
||||||
</form>
|
|
||||||
</details>
|
|
||||||
</section>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<section id="promptViewer" class="prompt-viewer panel is-empty">
|
|
||||||
<div class="viewer-empty">
|
|
||||||
<?= h(t('prompts.select_prompt', 'Select a prompt on the left to view it.')) ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="viewer-shell">
|
|
||||||
<div class="viewer-header">
|
|
||||||
<div>
|
|
||||||
<div class="viewer-title" id="viewerTitle"><?= h(t('prompts.prompt', 'Prompt')) ?></div>
|
|
||||||
<div class="version-meta" id="viewerMeta"></div>
|
|
||||||
</div>
|
|
||||||
<button type="button" id="editPromptButton"><?= h(t('prompts.edit', 'Edit')) ?></button>
|
|
||||||
</div>
|
|
||||||
<pre id="viewerContent" class="viewer-content"></pre>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer class="page-footer">
|
|
||||||
<a href="/"><?= h(t('prompts.back', 'Back to Racket sandbox')) ?></a>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="promptModalBackdrop" class="modal-backdrop">
|
|
||||||
<div class="prompt-modal">
|
|
||||||
|
|
||||||
<form id="promptModalForm" class="edit-mode" method="post" action="/prompts?lang=<?= h($language) ?>&mode=<?= h($mode) ?>">
|
|
||||||
|
|
||||||
<div class="modal-header">
|
|
||||||
<div>
|
|
||||||
<div class="modal-title" id="modalTitle"><?= h(t('prompts.prompt', 'Prompt')) ?></div>
|
|
||||||
<div class="version-meta" id="modalMeta"></div>
|
|
||||||
</div>
|
|
||||||
<button type="button" id="cancelEditButton"><?= h(t('prompts.close', 'Close')) ?></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-toolbar">
|
|
||||||
<button type="button" id="versionNewerButton"><?= h(t('prompts.newer_previous', 'newer previous version')) ?></button>
|
|
||||||
<button type="button" id="versionOlderButton"><?= h(t('prompts.older_previous', 'older previous version')) ?></button>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
<?= h(t('prompts.diff_view', 'Diff view:')) ?>
|
|
||||||
<select id="diffMode">
|
|
||||||
<option value="plain"><?= h(t('prompts.diff_plain', 'text, no diff')) ?></option>
|
|
||||||
<option value="all" selected><?= h(t('prompts.diff_all', 'all diff')) ?></option>
|
|
||||||
<option value="same"><?= h(t('prompts.diff_same', 'unchanged only')) ?></option>
|
|
||||||
<option value="added"><?= h(t('prompts.diff_added', 'additions only')) ?></option>
|
|
||||||
<option value="deleted"><?= h(t('prompts.diff_deleted', 'deletions only')) ?></option>
|
|
||||||
<option value="changed"><?= h(t('prompts.diff_changed', 'changes only')) ?></option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<span id="versionIndicator" class="version-meta"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
|
|
||||||
<div class="edit-pane">
|
|
||||||
<div class="modal-form-grid">
|
|
||||||
<label>
|
|
||||||
<?= h(t('prompts.name', 'Name')) ?><br>
|
|
||||||
<input type="text" id="modalName" name="name">
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
<?= h(t('prompts.language', 'Language')) ?><br>
|
|
||||||
<select id="modalLanguage" name="language">
|
|
||||||
<?php foreach ($store->supportedLanguages() as $lang): ?>
|
|
||||||
<option value="<?= h($lang) ?>"><?= h($store->languageLabel($lang)) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="defaultKeyRow" class="default-key-row">
|
|
||||||
<label>
|
|
||||||
<?= h(t('prompts.default_key', 'Default key')) ?><br>
|
|
||||||
<input type="text" id="modalDefaultKey" name="default_key">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="content-label">
|
|
||||||
<span><?= h(t('prompts.prompt_content', 'Prompt content')) ?></span>
|
|
||||||
<textarea id="modalContent" name="content"></textarea>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="version-pane">
|
|
||||||
<div>
|
|
||||||
<strong><?= h(t('prompts.previous_version', 'Previous version')) ?></strong>
|
|
||||||
<div id="selectedVersionMeta" class="version-meta"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="version-content" id="versionContent"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<input type="hidden" id="modalAction" name="action" value="">
|
|
||||||
<input type="hidden" id="modalPromptId" name="prompt_id" value="">
|
|
||||||
<input type="hidden" id="modalDefaultId" name="default_id" value="">
|
|
||||||
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" name="create_version" value="1" checked>
|
|
||||||
<?= h(t('prompts.store_version', 'store this edit as a new version')) ?>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
<?= h(t('prompts.version_note', 'Version note:')) ?>
|
|
||||||
<input type="text" name="version_note" value="editor edit">
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button type="submit"><?= h(t('prompts.save', 'Save')) ?></button>
|
|
||||||
<button type="button" id="snapshotButton"><?= h(t('prompts.store_snapshot', 'Store snapshot')) ?></button>
|
|
||||||
<button type="button" id="restoreVersionButton"><?= h(t('prompts.restore_version', 'Restore selected version')) ?></button>
|
|
||||||
<button type="button" id="deleteVersionButton"><?= h(t('prompts.delete_version', 'Delete selected version')) ?></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="modalAuxForm" method="post" action="/prompts?lang=<?= h($language) ?>&mode=<?= h($mode) ?>" class="hidden">
|
|
||||||
<input type="hidden" id="auxAction" name="action" value="">
|
|
||||||
<input type="hidden" id="auxPromptId" name="prompt_id" value="">
|
|
||||||
<input type="hidden" id="auxDefaultId" name="default_id" value="">
|
|
||||||
<input type="hidden" id="auxVersionNo" name="version_no" value="">
|
|
||||||
<input type="hidden" id="auxVersionNote" name="version_note" value="">
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<script type="application/json" id="promptDataJson">
|
|
||||||
<?= json_encode($promptData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>
|
|
||||||
</script>
|
|
||||||
<script type="application/json" id="promptTextJson">
|
|
||||||
<?= json_encode(array(
|
|
||||||
'prompt_not_found' => t('prompts.prompt_not_found', 'Prompt not found'),
|
'prompt_not_found' => t('prompts.prompt_not_found', 'Prompt not found'),
|
||||||
'no_previous_versions' => t('prompts.no_previous_versions', 'No previous versions stored.'),
|
'no_previous_versions' => t('prompts.no_previous_versions', 'No previous versions stored.'),
|
||||||
'no_lines_for_view' => t('prompts.no_lines_for_view', 'No lines for this view.'),
|
'no_lines_for_view' => t('prompts.no_lines_for_view', 'No lines for this view.'),
|
||||||
@@ -712,9 +453,73 @@ render_app_header(array(
|
|||||||
'of' => t('prompts.of', 'of'),
|
'of' => t('prompts.of', 'of'),
|
||||||
'old' => t('prompts.old', 'old'),
|
'old' => t('prompts.old', 'old'),
|
||||||
'new' => t('prompts.new', 'new'),
|
'new' => t('prompts.new', 'new'),
|
||||||
), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>
|
), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
</script>
|
|
||||||
<script src="/js/prompt-editor.js?v=<?= h($promptEditorVersion) ?>" defer></script>
|
|
||||||
|
|
||||||
</body>
|
$headerHtml = app_header_html(array(
|
||||||
</html>
|
'title' => t('prompts.title', 'Prompt administration'),
|
||||||
|
'nav_items' => $headerNavItems,
|
||||||
|
'user' => $user,
|
||||||
|
'admin_label' => t('app.admin', 'admin'),
|
||||||
|
'language_label' => t('prompts.language', 'Language'),
|
||||||
|
'language' => $language,
|
||||||
|
'languages' => $headerLanguages,
|
||||||
|
'language_action' => '/prompts',
|
||||||
|
'language_hidden' => array('mode' => $mode),
|
||||||
|
'logout_action' => '/prompts?lang=' . rawurlencode($language) . '&mode=' . rawurlencode($mode),
|
||||||
|
'logout_label' => t('app.logout', 'Logout'),
|
||||||
|
'message' => $message,
|
||||||
|
'error' => $error,
|
||||||
|
));
|
||||||
|
|
||||||
|
echo RacketSandboxTemplate::renderFile('prompts.html', array(
|
||||||
|
'language' => $language,
|
||||||
|
'language_url' => rawurlencode($language),
|
||||||
|
'mode_url' => rawurlencode($mode),
|
||||||
|
'title' => t('prompts.title', 'Prompt administration'),
|
||||||
|
'style_version' => $styleVersion,
|
||||||
|
'prompt_editor_version' => $promptEditorVersion,
|
||||||
|
'header_html' => $headerHtml,
|
||||||
|
'defaults_tab_active_class' => $mode === 'defaults' ? 'active' : '',
|
||||||
|
'defaults_tab_selected' => $mode === 'defaults' ? 'true' : 'false',
|
||||||
|
'personal_tab_active_class' => $mode !== 'defaults' ? 'active' : '',
|
||||||
|
'personal_tab_selected' => $mode !== 'defaults' ? 'true' : 'false',
|
||||||
|
'available_defaults_label' => t('prompts.available_defaults', 'Available default prompts'),
|
||||||
|
'your_prompts_label' => t('prompts.your_prompts', 'Your prompts'),
|
||||||
|
'default_admin_notice_html' => $defaultAdminNoticeHtml,
|
||||||
|
'copy_all_label' => t('prompts.copy_all', 'Copy all'),
|
||||||
|
'default_prompts_html' => $defaultPromptsHtml,
|
||||||
|
'no_defaults_html' => $noDefaultsHtml,
|
||||||
|
'create_default_html' => $createDefaultHtml,
|
||||||
|
'personal_prompts_html' => $personalPromptsHtml,
|
||||||
|
'no_personal_html' => $noPersonalHtml,
|
||||||
|
'create_personal_label' => t('prompts.create_personal', 'Create personal prompt'),
|
||||||
|
'name_label' => t('prompts.name', 'Name'),
|
||||||
|
'language_label' => t('prompts.language', 'Language'),
|
||||||
|
'language_options_html' => $languageOptionsHtml,
|
||||||
|
'language_options_plain_html' => $languageOptionsPlainHtml,
|
||||||
|
'prompt_content_label' => t('prompts.prompt_content', 'Prompt content'),
|
||||||
|
'select_prompt_label' => t('prompts.select_prompt', 'Select a prompt on the left to view it.'),
|
||||||
|
'prompt_label' => t('prompts.prompt', 'Prompt'),
|
||||||
|
'edit_label' => t('prompts.edit', 'Edit'),
|
||||||
|
'back_label' => t('prompts.back', 'Back to Racket sandbox'),
|
||||||
|
'close_label' => t('prompts.close', 'Close'),
|
||||||
|
'newer_previous_label' => t('prompts.newer_previous', 'newer previous version'),
|
||||||
|
'older_previous_label' => t('prompts.older_previous', 'older previous version'),
|
||||||
|
'diff_view_label' => t('prompts.diff_view', 'Diff view:'),
|
||||||
|
'diff_plain_label' => t('prompts.diff_plain', 'text, no diff'),
|
||||||
|
'diff_all_label' => t('prompts.diff_all', 'all diff'),
|
||||||
|
'diff_same_label' => t('prompts.diff_same', 'unchanged only'),
|
||||||
|
'diff_added_label' => t('prompts.diff_added', 'additions only'),
|
||||||
|
'diff_deleted_label' => t('prompts.diff_deleted', 'deletions only'),
|
||||||
|
'diff_changed_label' => t('prompts.diff_changed', 'changes only'),
|
||||||
|
'default_key_label' => t('prompts.default_key', 'Default key'),
|
||||||
|
'previous_version_label' => t('prompts.previous_version', 'Previous version'),
|
||||||
|
'store_version_label' => t('prompts.store_version', 'store this edit as a new version'),
|
||||||
|
'version_note_label' => t('prompts.version_note', 'Version note:'),
|
||||||
|
'save_label' => t('prompts.save', 'Save'),
|
||||||
|
'store_snapshot_label' => t('prompts.store_snapshot', 'Store snapshot'),
|
||||||
|
'restore_version_label' => t('prompts.restore_version', 'Restore selected version'),
|
||||||
|
'delete_version_label' => t('prompts.delete_version', 'Delete selected version'),
|
||||||
|
'prompt_data_json' => json_encode($promptData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
|
||||||
|
'prompt_text_json' => $promptTextJson,
|
||||||
|
));
|
||||||
|
|||||||
@@ -31,14 +31,12 @@
|
|||||||
* links op die pagina gebruikt.
|
* links op die pagina gebruikt.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ini_set('display_errors', '1');
|
require_once __DIR__ . '/config/reporting.php';
|
||||||
ini_set('display_startup_errors', '1');
|
|
||||||
ini_set('log_errors', '1');
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
require_once __DIR__ . '/private/nexttoken.php';
|
require_once __DIR__ . '/private/nexttoken.php';
|
||||||
require_once __DIR__ . '/private/base64config.php';
|
require_once __DIR__ . '/private/base64config.php';
|
||||||
require_once __DIR__ . '/private/racketzip.php';
|
require_once __DIR__ . '/private/racketzip.php';
|
||||||
|
require_once __DIR__ . '/private/Template.php';
|
||||||
|
|
||||||
$TOKENS = new NextTokenStore(__DIR__ . '/data/racket-sandbox.sqlite');
|
$TOKENS = new NextTokenStore(__DIR__ . '/data/racket-sandbox.sqlite');
|
||||||
|
|
||||||
@@ -131,9 +129,10 @@ function text_response($text, $status = 200)
|
|||||||
function fail_html($message, $status = 500)
|
function fail_html($message, $status = 500)
|
||||||
{
|
{
|
||||||
html_response(
|
html_response(
|
||||||
'<!doctype html>' .
|
RacketSandboxTemplate::renderFile('protocol-error.html', array(
|
||||||
'<html><head><meta charset="utf-8"><title>Error</title></head>' .
|
'title' => 'Error',
|
||||||
'<body><h1>Error</h1><pre>' . h($message) . '</pre></body></html>',
|
'message' => $message,
|
||||||
|
)),
|
||||||
$status
|
$status
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -185,88 +184,29 @@ function serve_bootstrap()
|
|||||||
$rows = '';
|
$rows = '';
|
||||||
|
|
||||||
foreach ($parts as $i => $part) {
|
foreach ($parts as $i => $part) {
|
||||||
$rows .=
|
$rows .= RacketSandboxTemplate::renderFile('partials/racket-part-row.html', array(
|
||||||
'<tr>' .
|
'index' => (string)($i + 1),
|
||||||
'<td>' . h((string)($i + 1)) . '</td>' .
|
'number' => $part['number'],
|
||||||
'<td>' . h($part['number']) . '</td>' .
|
'size' => (string)$part['size'],
|
||||||
'<td>' . h((string)$part['size']) . '</td>' .
|
'url' => $part['url'],
|
||||||
'<td><a href="' . h($part['url']) . '">' . h($part['url']) . '</a></td>' .
|
)) . "\n";
|
||||||
'</tr>' . "\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$zipSize = (int)($manifest['source_bytes'] ?? 0);
|
$zipSize = (int)($manifest['source_bytes'] ?? 0);
|
||||||
|
|
||||||
$pkg_url = make_url('/racket-pkg-index');
|
$pkg_url = make_url('/racket-pkg-index');
|
||||||
|
|
||||||
html_response('<!doctype html>
|
html_response(RacketSandboxTemplate::renderFile('bootstrap-racket.html', array(
|
||||||
<html lang="nl">
|
'pkg_url' => $pkg_url,
|
||||||
<head>
|
'zip_size' => (string)$zipSize,
|
||||||
<meta charset="utf-8">
|
'max_base64_kb' => (string)RACKET_ZIP_MAX_BASE64_KB,
|
||||||
<title>Racket bootstrap</title>
|
'max_base64_bytes' => (string)RACKET_ZIP_MAX_BASE64_BYTES,
|
||||||
<link rel="stylesheet" href="/styles.css">
|
'binary_chunk_bytes' => (string)RACKET_ZIP_BINARY_CHUNK_BYTES,
|
||||||
</head>
|
'part_count' => (string)count($parts),
|
||||||
<body class="simple-doc">
|
'created_at' => (string)($manifest['created_at'] ?? ''),
|
||||||
|
'next_id' => $NEXT_ID,
|
||||||
<h1>Racket bootstrap</h1>
|
'part_rows_html' => $rows,
|
||||||
|
)));
|
||||||
<p>
|
|
||||||
<code>racket.zip</code> is vooraf via de configuratiepagina gesplitst naar
|
|
||||||
parts in de map <code>data</code>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Package index: <a href="' . $pkg_url . '"> Racket package index</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Bronbestand: <code>config/racket.zip</code><br>
|
|
||||||
Bronbestand bytes: <code>' . h((string)$zipSize) . '</code><br>
|
|
||||||
Maximale base64 part-grootte: <code>' . h((string)RACKET_ZIP_MAX_BASE64_KB) . '</code> KiB (<code>' . h((string)RACKET_ZIP_MAX_BASE64_BYTES) . '</code> bytes)<br>
|
|
||||||
Binaire chunk-grootte: <code>' . h((string)RACKET_ZIP_BINARY_CHUNK_BYTES) . '</code> bytes<br>
|
|
||||||
Aantal parts: <code>' . h((string)count($parts)) . '</code><br>
|
|
||||||
Parts gemaakt op: <code>' . h((string)($manifest['created_at'] ?? '')) . '</code><br>
|
|
||||||
next-id voor alle links op deze pagina: <code>' . h($NEXT_ID) . '</code>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Elke link hieronder geeft <strong>text/plain</strong> met de
|
|
||||||
<strong>base64-representatie van één binair part</strong>.
|
|
||||||
De URL bevat alleen een nummer, geen bestandsnaam en geen extensie.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>partnummer</th>
|
|
||||||
<th>bytes</th>
|
|
||||||
<th>base64 text/plain URL</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
' . $rows . '
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h2>Reconstructie in de sandbox</h2>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Decodeer ieder base64-part afzonderlijk naar een binair part. Plak daarna de
|
|
||||||
binaire parts in numerieke volgorde aan elkaar.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<pre>
|
|
||||||
base64 -d part-000001.txt > part-000001
|
|
||||||
base64 -d part-000002.txt > part-000002
|
|
||||||
base64 -d part-000003.txt > part-000003
|
|
||||||
# enzovoort
|
|
||||||
|
|
||||||
cat part-* > racket.zip
|
|
||||||
unzip racket.zip -d /tmp/racket
|
|
||||||
</pre>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function serve_part()
|
function serve_part()
|
||||||
|
|||||||
+17
-51
@@ -28,14 +28,12 @@
|
|||||||
* RewriteRule ^racket-pkg-index$ rktpkgs.php [L,QSA]
|
* RewriteRule ^racket-pkg-index$ rktpkgs.php [L,QSA]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ini_set('display_errors', '1');
|
require_once __DIR__ . '/config/reporting.php';
|
||||||
ini_set('display_startup_errors', '1');
|
|
||||||
ini_set('log_errors', '1');
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
require_once __DIR__ . '/private/nexttoken.php';
|
require_once __DIR__ . '/private/nexttoken.php';
|
||||||
require_once __DIR__ . '/private/lib/catalog-http.php';
|
require_once __DIR__ . '/private/lib/catalog-http.php';
|
||||||
require_once __DIR__ . '/private/lib/racket-data.php';
|
require_once __DIR__ . '/private/lib/racket-data.php';
|
||||||
|
require_once __DIR__ . '/private/Template.php';
|
||||||
|
|
||||||
$TOKENS = new NextTokenStore(__DIR__ . '/data/racket-sandbox.sqlite');
|
$TOKENS = new NextTokenStore(__DIR__ . '/data/racket-sandbox.sqlite');
|
||||||
|
|
||||||
@@ -96,9 +94,10 @@ function html_response($html, $status = 200)
|
|||||||
function fail_html($message, $status = 500)
|
function fail_html($message, $status = 500)
|
||||||
{
|
{
|
||||||
html_response(
|
html_response(
|
||||||
'<!doctype html>' .
|
RacketSandboxTemplate::renderFile('protocol-error.html', array(
|
||||||
'<html><head><meta charset="utf-8"><title>Error</title></head>' .
|
'title' => 'Error',
|
||||||
'<body><h1>Error</h1><pre>' . h($message) . '</pre></body></html>',
|
'message' => $message,
|
||||||
|
)),
|
||||||
$status
|
$status
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -135,52 +134,19 @@ function serve_index()
|
|||||||
foreach ($names as $i => $name) {
|
foreach ($names as $i => $name) {
|
||||||
$url = make_url('/package', array('name' => $name));
|
$url = make_url('/package', array('name' => $name));
|
||||||
|
|
||||||
$rows .=
|
$rows .= RacketSandboxTemplate::renderFile('partials/package-index-row.html', array(
|
||||||
'<tr id="pkg-' . h($name) . '">' .
|
'id' => $name,
|
||||||
'<td>' . h((string)($i + 1)) . '</td>' .
|
'index' => (string)($i + 1),
|
||||||
'<td class="pkg-name">' .
|
'url' => $url,
|
||||||
'<a class="pkg-link" href="' . h($url) . '">' . h($name) . '</a>' .
|
'name' => $name,
|
||||||
'</td>' .
|
)) . "\n";
|
||||||
'</tr>' . "\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html_response('<!doctype html>
|
html_response(RacketSandboxTemplate::renderFile('racket-pkg-index.html', array(
|
||||||
<html lang="nl">
|
'package_count' => (string)count($names),
|
||||||
<head>
|
'next_id' => $NEXT_ID,
|
||||||
<meta charset="utf-8">
|
'package_rows_html' => $rows,
|
||||||
<title>Racket package index</title>
|
)));
|
||||||
<link rel="stylesheet" href="/styles.css">
|
|
||||||
</head>
|
|
||||||
<body class="simple-doc package-index-page">
|
|
||||||
|
|
||||||
<h1>Racket package index</h1>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Volledige HTML-index van de Racket package catalogus op basis van
|
|
||||||
<code>pkgs-all</code>. De package-naam is de ophaallink via
|
|
||||||
<code>rktsndbx.dijkewijk.nl</code>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Aantal packages: <code>' . h((string)count($names)) . '</code><br>
|
|
||||||
next-id voor alle package-links op deze pagina:
|
|
||||||
<code>' . h($NEXT_ID) . '</code>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>package</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
' . $rows . '
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$TOKENS->check_valid_next('html');
|
$TOKENS->check_valid_next('html');
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ require_once __DIR__ . '/private/auth.php';
|
|||||||
require_once __DIR__ . '/private/header.php';
|
require_once __DIR__ . '/private/header.php';
|
||||||
require_once __DIR__ . '/private/languagestore.php';
|
require_once __DIR__ . '/private/languagestore.php';
|
||||||
require_once __DIR__ . '/private/usersettings.php';
|
require_once __DIR__ . '/private/usersettings.php';
|
||||||
|
require_once __DIR__ . '/private/viewdata.php';
|
||||||
|
|
||||||
ini_set('display_errors', '1');
|
require_once __DIR__ . '/config/reporting.php';
|
||||||
ini_set('display_startup_errors', '1');
|
|
||||||
ini_set('log_errors', '1');
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
|
|
||||||
$DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite';
|
$DB_FILE = __DIR__ . '/data/racket-sandbox.sqlite';
|
||||||
|
|
||||||
@@ -30,11 +28,11 @@ function h($s)
|
|||||||
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
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;
|
global $languageStore, $language;
|
||||||
|
|
||||||
return $languageStore->translate($key, $language, $fallback);
|
return $languageStore->translateFormat($key, $language, $values, $fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function post_value($name, $default = '')
|
function post_value($name, $default = '')
|
||||||
@@ -77,37 +75,7 @@ $language = resolve_user_language(
|
|||||||
$languageStore->supportedLanguages()
|
$languageStore->supportedLanguages()
|
||||||
);
|
);
|
||||||
|
|
||||||
$languageStore->seedDefaults(array(
|
seed_template_translations($languageStore, 'users.html');
|
||||||
'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.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.enabled' => array('en' => 'Enabled', 'nl' => 'Ingeschakeld'),
|
|
||||||
'app.full_name' => array('en' => 'Full name', 'nl' => 'Volledige naam'),
|
|
||||||
'app.email' => array('en' => 'Email', 'nl' => 'E-mail'),
|
|
||||||
'app.password' => array('en' => 'Password', 'nl' => 'Wachtwoord'),
|
|
||||||
'app.new_password' => array('en' => 'New password', 'nl' => 'Nieuw wachtwoord'),
|
|
||||||
'app.created' => array('en' => 'Created', 'nl' => 'Gemaakt'),
|
|
||||||
'app.last_login' => array('en' => 'Last login', 'nl' => 'Laatste login'),
|
|
||||||
'app.actions' => array('en' => 'Actions', 'nl' => 'Acties'),
|
|
||||||
'app.create_user' => array('en' => 'Create user', 'nl' => 'Gebruiker aanmaken'),
|
|
||||||
'app.update_user' => array('en' => 'Update user', 'nl' => 'Gebruiker aanpassen'),
|
|
||||||
'app.change_password' => array('en' => 'Change password', 'nl' => 'Wachtwoord wijzigen'),
|
|
||||||
'app.delete_user' => array('en' => 'Delete user', 'nl' => 'Gebruiker verwijderen'),
|
|
||||||
'app.delete_user_confirm' => array('en' => 'Delete user', 'nl' => 'Gebruiker verwijderen'),
|
|
||||||
'app.cannot_delete_self' => array('en' => 'You cannot delete your own account.', 'nl' => 'Je kunt je eigen account niet verwijderen.'),
|
|
||||||
'app.cannot_disable_self' => array('en' => 'You cannot disable your own account.', 'nl' => 'Je kunt je eigen account niet uitschakelen.'),
|
|
||||||
'app.cannot_remove_own_admin' => array('en' => 'You cannot remove your own admin rights.', 'nl' => 'Je kunt je eigen adminrechten niet verwijderen.'),
|
|
||||||
'app.user_created' => array('en' => 'User created.', 'nl' => 'Gebruiker aangemaakt.'),
|
|
||||||
'app.user_updated' => array('en' => 'User updated.', 'nl' => 'Gebruiker aangepast.'),
|
|
||||||
'app.password_changed' => array('en' => 'Password changed.', 'nl' => 'Wachtwoord gewijzigd.'),
|
|
||||||
'app.user_deleted' => array('en' => 'User deleted.', 'nl' => 'Gebruiker verwijderd.'),
|
|
||||||
'app.back_to_sandbox' => array('en' => 'Back to Racket sandbox', 'nl' => 'Terug naar Racket sandbox'),
|
|
||||||
'app.configuration' => array('en' => 'Configuration', 'nl' => 'Configuratie'),
|
|
||||||
));
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$action = post_value('action');
|
$action = post_value('action');
|
||||||
@@ -169,20 +137,46 @@ foreach ($languageStore->supportedLanguages() as $lang) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
header('Content-Type: text/html; charset=utf-8');
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
?>
|
$userRowsHtml = '';
|
||||||
<!doctype html>
|
|
||||||
<html lang="<?= h($language) ?>">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title><?= h(t('app.user_management', 'User management')) ?></title>
|
|
||||||
<link rel="stylesheet" href="/styles.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="page">
|
foreach ($users as $managedUser) {
|
||||||
|
if ($managedUser->id() !== $currentUser->id()) {
|
||||||
|
$deleteHtml = RacketSandboxTemplate::renderFile('partials/user-delete-form.html', array(
|
||||||
|
'language_url' => rawurlencode($language),
|
||||||
|
'confirm_json' => json_encode(t('app.delete_user_confirm', 'Delete user {{email}}?', array(
|
||||||
|
'email' => $managedUser->email(),
|
||||||
|
)), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT),
|
||||||
|
'email' => $managedUser->email(),
|
||||||
|
'user_id' => $managedUser->id(),
|
||||||
|
'delete_user_label' => t('app.delete_user', 'Delete user'),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$deleteHtml = RacketSandboxTemplate::renderFile('partials/user-self-note.html', array(
|
||||||
|
'cannot_delete_self' => t('app.cannot_delete_self', 'You cannot delete your own account.'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
<?php
|
$userRowsHtml .= RacketSandboxTemplate::renderFile('partials/user-row.html', array(
|
||||||
render_app_header(array(
|
'language_url' => rawurlencode($language),
|
||||||
|
'user_id' => $managedUser->id(),
|
||||||
|
'full_name_label' => t('app.full_name', 'Full name'),
|
||||||
|
'full_name' => $managedUser->fullName(),
|
||||||
|
'email_label' => t('app.email', 'Email'),
|
||||||
|
'email' => $managedUser->email(),
|
||||||
|
'admin_label' => t('app.admin', 'Admin'),
|
||||||
|
'enabled_label' => t('app.enabled', 'Enabled'),
|
||||||
|
'is_admin_checked' => $managedUser->isAdmin() ? ' checked' : '',
|
||||||
|
'is_enabled_checked' => $managedUser->isEnabled() ? ' checked' : '',
|
||||||
|
'created_at' => fmt_time($managedUser->createdAt()),
|
||||||
|
'last_login_at' => fmt_time($managedUser->lastLoginAt()),
|
||||||
|
'update_user_label' => t('app.update_user', 'Update user'),
|
||||||
|
'new_password_label' => t('app.new_password', 'New password'),
|
||||||
|
'change_password_label' => t('app.change_password', 'Change password'),
|
||||||
|
'delete_html' => $deleteHtml,
|
||||||
|
)) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$headerHtml = app_header_html(array(
|
||||||
'title' => t('app.user_management', 'User management'),
|
'title' => t('app.user_management', 'User management'),
|
||||||
'nav_items' => array(
|
'nav_items' => array(
|
||||||
array('label' => t('app.back_to_sandbox', 'Back to Racket sandbox'), 'url' => '/?lang=' . rawurlencode($language)),
|
array('label' => t('app.back_to_sandbox', 'Back to Racket sandbox'), 'url' => '/?lang=' . rawurlencode($language)),
|
||||||
@@ -215,83 +209,21 @@ render_app_header(array(
|
|||||||
'message' => $message,
|
'message' => $message,
|
||||||
'error' => $error,
|
'error' => $error,
|
||||||
));
|
));
|
||||||
?>
|
|
||||||
|
|
||||||
<main class="page-main dashboard-main">
|
echo RacketSandboxTemplate::renderFile('users.html', array(
|
||||||
|
'language' => $language,
|
||||||
<section class="panel">
|
'language_url' => rawurlencode($language),
|
||||||
<h2><?= h(t('app.create_user', 'Create user')) ?></h2>
|
'title' => t('app.user_management', 'User management'),
|
||||||
|
'header_html' => $headerHtml,
|
||||||
<form method="post" action="/users?lang=<?= h($language) ?>" class="admin-form-grid">
|
'create_user_label' => t('app.create_user', 'Create user'),
|
||||||
<input type="hidden" name="action" value="create_user">
|
'full_name_label' => t('app.full_name', 'Full name'),
|
||||||
<label><?= h(t('app.full_name', 'Full name')) ?><br><input type="text" name="full_name" required></label>
|
'email_label' => t('app.email', 'Email'),
|
||||||
<label><?= h(t('app.email', 'Email')) ?><br><input type="email" name="email" required></label>
|
'password_label' => t('app.password', 'Password'),
|
||||||
<label><?= h(t('app.password', 'Password')) ?><br><input type="password" name="password" autocomplete="new-password" required></label>
|
'admin_label' => t('app.admin', 'Admin'),
|
||||||
<label><input type="checkbox" name="is_admin" value="1"> <?= h(t('app.admin', 'Admin')) ?></label>
|
'enabled_label' => t('app.enabled', 'Enabled'),
|
||||||
<label><input type="checkbox" name="is_enabled" value="1" checked> <?= h(t('app.enabled', 'Enabled')) ?></label>
|
'user_management_label' => t('app.user_management', 'User management'),
|
||||||
<button type="submit"><?= h(t('app.create_user', 'Create user')) ?></button>
|
'created_label' => t('app.created', 'Created'),
|
||||||
</form>
|
'last_login_label' => t('app.last_login', 'Last login'),
|
||||||
</section>
|
'actions_label' => t('app.actions', 'Actions'),
|
||||||
|
'user_rows_html' => $userRowsHtml,
|
||||||
<section class="panel">
|
));
|
||||||
<h2><?= h(t('app.user_management', 'User management')) ?></h2>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th><?= h(t('app.full_name', 'Full name')) ?></th>
|
|
||||||
<th><?= h(t('app.email', 'Email')) ?></th>
|
|
||||||
<th><?= h(t('app.admin', 'Admin')) ?></th>
|
|
||||||
<th><?= h(t('app.enabled', 'Enabled')) ?></th>
|
|
||||||
<th><?= h(t('app.created', 'Created')) ?></th>
|
|
||||||
<th><?= h(t('app.last_login', 'Last login')) ?></th>
|
|
||||||
<th><?= h(t('app.actions', 'Actions')) ?></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($users as $managedUser): ?>
|
|
||||||
<tr>
|
|
||||||
<td colspan="7">
|
|
||||||
<form method="post" action="/users?lang=<?= h($language) ?>" class="user-row-form">
|
|
||||||
<input type="hidden" name="action" value="update_user">
|
|
||||||
<input type="hidden" name="user_id" value="<?= h($managedUser->id()) ?>">
|
|
||||||
|
|
||||||
<label><?= h(t('app.full_name', 'Full name')) ?><br><input type="text" name="full_name" value="<?= h($managedUser->fullName()) ?>" required></label>
|
|
||||||
<label><?= h(t('app.email', 'Email')) ?><br><input type="email" name="email" value="<?= h($managedUser->email()) ?>" required></label>
|
|
||||||
<label><input type="checkbox" name="is_admin" value="1" <?= $managedUser->isAdmin() ? 'checked' : '' ?>> <?= h(t('app.admin', 'Admin')) ?></label>
|
|
||||||
<label><input type="checkbox" name="is_enabled" value="1" <?= $managedUser->isEnabled() ? 'checked' : '' ?>> <?= h(t('app.enabled', 'Enabled')) ?></label>
|
|
||||||
<span><?= h(fmt_time($managedUser->createdAt())) ?></span>
|
|
||||||
<span><?= h(fmt_time($managedUser->lastLoginAt())) ?></span>
|
|
||||||
<button type="submit"><?= h(t('app.update_user', 'Update user')) ?></button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="user-row-actions">
|
|
||||||
<form method="post" action="/users?lang=<?= h($language) ?>">
|
|
||||||
<input type="hidden" name="action" value="set_password">
|
|
||||||
<input type="hidden" name="email" value="<?= h($managedUser->email()) ?>">
|
|
||||||
<label><?= h(t('app.new_password', 'New password')) ?><br><input type="password" name="password" autocomplete="new-password"></label>
|
|
||||||
<button type="submit"><?= h(t('app.change_password', 'Change password')) ?></button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<?php if ($managedUser->id() !== $currentUser->id()): ?>
|
|
||||||
<form method="post" action="/users?lang=<?= h($language) ?>"
|
|
||||||
onsubmit="return confirm('<?= h(t('app.delete_user_confirm', 'Delete user')) ?> <?= h($managedUser->email()) ?>?');">
|
|
||||||
<input type="hidden" name="action" value="delete_user">
|
|
||||||
<input type="hidden" name="user_id" value="<?= h($managedUser->id()) ?>">
|
|
||||||
<button type="submit"><?= h(t('app.delete_user', 'Delete user')) ?></button>
|
|
||||||
</form>
|
|
||||||
<?php else: ?>
|
|
||||||
<p class="small"><?= h(t('app.cannot_delete_self', 'You cannot delete your own account.')) ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user