mirror of
https://github.com/ManfredAabye/opensimMULTITOOLS-II.git
synced 2026-08-14 08:52:24 +00:00
020420261121
This commit is contained in:
+1
-1
@@ -140,7 +140,7 @@ SCRIPTNAME="opensimMULTITOOL II"
|
||||
#testmodus=1 # Testmodus: 1=aktiviert, 0=deaktiviert
|
||||
|
||||
# Versionsnummer besteht aus: Jahr.Monat.Funktionsanzahl.Eigentliche_Versionsnummer
|
||||
VERSION="V25.5.130.437"
|
||||
VERSION="V26.4.230.588"
|
||||
log "\e[36m$SCRIPTNAME\e[0m $VERSION"
|
||||
echo "Dies ist ein Tool welches der Verwaltung von OpenSim Servern dient."
|
||||
echo "Bitte beachten Sie, dass die Anwendung auf eigene Gefahr und Verantwortung erfolgt."
|
||||
|
||||
+31
-12
@@ -1,21 +1,28 @@
|
||||
# OSMTool Web PHP (DE/EN/FR/ES)
|
||||
|
||||
This folder contains a minimal and secure PHP web controller for `osmtool_main.sh`.
|
||||
This folder contains a complete PHP web interface for `osmtool_main.sh`.
|
||||
|
||||
## Features
|
||||
|
||||
- Login-protected web UI
|
||||
- Language switch: `de`, `en`, `fr`, `es`
|
||||
- CSRF protection
|
||||
- Whitelisted actions only
|
||||
- Session-based execution history
|
||||
- Live status cards for screen sessions, ports and latest artifacts
|
||||
- Command preview and output viewer
|
||||
- External CSS and JavaScript assets
|
||||
- Whitelisted module/action execution only
|
||||
- Runner script wrapper for safer command execution
|
||||
- Configurable timeout for long-running commands
|
||||
|
||||
## File Layout
|
||||
|
||||
- `public/index.php` - web UI and request handling
|
||||
- `src/bootstrap.php` - session, auth helpers, CSRF
|
||||
- `public/style.css` - layout and visual design
|
||||
- `public/app.js` - client-side form switching for modules and actions
|
||||
- `src/web_init.php` - session, auth helpers, CSRF
|
||||
- `src/i18n.php` - translations
|
||||
- `src/runner.php` - command whitelist and execution
|
||||
- `src/runner.php` - module catalog, command whitelist and execution
|
||||
- `src/config.sample.php` - configuration template
|
||||
- `bin/osmtool_web_runner.sh` - controlled bridge to `osmtool_main.sh`
|
||||
|
||||
@@ -60,18 +67,30 @@ www-data ALL=(manni) NOPASSWD: /var/www/html/osmtool-web/bin/osmtool_web_runner.
|
||||
|
||||
Then keep `use_sudo=true` and `sudo_user=manni` in config.
|
||||
|
||||
## Supported Actions
|
||||
## Supported Modules
|
||||
|
||||
- `start`
|
||||
- `stop`
|
||||
- `restart`
|
||||
- `smoke`
|
||||
- `report`
|
||||
- `install`
|
||||
- `startstop`
|
||||
- `cleanup`
|
||||
- `health`
|
||||
- `cron-list`
|
||||
- `backup`
|
||||
- `restore`
|
||||
- `update`
|
||||
- `config`
|
||||
- `report`
|
||||
- `smoke`
|
||||
- `cron`
|
||||
|
||||
## Notes On Scope
|
||||
|
||||
- The web interface now exposes the modular command set instead of only a few shortcut actions.
|
||||
- Visible form fields change automatically depending on the selected module and action.
|
||||
- Sensitive values such as passwords and secrets are masked in the command preview.
|
||||
- Timeout handling uses `command_timeout_seconds` from `src/config.php` or `src/config.sample.php`.
|
||||
- Live status cards can be tuned with `status_ports` and `status_screens` in `src/config.php`.
|
||||
|
||||
## Notes
|
||||
|
||||
- Do not expose this UI publicly without HTTPS and IP restrictions.
|
||||
- Keep the password strong and unique.
|
||||
- Extend actions only in `src/runner.php` and `bin/osmtool_web_runner.sh` together.
|
||||
- Extend modules or actions in `src/runner.php` and `bin/osmtool_web_runner.sh` together.
|
||||
|
||||
@@ -10,10 +10,20 @@ if [[ ! -x "$MAIN_SCRIPT" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ACTION="${1:-}"
|
||||
PROFILE="${2:-grid-sim}"
|
||||
LANG="${3:-de}"
|
||||
WORKDIR="${4:-/opt}"
|
||||
MODULE="${1:-}"
|
||||
ACTION="${2:-}"
|
||||
PROFILE="${3:-grid-sim}"
|
||||
LANG="${4:-de}"
|
||||
WORKDIR="${5:-/opt}"
|
||||
|
||||
shift $(( $# >= 5 ? 5 : $# ))
|
||||
|
||||
EXTRA_ARGS=("$@")
|
||||
|
||||
case "$MODULE" in
|
||||
install|startstop|cleanup|health|backup|restore|update|config|report|smoke|cron) ;;
|
||||
*) echo "ERROR: unsupported module" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
case "$PROFILE" in
|
||||
grid-sim|robust|standalone) ;;
|
||||
@@ -26,33 +36,7 @@ case "$LANG" in
|
||||
esac
|
||||
|
||||
run_main() {
|
||||
bash "$MAIN_SCRIPT" --mode cli --profile "$PROFILE" --lang "$LANG" --workdir "$WORKDIR" "$@"
|
||||
bash "$MAIN_SCRIPT" --mode cli --profile "$PROFILE" --lang "$LANG" --workdir "$WORKDIR" --module "$MODULE" --action "$ACTION" "${EXTRA_ARGS[@]}"
|
||||
}
|
||||
|
||||
case "$ACTION" in
|
||||
start)
|
||||
run_main --module startstop --action start
|
||||
;;
|
||||
stop)
|
||||
run_main --module startstop --action stop
|
||||
;;
|
||||
restart)
|
||||
run_main --module startstop --action restart
|
||||
;;
|
||||
smoke)
|
||||
run_main --module smoke --action run
|
||||
;;
|
||||
report)
|
||||
run_main --module report --action generate
|
||||
;;
|
||||
health)
|
||||
run_main --module health --action run
|
||||
;;
|
||||
cron-list)
|
||||
run_main --module cron --action list
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: unsupported action" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
run_main
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
(function () {
|
||||
const payload = window.osmtoolUiData;
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moduleSelect = document.getElementById('module');
|
||||
const actionSelect = document.getElementById('action');
|
||||
const profileSelect = document.getElementById('profile');
|
||||
const descriptionNode = document.getElementById('action-description');
|
||||
const fieldRows = Array.from(document.querySelectorAll('.action-field'));
|
||||
const targetSelect = document.getElementById('field-target');
|
||||
|
||||
if (!moduleSelect || !actionSelect || !profileSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
function currentModule() {
|
||||
return payload.catalog[moduleSelect.value] || null;
|
||||
}
|
||||
|
||||
function currentAction() {
|
||||
const moduleData = currentModule();
|
||||
if (!moduleData) {
|
||||
return null;
|
||||
}
|
||||
return moduleData.actions[actionSelect.value] || null;
|
||||
}
|
||||
|
||||
function rebuildActionOptions() {
|
||||
const moduleData = currentModule();
|
||||
if (!moduleData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previous = actionSelect.value;
|
||||
actionSelect.innerHTML = '';
|
||||
|
||||
Object.entries(moduleData.actions).forEach(([actionKey, actionData], index) => {
|
||||
if (Array.isArray(actionData.profiles) && !actionData.profiles.includes(profileSelect.value)) {
|
||||
return;
|
||||
}
|
||||
const option = document.createElement('option');
|
||||
option.value = actionKey;
|
||||
option.textContent = actionData.label;
|
||||
if (actionKey === previous || (!moduleData.actions[previous] && index === 0)) {
|
||||
option.selected = true;
|
||||
}
|
||||
actionSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function rebuildTargetOptions() {
|
||||
if (!targetSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetField = payload.fields.target;
|
||||
const options = (targetField.target_by_profile && targetField.target_by_profile[profileSelect.value]) || [];
|
||||
const previous = targetSelect.value;
|
||||
targetSelect.innerHTML = '';
|
||||
|
||||
options.forEach((optionValue, index) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = optionValue;
|
||||
option.textContent = optionValue;
|
||||
if (optionValue === previous || (!options.includes(previous) && index === 0)) {
|
||||
option.selected = true;
|
||||
}
|
||||
targetSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function updateVisibleFields() {
|
||||
const actionData = currentAction();
|
||||
const visible = new Set(actionData ? actionData.fields : []);
|
||||
|
||||
fieldRows.forEach((row) => {
|
||||
const shouldShow = visible.has(row.dataset.field);
|
||||
row.classList.toggle('is-hidden', !shouldShow);
|
||||
});
|
||||
|
||||
rebuildTargetOptions();
|
||||
if (descriptionNode && actionData) {
|
||||
descriptionNode.textContent = actionData.description;
|
||||
}
|
||||
}
|
||||
|
||||
moduleSelect.addEventListener('change', () => {
|
||||
rebuildActionOptions();
|
||||
updateVisibleFields();
|
||||
});
|
||||
|
||||
profileSelect.addEventListener('change', () => {
|
||||
rebuildActionOptions();
|
||||
updateVisibleFields();
|
||||
});
|
||||
actionSelect.addEventListener('change', updateVisibleFields);
|
||||
|
||||
rebuildActionOptions();
|
||||
updateVisibleFields();
|
||||
})();
|
||||
+428
-180
@@ -1,16 +1,49 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../src/bootstrap.php';
|
||||
require_once __DIR__ . '/../src/web_init.php';
|
||||
require_once __DIR__ . '/../src/runner.php';
|
||||
|
||||
$lang = detect_lang(supported_languages());
|
||||
$_SESSION['lang'] = $lang;
|
||||
|
||||
$catalog = translated_catalog($lang, $config);
|
||||
$fieldDefs = web_field_definitions($config);
|
||||
|
||||
$selectedModule = (string)($_POST['module'] ?? $_GET['module'] ?? 'startstop');
|
||||
if (!isset($catalog[$selectedModule])) {
|
||||
$selectedModule = 'startstop';
|
||||
}
|
||||
|
||||
$selectedProfile = (string)($_POST['profile'] ?? $_GET['profile'] ?? 'grid-sim');
|
||||
if (!in_array($selectedProfile, allowed_profiles(), true)) {
|
||||
$selectedProfile = 'grid-sim';
|
||||
}
|
||||
|
||||
$selectedAction = (string)($_POST['action'] ?? $_GET['action'] ?? array_key_first($catalog[$selectedModule]['actions']));
|
||||
if (!isset($catalog[$selectedModule]['actions'][$selectedAction])) {
|
||||
$selectedAction = (string)array_key_first($catalog[$selectedModule]['actions']);
|
||||
}
|
||||
|
||||
$defaultWorkdir = (string)($config['default_workdir'] ?? '/opt');
|
||||
$workdir = trim((string)($_POST['workdir'] ?? $_GET['workdir'] ?? $defaultWorkdir));
|
||||
if ($workdir === '') {
|
||||
$workdir = $defaultWorkdir;
|
||||
}
|
||||
|
||||
$formValues = [];
|
||||
foreach ($fieldDefs as $field => $definition) {
|
||||
$formValues[$field] = field_value_from_request($field, $_POST + $_GET, $fieldDefs);
|
||||
}
|
||||
if ($formValues['target'] === '') {
|
||||
$formValues['target'] = default_target_for_profile($selectedProfile);
|
||||
}
|
||||
|
||||
$message = '';
|
||||
$messageType = 'ok';
|
||||
$resultOutput = '';
|
||||
$resultCode = null;
|
||||
$commandPreview = '';
|
||||
|
||||
if (isset($_GET['logout'])) {
|
||||
$_SESSION = [];
|
||||
@@ -31,6 +64,7 @@ if (($_POST['form'] ?? '') === 'login') {
|
||||
$password = (string)($_POST['password'] ?? '');
|
||||
$expected = (string)($config['web_password'] ?? '');
|
||||
if ($expected !== '' && hash_equals($expected, $password)) {
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['auth'] = true;
|
||||
header('Location: index.php?lang=' . urlencode($lang));
|
||||
exit;
|
||||
@@ -41,17 +75,28 @@ if (($_POST['form'] ?? '') === 'login') {
|
||||
}
|
||||
|
||||
if (is_logged_in() && (($_POST['form'] ?? '') === 'run')) {
|
||||
if (!verify_csrf($_POST['csrf'] ?? null)) {
|
||||
$message = t($lang, 'csrf_error');
|
||||
$messageType = 'error';
|
||||
} else {
|
||||
$action = (string)($_POST['action'] ?? '');
|
||||
$profile = (string)($_POST['profile'] ?? 'grid-sim');
|
||||
$workdir = (string)($_POST['workdir'] ?? (string)($config['default_workdir'] ?? '/opt'));
|
||||
$postedLang = detect_lang(supported_languages());
|
||||
$_SESSION['lang'] = $postedLang;
|
||||
$lang = $postedLang;
|
||||
|
||||
$result = run_whitelisted_action($config, $action, $profile, $lang, $workdir);
|
||||
try {
|
||||
if (!verify_csrf($_POST['csrf'] ?? null)) {
|
||||
throw new RuntimeException(t($lang, 'csrf_error'));
|
||||
}
|
||||
|
||||
$request = normalize_run_request($config, $_POST, $lang);
|
||||
$selectedModule = $request['module'];
|
||||
$selectedAction = $request['action'];
|
||||
$selectedProfile = $request['profile'];
|
||||
$workdir = $request['workdir'];
|
||||
foreach ($request['fields'] as $field => $value) {
|
||||
$formValues[$field] = $value;
|
||||
}
|
||||
|
||||
$result = run_whitelisted_action($config, $request);
|
||||
$resultOutput = (string)($result['output'] ?? '');
|
||||
$resultCode = (int)($result['exitCode'] ?? 1);
|
||||
$commandPreview = (string)($result['commandPreview'] ?? '');
|
||||
|
||||
if (($result['ok'] ?? false) === true) {
|
||||
$message = t($lang, 'status_ok') . ' (exit=' . $resultCode . ')';
|
||||
@@ -60,24 +105,84 @@ if (is_logged_in() && (($_POST['form'] ?? '') === 'run')) {
|
||||
$message = t($lang, 'status_error') . ' (exit=' . $resultCode . ')';
|
||||
$messageType = 'error';
|
||||
}
|
||||
|
||||
push_execution_history([
|
||||
'time' => date('Y-m-d H:i:s'),
|
||||
'module' => $selectedModule,
|
||||
'action' => $selectedAction,
|
||||
'profile' => $selectedProfile,
|
||||
'exitCode' => $resultCode,
|
||||
'ok' => $messageType === 'ok',
|
||||
]);
|
||||
} catch (Throwable $exception) {
|
||||
$message = $exception->getMessage();
|
||||
$messageType = 'error';
|
||||
$resultCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
$actions = [
|
||||
'start' => t($lang, 'action_start'),
|
||||
'stop' => t($lang, 'action_stop'),
|
||||
'restart' => t($lang, 'action_restart'),
|
||||
'smoke' => t($lang, 'action_smoke'),
|
||||
'report' => t($lang, 'action_report'),
|
||||
'health' => t($lang, 'action_health'),
|
||||
'cron-list' => t($lang, 'action_cron_list'),
|
||||
];
|
||||
$moduleCount = count($catalog);
|
||||
$actionCount = 0;
|
||||
foreach ($catalog as $moduleData) {
|
||||
$actionCount += count($moduleData['actions']);
|
||||
}
|
||||
|
||||
$profiles = [
|
||||
'grid-sim' => 'grid-sim',
|
||||
'robust' => 'robust',
|
||||
'standalone' => 'standalone',
|
||||
];
|
||||
$history = execution_history();
|
||||
$activeAction = $catalog[$selectedModule]['actions'][$selectedAction];
|
||||
$liveStatus = is_logged_in() ? collect_live_status($config, $selectedProfile, $workdir) : null;
|
||||
|
||||
$clientCatalog = [];
|
||||
foreach ($catalog as $moduleKey => $moduleData) {
|
||||
$clientCatalog[$moduleKey] = [
|
||||
'label' => $moduleData['label'],
|
||||
'description' => $moduleData['description'],
|
||||
'actions' => [],
|
||||
];
|
||||
foreach ($moduleData['actions'] as $actionKey => $actionData) {
|
||||
$clientCatalog[$moduleKey]['actions'][$actionKey] = [
|
||||
'label' => $actionData['label'],
|
||||
'description' => $actionData['description'],
|
||||
'fields' => $actionData['fields'],
|
||||
'profiles' => $actionData['profiles'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$clientFields = [];
|
||||
foreach ($fieldDefs as $field => $definition) {
|
||||
$clientFields[$field] = [
|
||||
'type' => $definition['type'],
|
||||
'options' => $definition['options'] ?? [],
|
||||
'target_by_profile' => $definition['target_by_profile'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
function render_input(string $field, array $definition, string $value, string $lang, string $profile): string
|
||||
{
|
||||
$label = h(t($lang, $definition['label_key']));
|
||||
$placeholder = h((string)($definition['placeholder'] ?? ''));
|
||||
$fieldEscaped = h($field);
|
||||
$valueEscaped = h($value);
|
||||
|
||||
if (($definition['type'] ?? '') === 'select') {
|
||||
$options = $definition['options'] ?? [];
|
||||
if ($field === 'target') {
|
||||
$options = $definition['target_by_profile'][$profile] ?? [];
|
||||
}
|
||||
$html = '<label for="field-' . $fieldEscaped . '">' . $label . '</label>';
|
||||
$html .= '<select id="field-' . $fieldEscaped . '" name="' . $fieldEscaped . '">';
|
||||
foreach ($options as $option) {
|
||||
$selected = $option === $value ? ' selected' : '';
|
||||
$html .= '<option value="' . h($option) . '"' . $selected . '>' . h($option) . '</option>';
|
||||
}
|
||||
$html .= '</select>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
$type = ($definition['type'] ?? 'text') === 'password' ? 'password' : 'text';
|
||||
return '<label for="field-' . $fieldEscaped . '">' . $label . '</label>'
|
||||
. '<input id="field-' . $fieldEscaped . '" type="' . $type . '" name="' . $fieldEscaped . '" value="' . $valueEscaped . '" placeholder="' . $placeholder . '">';
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="<?= h($lang) ?>">
|
||||
@@ -85,180 +190,323 @@ $profiles = [
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= h(t($lang, 'title')) ?></title>
|
||||
<style>
|
||||
:root {
|
||||
--bg1: #0f172a;
|
||||
--bg2: #1e293b;
|
||||
--card: #111827;
|
||||
--line: #334155;
|
||||
--text: #e5e7eb;
|
||||
--muted: #94a3b8;
|
||||
--ok: #16a34a;
|
||||
--err: #dc2626;
|
||||
--btn: #0284c7;
|
||||
--btnHover: #0369a1;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", Tahoma, sans-serif;
|
||||
background: radial-gradient(1000px 600px at 20% 0%, #1d4ed8 0%, transparent 60%),
|
||||
radial-gradient(900px 500px at 100% 100%, #0ea5e9 0%, transparent 50%),
|
||||
linear-gradient(160deg, var(--bg1), var(--bg2));
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
}
|
||||
.wrap { max-width: 980px; margin: 0 auto; }
|
||||
.card {
|
||||
background: color-mix(in oklab, var(--card), black 8%);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
padding: 18px;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,.25);
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
h1 { margin: 0 0 8px; font-size: 1.7rem; }
|
||||
p { margin: 0 0 12px; color: var(--muted); }
|
||||
.topbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
label { display: block; margin-bottom: 6px; font-weight: 600; }
|
||||
input, select, button {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--line);
|
||||
background: #0b1220;
|
||||
color: var(--text);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
button {
|
||||
background: var(--btn);
|
||||
border: none;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover { background: var(--btnHover); }
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.full { grid-column: 1 / -1; }
|
||||
.msg {
|
||||
margin: 10px 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.ok { background: rgba(22,163,74,.2); border: 1px solid rgba(22,163,74,.5); }
|
||||
.error { background: rgba(220,38,38,.2); border: 1px solid rgba(220,38,38,.5); }
|
||||
pre {
|
||||
background: #020617;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
max-height: 420px;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.lang-links a {
|
||||
color: #bae6fd;
|
||||
margin-right: 8px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.lang-links a:hover { text-decoration: underline; }
|
||||
@media (max-width: 800px) {
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script>
|
||||
window.osmtoolUiData = <?= json_encode(['catalog' => $clientCatalog, 'fields' => $clientFields], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
|
||||
</script>
|
||||
<script src="app.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="card">
|
||||
<div class="topbar">
|
||||
<div>
|
||||
<div class="page-shell">
|
||||
<div class="hero-orb hero-orb-left"></div>
|
||||
<div class="hero-orb hero-orb-right"></div>
|
||||
<main class="wrap">
|
||||
<section class="hero card">
|
||||
<div class="hero-copy">
|
||||
<span class="eyebrow"><?= h(t($lang, 'hero_badge')) ?></span>
|
||||
<h1><?= h(t($lang, 'title')) ?></h1>
|
||||
<p><?= h(t($lang, 'subtitle')) ?></p>
|
||||
<p class="hero-text"><?= h(t($lang, 'subtitle_full')) ?></p>
|
||||
</div>
|
||||
<div class="lang-links">
|
||||
<a href="?lang=de">DE</a>
|
||||
<a href="?lang=en">EN</a>
|
||||
<a href="?lang=fr">FR</a>
|
||||
<a href="?lang=es">ES</a>
|
||||
<?php if (is_logged_in()): ?>
|
||||
| <a href="?logout=1&lang=<?= h($lang) ?>"><?= h(t($lang, 'logout')) ?></a>
|
||||
<?php endif; ?>
|
||||
<div class="hero-tools">
|
||||
<div class="lang-links">
|
||||
<a href="?lang=de">DE</a>
|
||||
<a href="?lang=en">EN</a>
|
||||
<a href="?lang=fr">FR</a>
|
||||
<a href="?lang=es">ES</a>
|
||||
<?php if (is_logged_in()): ?>
|
||||
<a class="logout-link" href="?logout=1&lang=<?= h($lang) ?>"><?= h(t($lang, 'logout')) ?></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
<article class="stat-tile">
|
||||
<span><?= h(t($lang, 'stat_modules')) ?></span>
|
||||
<strong><?= h((string)$moduleCount) ?></strong>
|
||||
</article>
|
||||
<article class="stat-tile">
|
||||
<span><?= h(t($lang, 'stat_actions')) ?></span>
|
||||
<strong><?= h((string)$actionCount) ?></strong>
|
||||
</article>
|
||||
<article class="stat-tile">
|
||||
<span><?= h(t($lang, 'stat_timeout')) ?></span>
|
||||
<strong><?= h((string)($config['command_timeout_seconds'] ?? 120)) ?>s</strong>
|
||||
</article>
|
||||
<article class="stat-tile">
|
||||
<span><?= h(t($lang, 'stat_workdir')) ?></span>
|
||||
<strong><?= h($workdir) ?></strong>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if ($message !== ''): ?>
|
||||
<div class="msg <?= h($messageType) ?>"><?= h($message) ?></div>
|
||||
<section class="message-bar <?= h($messageType) ?> card">
|
||||
<strong><?= h($message) ?></strong>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!is_logged_in()): ?>
|
||||
<form method="post">
|
||||
<input type="hidden" name="form" value="login">
|
||||
<input type="hidden" name="csrf" value="<?= h(csrf_token()) ?>">
|
||||
<input type="hidden" name="lang" value="<?= h($lang) ?>">
|
||||
<div class="grid">
|
||||
<div class="full">
|
||||
<label><?= h(t($lang, 'password')) ?></label>
|
||||
<input type="password" name="password" autocomplete="current-password" required>
|
||||
</div>
|
||||
<div class="full">
|
||||
<section class="login-layout">
|
||||
<article class="card login-card">
|
||||
<h2><?= h(t($lang, 'login_title')) ?></h2>
|
||||
<p><?= h(t($lang, 'login_intro')) ?></p>
|
||||
<form method="post" class="stack-form">
|
||||
<input type="hidden" name="form" value="login">
|
||||
<input type="hidden" name="csrf" value="<?= h(csrf_token()) ?>">
|
||||
<input type="hidden" name="lang" value="<?= h($lang) ?>">
|
||||
<label for="login-password"><?= h(t($lang, 'password')) ?></label>
|
||||
<input id="login-password" type="password" name="password" autocomplete="current-password" required>
|
||||
<button type="submit"><?= h(t($lang, 'login')) ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</article>
|
||||
<article class="card feature-card">
|
||||
<h2><?= h(t($lang, 'feature_title')) ?></h2>
|
||||
<ul class="feature-list">
|
||||
<li><?= h(t($lang, 'feature_modules')) ?></li>
|
||||
<li><?= h(t($lang, 'feature_history')) ?></li>
|
||||
<li><?= h(t($lang, 'feature_reports')) ?></li>
|
||||
<li><?= h(t($lang, 'feature_security')) ?></li>
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<p><?= h(t($lang, 'hint')) ?></p>
|
||||
<form method="post">
|
||||
<input type="hidden" name="form" value="run">
|
||||
<input type="hidden" name="csrf" value="<?= h(csrf_token()) ?>">
|
||||
<input type="hidden" name="lang" value="<?= h($lang) ?>">
|
||||
<section class="dashboard-grid">
|
||||
<article class="card quick-card">
|
||||
<div class="section-head">
|
||||
<h2><?= h(t($lang, 'quick_actions')) ?></h2>
|
||||
<p><?= h(t($lang, 'quick_actions_hint')) ?></p>
|
||||
</div>
|
||||
<div class="quick-actions-grid">
|
||||
<?php foreach ([
|
||||
['module' => 'startstop', 'action' => 'start', 'label' => t($lang, 'action_start')],
|
||||
['module' => 'startstop', 'action' => 'restart', 'label' => t($lang, 'action_restart')],
|
||||
['module' => 'health', 'action' => 'run', 'label' => t($lang, 'health_run')],
|
||||
['module' => 'report', 'action' => 'generate', 'label' => t($lang, 'report_generate')],
|
||||
] as $quick): ?>
|
||||
<form method="post" class="quick-form">
|
||||
<input type="hidden" name="form" value="run">
|
||||
<input type="hidden" name="csrf" value="<?= h(csrf_token()) ?>">
|
||||
<input type="hidden" name="lang" value="<?= h($lang) ?>">
|
||||
<input type="hidden" name="module" value="<?= h($quick['module']) ?>">
|
||||
<input type="hidden" name="action" value="<?= h($quick['action']) ?>">
|
||||
<input type="hidden" name="profile" value="<?= h($selectedProfile) ?>">
|
||||
<input type="hidden" name="workdir" value="<?= h($workdir) ?>">
|
||||
<input type="hidden" name="target" value="<?= h(default_target_for_profile($selectedProfile)) ?>">
|
||||
<button type="submit" class="ghost-button"><?= h($quick['label']) ?></button>
|
||||
</form>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div class="grid">
|
||||
<div>
|
||||
<label><?= h(t($lang, 'profile')) ?></label>
|
||||
<select name="profile" required>
|
||||
<?php foreach ($profiles as $value => $label): ?>
|
||||
<option value="<?= h($value) ?>"><?= h($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<article class="card status-card">
|
||||
<div class="section-head">
|
||||
<h2><?= h(t($lang, 'selected_action')) ?></h2>
|
||||
<p id="action-description"><?= h($activeAction['description']) ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<label><?= h(t($lang, 'action')) ?></label>
|
||||
<select name="action" required>
|
||||
<?php foreach ($actions as $value => $label): ?>
|
||||
<option value="<?= h($value) ?>"><?= h($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="badge-row">
|
||||
<span class="badge"><?= h($catalog[$selectedModule]['label']) ?></span>
|
||||
<span class="badge muted"><?= h($activeAction['label']) ?></span>
|
||||
<span class="badge muted"><?= h($selectedProfile) ?></span>
|
||||
</div>
|
||||
<div class="full">
|
||||
<label><?= h(t($lang, 'workdir')) ?></label>
|
||||
<input name="workdir" value="<?= h((string)($config['default_workdir'] ?? '/opt')) ?>">
|
||||
</div>
|
||||
<div class="full">
|
||||
<button type="submit"><?= h(t($lang, 'execute')) ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<?php if ($resultCode !== null): ?>
|
||||
<div class="card" style="margin-top:14px;">
|
||||
<label><?= h(t($lang, 'output')) ?></label>
|
||||
<pre><?= h($resultOutput) ?></pre>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<section class="live-status-grid">
|
||||
<article class="card live-card">
|
||||
<div class="section-head">
|
||||
<h2><?= h(t($lang, 'live_status_title')) ?></h2>
|
||||
<p><?= h(t($lang, 'live_status_hint')) ?> <?= h((string)($liveStatus['generatedAt'] ?? '')) ?></p>
|
||||
</div>
|
||||
<div class="live-metrics">
|
||||
<div class="live-metric">
|
||||
<span><?= h(t($lang, 'live_screens')) ?></span>
|
||||
<strong><?= h((string)($liveStatus['screensSummary']['ok'] ?? 0)) ?>/<?= h((string)($liveStatus['screensSummary']['total'] ?? 0)) ?></strong>
|
||||
</div>
|
||||
<div class="live-metric">
|
||||
<span><?= h(t($lang, 'live_ports')) ?></span>
|
||||
<strong><?= h((string)($liveStatus['portsSummary']['ok'] ?? 0)) ?>/<?= h((string)($liveStatus['portsSummary']['total'] ?? 0)) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-columns">
|
||||
<div>
|
||||
<h3><?= h(t($lang, 'live_screens')) ?></h3>
|
||||
<?php if (($liveStatus['screens']['available'] ?? false) !== true): ?>
|
||||
<p class="empty-state"><?= h(t($lang, 'live_unavailable')) ?></p>
|
||||
<?php else: ?>
|
||||
<ul class="status-list">
|
||||
<?php foreach (($liveStatus['screens']['items'] ?? []) as $item): ?>
|
||||
<li>
|
||||
<span><?= h((string)$item['name']) ?></span>
|
||||
<span class="status-pill <?= !empty($item['ok']) ? 'ok' : 'error' ?>"><?= h(!empty($item['ok']) ? t($lang, 'live_up') : t($lang, 'live_down')) ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div>
|
||||
<h3><?= h(t($lang, 'live_ports')) ?></h3>
|
||||
<?php if (($liveStatus['ports']['available'] ?? false) !== true): ?>
|
||||
<p class="empty-state"><?= h(t($lang, 'live_unavailable')) ?></p>
|
||||
<?php else: ?>
|
||||
<ul class="status-list">
|
||||
<?php foreach (($liveStatus['ports']['items'] ?? []) as $item): ?>
|
||||
<li>
|
||||
<span><?= h((string)$item['name']) ?></span>
|
||||
<span class="status-pill <?= !empty($item['ok']) ? 'ok' : 'error' ?>"><?= h(!empty($item['ok']) ? t($lang, 'live_listen') : t($lang, 'live_closed')) ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card artifact-card">
|
||||
<div class="section-head">
|
||||
<h2><?= h(t($lang, 'artifact_title')) ?></h2>
|
||||
<p><?= h(t($lang, 'artifact_hint')) ?></p>
|
||||
</div>
|
||||
<ul class="artifact-list">
|
||||
<?php foreach (($liveStatus['artifacts'] ?? []) as $artifact): ?>
|
||||
<li>
|
||||
<strong><?= h((string)$artifact['label']) ?></strong>
|
||||
<span><?= h($artifact['path'] !== null ? basename((string)$artifact['path']) : t($lang, 'artifact_missing')) ?></span>
|
||||
<span class="artifact-age"><?= h((string)$artifact['age']) ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="workspace-grid">
|
||||
<article class="card form-card">
|
||||
<div class="section-head">
|
||||
<h2><?= h(t($lang, 'form_title')) ?></h2>
|
||||
<p><?= h(t($lang, 'hint_full')) ?></p>
|
||||
</div>
|
||||
<form method="post" class="command-form" id="command-form">
|
||||
<input type="hidden" name="form" value="run">
|
||||
<input type="hidden" name="csrf" value="<?= h(csrf_token()) ?>">
|
||||
<input type="hidden" name="lang" value="<?= h($lang) ?>">
|
||||
|
||||
<div class="field-grid base-grid">
|
||||
<div class="field-row">
|
||||
<label for="module"><?= h(t($lang, 'field_module')) ?></label>
|
||||
<select id="module" name="module">
|
||||
<?php foreach ($catalog as $moduleKey => $moduleData): ?>
|
||||
<option value="<?= h($moduleKey) ?>"<?= $moduleKey === $selectedModule ? ' selected' : '' ?>><?= h($moduleData['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<label for="action"><?= h(t($lang, 'action')) ?></label>
|
||||
<select id="action" name="action">
|
||||
<?php foreach ($catalog[$selectedModule]['actions'] as $actionKey => $actionData): ?>
|
||||
<option value="<?= h($actionKey) ?>"<?= $actionKey === $selectedAction ? ' selected' : '' ?>><?= h($actionData['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<label for="profile"><?= h(t($lang, 'profile')) ?></label>
|
||||
<select id="profile" name="profile">
|
||||
<?php foreach (allowed_profiles() as $profile): ?>
|
||||
<option value="<?= h($profile) ?>"<?= $profile === $selectedProfile ? ' selected' : '' ?>><?= h($profile) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field-row field-row-wide">
|
||||
<label for="workdir"><?= h(t($lang, 'workdir')) ?></label>
|
||||
<input id="workdir" name="workdir" value="<?= h($workdir) ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-head compact">
|
||||
<h3><?= h(t($lang, 'advanced_fields')) ?></h3>
|
||||
<p><?= h(t($lang, 'advanced_fields_hint')) ?></p>
|
||||
</div>
|
||||
|
||||
<div class="field-grid advanced-grid">
|
||||
<?php foreach ($fieldDefs as $field => $definition): ?>
|
||||
<?php $hidden = !in_array($field, $activeAction['fields'], true); ?>
|
||||
<div class="field-row action-field<?= $hidden ? ' is-hidden' : '' ?>" data-field="<?= h($field) ?>">
|
||||
<?= render_input($field, $definition, $formValues[$field], $lang, $selectedProfile) ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="form-footer">
|
||||
<button type="submit"><?= h(t($lang, 'execute')) ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<article class="card catalog-card">
|
||||
<div class="section-head">
|
||||
<h2><?= h(t($lang, 'catalog_title')) ?></h2>
|
||||
<p><?= h(t($lang, 'catalog_hint')) ?></p>
|
||||
</div>
|
||||
<div class="catalog-list">
|
||||
<?php foreach ($catalog as $moduleData): ?>
|
||||
<div class="catalog-item">
|
||||
<h3><?= h($moduleData['label']) ?></h3>
|
||||
<p><?= h($moduleData['description']) ?></p>
|
||||
<ul>
|
||||
<?php foreach ($moduleData['actions'] as $actionData): ?>
|
||||
<li>
|
||||
<strong><?= h($actionData['label']) ?></strong>
|
||||
<span><?= h($actionData['description']) ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="results-grid">
|
||||
<article class="card result-card">
|
||||
<div class="section-head">
|
||||
<h2><?= h(t($lang, 'execution_result')) ?></h2>
|
||||
<p><?= h(t($lang, 'execution_result_hint')) ?></p>
|
||||
</div>
|
||||
<?php if ($commandPreview !== ''): ?>
|
||||
<div class="preview-block">
|
||||
<span><?= h(t($lang, 'command_preview')) ?></span>
|
||||
<code><?= h($commandPreview) ?></code>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($resultCode !== null): ?>
|
||||
<div class="preview-block">
|
||||
<span><?= h(t($lang, 'output')) ?></span>
|
||||
<pre><?= h($resultOutput) ?></pre>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p class="empty-state"><?= h(t($lang, 'no_result_yet')) ?></p>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
|
||||
<article class="card history-card">
|
||||
<div class="section-head">
|
||||
<h2><?= h(t($lang, 'history_title')) ?></h2>
|
||||
<p><?= h(t($lang, 'history_hint')) ?></p>
|
||||
</div>
|
||||
<?php if ($history === []): ?>
|
||||
<p class="empty-state"><?= h(t($lang, 'history_empty')) ?></p>
|
||||
<?php else: ?>
|
||||
<ul class="history-list">
|
||||
<?php foreach ($history as $entry): ?>
|
||||
<li>
|
||||
<strong><?= h((string)$entry['time']) ?></strong>
|
||||
<span><?= h((string)$entry['module']) ?> / <?= h((string)$entry['action']) ?> / <?= h((string)$entry['profile']) ?></span>
|
||||
<span class="history-status <?= !empty($entry['ok']) ? 'ok' : 'error' ?>">exit=<?= h((string)$entry['exitCode']) ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,567 @@
|
||||
:root {
|
||||
--bg: #f5efe2;
|
||||
--bg-accent: #eadac0;
|
||||
--ink: #221b17;
|
||||
--muted: #6b5b51;
|
||||
--card: rgba(255, 250, 240, 0.88);
|
||||
--line: rgba(86, 60, 44, 0.16);
|
||||
--primary: #9d3c27;
|
||||
--primary-strong: #7d2918;
|
||||
--accent: #164e63;
|
||||
--ok: #1f7a4e;
|
||||
--error: #a12f2f;
|
||||
--shadow: 0 18px 50px rgba(81, 45, 24, 0.12);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
font-family: Georgia, "Trebuchet MS", serif;
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(157, 60, 39, 0.18), transparent 28%),
|
||||
radial-gradient(circle at bottom right, rgba(22, 78, 99, 0.14), transparent 32%),
|
||||
linear-gradient(180deg, #f8f3e8, #f2ead9 45%, #efe4d0);
|
||||
}
|
||||
|
||||
body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.page-shell {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-orb {
|
||||
position: absolute;
|
||||
border-radius: 999px;
|
||||
filter: blur(8px);
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-orb-left {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
left: -90px;
|
||||
top: 80px;
|
||||
background: rgba(157, 60, 39, 0.18);
|
||||
}
|
||||
|
||||
.hero-orb-right {
|
||||
width: 420px;
|
||||
height: 420px;
|
||||
right: -120px;
|
||||
top: 260px;
|
||||
background: rgba(22, 78, 99, 0.16);
|
||||
}
|
||||
|
||||
.wrap {
|
||||
width: min(1360px, calc(100% - 32px));
|
||||
margin: 0 auto;
|
||||
padding: 28px 0 48px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 24px;
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: 1.25fr 1fr;
|
||||
gap: 24px;
|
||||
padding: 28px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(157, 60, 39, 0.1);
|
||||
color: var(--primary);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 12px;
|
||||
font-size: clamp(2.1rem, 4vw, 3.4rem);
|
||||
line-height: 0.95;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.35rem;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.hero-text,
|
||||
.section-head p,
|
||||
.catalog-item p,
|
||||
.empty-state,
|
||||
.feature-list,
|
||||
.history-list span,
|
||||
.stat-tile span,
|
||||
.preview-block span,
|
||||
.message-bar strong,
|
||||
.lang-links a,
|
||||
label {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.hero-copy,
|
||||
.hero-tools,
|
||||
.login-card,
|
||||
.feature-card,
|
||||
.quick-card,
|
||||
.status-card,
|
||||
.form-card,
|
||||
.catalog-card,
|
||||
.result-card,
|
||||
.history-card,
|
||||
.message-bar {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.lang-links {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.lang-links a {
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.logout-link {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.stats-grid,
|
||||
.dashboard-grid,
|
||||
.workspace-grid,
|
||||
.results-grid,
|
||||
.quick-actions-grid,
|
||||
.field-grid,
|
||||
.login-layout {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.stat-tile {
|
||||
border-radius: 18px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255, 255, 255, 0.48);
|
||||
}
|
||||
|
||||
.stat-tile strong {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
font-size: 1.3rem;
|
||||
color: var(--ink);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.message-bar {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.message-bar.ok {
|
||||
border-color: rgba(31, 122, 78, 0.28);
|
||||
background: rgba(31, 122, 78, 0.08);
|
||||
}
|
||||
|
||||
.message-bar.error {
|
||||
border-color: rgba(161, 47, 47, 0.28);
|
||||
background: rgba(161, 47, 47, 0.08);
|
||||
}
|
||||
|
||||
.login-layout {
|
||||
grid-template-columns: minmax(0, 430px) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.stack-form {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
list-style: square;
|
||||
padding-left: 20px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1.1fr 0.9fr;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.live-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.2fr) minmax(280px, 0.8fr);
|
||||
gap: 18px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.live-card,
|
||||
.artifact-card {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.live-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.live-metric {
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.48);
|
||||
}
|
||||
|
||||
.live-metric strong {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.status-columns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.status-list,
|
||||
.artifact-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 12px 0 0;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-list li,
|
||||
.artifact-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(86, 60, 44, 0.12);
|
||||
background: rgba(255, 255, 255, 0.46);
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.status-pill.ok {
|
||||
background: rgba(31, 122, 78, 0.12);
|
||||
color: var(--ok);
|
||||
}
|
||||
|
||||
.status-pill.error {
|
||||
background: rgba(161, 47, 47, 0.12);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.artifact-age {
|
||||
color: var(--muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-head.compact {
|
||||
margin: 18px 0 12px;
|
||||
}
|
||||
|
||||
.quick-actions-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.badge-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(157, 60, 39, 0.12);
|
||||
color: var(--primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.badge.muted {
|
||||
background: rgba(22, 78, 99, 0.1);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.workspace-grid {
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.85fr);
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.base-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.advanced-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.field-row-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.field-row.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
button,
|
||||
code,
|
||||
pre {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(86, 60, 44, 0.18);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
color: var(--ink);
|
||||
padding: 14px 15px;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus {
|
||||
outline: 2px solid rgba(157, 60, 39, 0.22);
|
||||
border-color: rgba(157, 60, 39, 0.38);
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
padding: 14px 18px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-strong));
|
||||
color: #fff7f1;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: transform 140ms ease, box-shadow 140ms ease;
|
||||
box-shadow: 0 10px 24px rgba(125, 41, 24, 0.18);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.ghost-button {
|
||||
background: rgba(255, 255, 255, 0.68);
|
||||
color: var(--ink);
|
||||
border: 1px solid var(--line);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.form-footer {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.catalog-list {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
max-height: 860px;
|
||||
overflow: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.catalog-item {
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255, 255, 255, 0.46);
|
||||
}
|
||||
|
||||
.catalog-item ul,
|
||||
.history-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 14px 0 0;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.catalog-item li,
|
||||
.history-list li {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 10px 0;
|
||||
border-top: 1px solid rgba(86, 60, 44, 0.08);
|
||||
}
|
||||
|
||||
.catalog-item li:first-child,
|
||||
.history-list li:first-child {
|
||||
border-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.results-grid {
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(320px, 0.85fr);
|
||||
}
|
||||
|
||||
.preview-block {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
code,
|
||||
pre {
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(86, 60, 44, 0.16);
|
||||
background: #231e1a;
|
||||
color: #efe6d9;
|
||||
}
|
||||
|
||||
code {
|
||||
display: block;
|
||||
padding: 14px 16px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 16px;
|
||||
min-height: 220px;
|
||||
max-height: 620px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.history-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.history-status.ok {
|
||||
background: rgba(31, 122, 78, 0.12);
|
||||
color: var(--ok);
|
||||
}
|
||||
|
||||
.history-status.error {
|
||||
background: rgba(161, 47, 47, 0.12);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.hero,
|
||||
.dashboard-grid,
|
||||
.live-status-grid,
|
||||
.workspace-grid,
|
||||
.results-grid,
|
||||
.login-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.catalog-list {
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.wrap {
|
||||
width: min(100% - 20px, 1360px);
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.hero,
|
||||
.hero-copy,
|
||||
.hero-tools,
|
||||
.login-card,
|
||||
.feature-card,
|
||||
.quick-card,
|
||||
.status-card,
|
||||
.form-card,
|
||||
.catalog-card,
|
||||
.result-card,
|
||||
.history-card,
|
||||
.message-bar {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.stats-grid,
|
||||
.live-metrics,
|
||||
.status-columns,
|
||||
.quick-actions-grid,
|
||||
.base-grid,
|
||||
.advanced-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
/* W3.CSS 5.01 March 11 2026 by Jan Egil and Borge Refsnes */
|
||||
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
||||
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
||||
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
|
||||
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
|
||||
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
|
||||
a{background-color:transparent}a:active,a:hover{outline-width:0}
|
||||
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
|
||||
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
|
||||
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
|
||||
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
|
||||
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
|
||||
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
|
||||
button,input{overflow:visible}button,select{text-transform:none}
|
||||
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
|
||||
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
|
||||
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
|
||||
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
|
||||
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
|
||||
[type=checkbox],[type=radio]{padding:0}
|
||||
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
|
||||
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
|
||||
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
|
||||
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
|
||||
/* End extract */
|
||||
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
|
||||
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
|
||||
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
|
||||
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
|
||||
hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
||||
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
|
||||
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
|
||||
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
|
||||
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
|
||||
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
|
||||
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
|
||||
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
|
||||
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
|
||||
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
|
||||
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
|
||||
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
|
||||
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
|
||||
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
|
||||
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
|
||||
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
|
||||
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
|
||||
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
|
||||
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
|
||||
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
|
||||
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
|
||||
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
|
||||
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
|
||||
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
|
||||
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
|
||||
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
|
||||
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
|
||||
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
|
||||
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
|
||||
.w3-main,#main{transition:margin-left .4s}
|
||||
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
|
||||
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
|
||||
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
|
||||
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
|
||||
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
|
||||
.w3-bar .w3-button{white-space:normal}
|
||||
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
|
||||
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
|
||||
.w3-responsive{display:block;overflow-x:auto}
|
||||
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
|
||||
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
|
||||
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
|
||||
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
|
||||
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
|
||||
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
|
||||
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
|
||||
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
|
||||
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
|
||||
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
|
||||
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
|
||||
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
|
||||
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
|
||||
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
|
||||
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
|
||||
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
|
||||
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
|
||||
@media (max-width:1205px){.w3-auto{max-width:95%}}
|
||||
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
|
||||
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
|
||||
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
|
||||
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
|
||||
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
|
||||
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
|
||||
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
|
||||
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
|
||||
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
|
||||
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
|
||||
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
|
||||
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
|
||||
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
|
||||
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
|
||||
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
|
||||
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
|
||||
.w3-display-position{position:absolute}
|
||||
.w3-circle{border-radius:50%}
|
||||
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
||||
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
||||
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
||||
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
|
||||
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
|
||||
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
||||
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
||||
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
||||
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
|
||||
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
|
||||
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
|
||||
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
|
||||
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
|
||||
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
|
||||
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
|
||||
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
|
||||
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
|
||||
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
|
||||
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
|
||||
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
|
||||
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
|
||||
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
|
||||
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
|
||||
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
|
||||
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
|
||||
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
|
||||
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
|
||||
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
|
||||
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
|
||||
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
|
||||
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
|
||||
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
|
||||
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
|
||||
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
|
||||
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
|
||||
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
|
||||
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
|
||||
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
|
||||
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
|
||||
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
|
||||
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
|
||||
.w3-left{float:left!important}.w3-right{float:right!important}
|
||||
.w3-button:hover{color:#000!important;background-color:#ccc!important}
|
||||
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
||||
.w3-hover-none:hover{box-shadow:none!important}
|
||||
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
|
||||
/* Colors */
|
||||
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
|
||||
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
||||
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
|
||||
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
|
||||
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
|
||||
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
|
||||
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
|
||||
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
|
||||
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
|
||||
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
|
||||
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
|
||||
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
|
||||
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
|
||||
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
|
||||
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
|
||||
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
|
||||
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
|
||||
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
|
||||
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
|
||||
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
|
||||
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
|
||||
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
|
||||
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
|
||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
|
||||
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
||||
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
||||
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}
|
||||
.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
|
||||
.w3-cobalt,.w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
|
||||
.w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important}
|
||||
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
|
||||
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}
|
||||
.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
|
||||
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
|
||||
.w3-danger{color:#fff!important;background-color:#dd0000!important}
|
||||
.w3-note{color:#000!important;background-color:#fff599!important}
|
||||
.w3-info{color:#fff!important;background-color:#0a6fc2!important}
|
||||
.w3-warning{color:#000!important;background-color:#ffb305!important}
|
||||
.w3-success{color:#fff!important;background-color:#008a00!important}
|
||||
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
||||
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
||||
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
||||
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
|
||||
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
|
||||
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
|
||||
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
|
||||
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
|
||||
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
|
||||
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
|
||||
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
|
||||
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
|
||||
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
|
||||
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
|
||||
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
|
||||
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
|
||||
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
|
||||
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
|
||||
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
|
||||
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
|
||||
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
|
||||
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
|
||||
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
|
||||
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
|
||||
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
|
||||
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
|
||||
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
|
||||
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
|
||||
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
|
||||
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
|
||||
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
|
||||
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
|
||||
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
|
||||
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
|
||||
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
|
||||
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
|
||||
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
|
||||
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
|
||||
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
|
||||
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
|
||||
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
|
||||
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
|
||||
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
|
||||
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
|
||||
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
|
||||
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
|
||||
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
|
||||
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
|
||||
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
|
||||
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
|
||||
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
|
||||
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
|
||||
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
|
||||
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
|
||||
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
|
||||
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
|
||||
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
|
||||
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
||||
@@ -18,6 +18,19 @@ return [
|
||||
// Default workdir passed to osmtool_main.sh.
|
||||
'default_workdir' => '/opt',
|
||||
|
||||
// Optional dashboard overrides for live status cards.
|
||||
'status_ports' => [
|
||||
'grid-sim' => ['8002', '9000', '9001', '8088'],
|
||||
'robust' => ['8002', '9000', '9001'],
|
||||
'standalone' => ['9000'],
|
||||
],
|
||||
|
||||
'status_screens' => [
|
||||
'grid-sim' => ['robustserver', 'sim1'],
|
||||
'robust' => ['robustserver'],
|
||||
'standalone' => ['standalone'],
|
||||
],
|
||||
|
||||
// Allow running through sudo if needed.
|
||||
'use_sudo' => true,
|
||||
|
||||
|
||||
+360
-24
@@ -10,9 +10,12 @@ function translations(): array
|
||||
{
|
||||
return [
|
||||
'de' => [
|
||||
'title' => 'OSMTool Web Steuerung',
|
||||
'title' => 'OSMTool Webinterface',
|
||||
'subtitle' => 'Sichere Web-Oberflaeche fuer OpenSim Aktionen',
|
||||
'subtitle_full' => 'Zentrale Steuerung fuer Start, Pflege, Sicherung, Reports und Updates der modularen opensimMULTITOOLS-II Umgebung.',
|
||||
'hero_badge' => 'OpenSim Control Desk',
|
||||
'login_title' => 'Anmeldung',
|
||||
'login_intro' => 'Melden Sie sich an, um freigegebene OSMTool-Module ueber den Browser auszufuehren.',
|
||||
'password' => 'Passwort',
|
||||
'login' => 'Einloggen',
|
||||
'logout' => 'Ausloggen',
|
||||
@@ -26,21 +29,196 @@ function translations(): array
|
||||
'status_error' => 'Fehler',
|
||||
'invalid_login' => 'Anmeldung fehlgeschlagen.',
|
||||
'csrf_error' => 'Ungueltiges Formular-Token.',
|
||||
'not_allowed' => 'Aktion nicht erlaubt.',
|
||||
'run_failed' => 'Ausfuehrung fehlgeschlagen.',
|
||||
'hint' => 'Es werden nur freigegebene Aktionen ausgefuehrt.',
|
||||
'hint_full' => 'Nur whitelisted OSMTool-Befehle werden ueber den Runner gestartet. Erweiterte Parameter sind modulabhaengig sichtbar.',
|
||||
'action_start' => 'Start',
|
||||
'action_stop' => 'Stop',
|
||||
'action_restart' => 'Neustart',
|
||||
'action_smoke' => 'Smoke-Test',
|
||||
'action_report' => 'Report erzeugen',
|
||||
'action_report' => 'Report',
|
||||
'action_health' => 'Healthcheck',
|
||||
'action_cron_list' => 'Cronjobs anzeigen',
|
||||
'stat_modules' => 'Module',
|
||||
'stat_actions' => 'Aktionen',
|
||||
'stat_timeout' => 'Timeout',
|
||||
'stat_workdir' => 'Aktiver Pfad',
|
||||
'feature_title' => 'Funktionsumfang',
|
||||
'feature_modules' => 'Volle Modul-Auswahl fuer Betrieb, Pflege, Backup, Restore, Report und Update.',
|
||||
'feature_history' => 'Kurzer Ausfuehrungsverlauf pro Sitzung.',
|
||||
'feature_reports' => 'Befehlsvorschau und direkte Konsolenausgabe im Browser.',
|
||||
'feature_security' => 'Login, CSRF-Schutz, Action-Whitelist und Timeout-Steuerung.',
|
||||
'quick_actions' => 'Schnellaktionen',
|
||||
'quick_actions_hint' => 'Direkter Zugriff auf die am haeufigsten genutzten Betriebsbefehle.',
|
||||
'selected_action' => 'Aktuelle Auswahl',
|
||||
'form_title' => 'Befehl konfigurieren',
|
||||
'advanced_fields' => 'Erweiterte Parameter',
|
||||
'advanced_fields_hint' => 'Es werden nur die Felder gezeigt, die das ausgewaehlte Modul wirklich nutzt.',
|
||||
'field_module' => 'Modul',
|
||||
'catalog_title' => 'Modul-Katalog',
|
||||
'catalog_hint' => 'Uebersicht aller im Webinterface verfuegbaren Module und Aktionen.',
|
||||
'execution_result' => 'Ausfuehrung',
|
||||
'execution_result_hint' => 'Hier erscheinen Vorschau und Rueckgabe des zuletzt gestarteten Befehls.',
|
||||
'command_preview' => 'Kommando-Vorschau',
|
||||
'no_result_yet' => 'Noch keine Ausfuehrung in dieser Sitzung.',
|
||||
'history_title' => 'Verlauf',
|
||||
'history_hint' => 'Die letzten Web-Aktionen dieser Sitzung.',
|
||||
'history_empty' => 'Noch kein Verlauf vorhanden.',
|
||||
'live_status_title' => 'Live-Status',
|
||||
'live_status_hint' => 'Aktueller Snapshot von Runtime und Ports. Letzte Aktualisierung:',
|
||||
'live_screens' => 'Screen-Sessions',
|
||||
'live_ports' => 'Ports',
|
||||
'live_unavailable' => 'Statusdaten konnten auf diesem Host nicht gelesen werden.',
|
||||
'live_up' => 'aktiv',
|
||||
'live_down' => 'offline',
|
||||
'live_listen' => 'offen',
|
||||
'live_closed' => 'zu',
|
||||
'artifact_title' => 'Letzte Artefakte',
|
||||
'artifact_hint' => 'Die juengsten Backups, Reports und Rollback-Manifeste im Workdir.',
|
||||
'artifact_missing' => 'nicht gefunden',
|
||||
'module_install' => 'Installation',
|
||||
'module_install_desc' => 'Server vorbereiten, OpenSim installieren und Janus einrichten.',
|
||||
'module_startstop' => 'Start / Stop',
|
||||
'module_startstop_desc' => 'Grid, Robust, Regionen, Standalone oder Janus starten und stoppen.',
|
||||
'module_cleanup' => 'Bereinigung',
|
||||
'module_cleanup_desc' => 'Cache, Logs und temporaere Dateien kontrolliert entfernen.',
|
||||
'module_health' => 'Health',
|
||||
'module_health_desc' => 'Screens, Ports, Datenbank und Logs pruefen.',
|
||||
'module_backup' => 'Backup',
|
||||
'module_backup_desc' => 'SQL- und OAR-Backups ausloesen.',
|
||||
'module_restore' => 'Restore',
|
||||
'module_restore_desc' => 'Backups listen und SQL- oder OAR-Restore starten.',
|
||||
'module_update' => 'Update',
|
||||
'module_update_desc' => 'Rollback erstellen und OpenSim oder Janus aktualisieren.',
|
||||
'module_config' => 'Konfiguration',
|
||||
'module_config_desc' => 'Dateien oder komplette Runtime-Konfiguration validieren.',
|
||||
'module_report' => 'Report',
|
||||
'module_report_desc' => 'Statusreport fuer Runtime, Backups und Logs erzeugen.',
|
||||
'module_smoke' => 'Smoke',
|
||||
'module_smoke_desc' => 'Start/Restart/Stop als automatischen Rauchtest pruefen.',
|
||||
'module_cron' => 'Cron',
|
||||
'module_cron_desc' => 'OSMTool-Cronjobs installieren, anzeigen und entfernen.',
|
||||
'install_server_check' => 'Server-Check',
|
||||
'install_server_check_desc' => 'Prueft die Server-Bereitschaft und benoetigte Werkzeuge.',
|
||||
'install_prepare_ubuntu' => 'Ubuntu vorbereiten',
|
||||
'install_prepare_ubuntu_desc' => 'Fuehrt Basis-Updates und Paketvorbereitung aus.',
|
||||
'install_opensim_deps' => 'OpenSim Abhaengigkeiten',
|
||||
'install_opensim_deps_desc' => 'Installiert Laufzeit- und Build-Abhaengigkeiten fuer OpenSim.',
|
||||
'install_dotnet8' => 'Dotnet 8 installieren',
|
||||
'install_dotnet8_desc' => 'Installiert das .NET 8 SDK fuer Build und Runtime.',
|
||||
'install_opensim' => 'OpenSim installieren',
|
||||
'install_opensim_desc' => 'Klonen, aktualisieren, bauen und optional deployen.',
|
||||
'configure_opensim' => 'OpenSim konfigurieren',
|
||||
'configure_opensim_desc' => 'Erzeugt fehlende Runtime-INI aus Vorlagen.',
|
||||
'configure_database' => 'Datenbank konfigurieren',
|
||||
'configure_database_desc' => 'Legt Datenbanken, Benutzer und Rechte fuer die Runtime an.',
|
||||
'compile_janus' => 'Janus kompilieren',
|
||||
'compile_janus_desc' => 'Baut Janus Gateway aus dem Quellcode.',
|
||||
'configure_janus' => 'Janus konfigurieren',
|
||||
'configure_janus_desc' => 'Schreibt Janus-Konfiguration fuer OpenSim/WebRTC.',
|
||||
'install_janus' => 'Janus installieren',
|
||||
'install_janus_desc' => 'Kompiliert und konfiguriert Janus in einem Lauf.',
|
||||
'start_desc' => 'Startet das gewaehlte Ziel.',
|
||||
'stop_desc' => 'Stoppt das gewaehlte Ziel.',
|
||||
'restart_desc' => 'Startet das gewaehlte Ziel neu.',
|
||||
'cleanup_cache' => 'Cache bereinigen',
|
||||
'cleanup_cache_desc' => 'Entfernt Cache-, MapTiles- und Assetcache-Verzeichnisse.',
|
||||
'cleanup_logs' => 'Logs bereinigen',
|
||||
'cleanup_logs_desc' => 'Loescht Logdateien im Workdir.',
|
||||
'cleanup_tmp' => 'Temp bereinigen',
|
||||
'cleanup_tmp_desc' => 'Loescht temporaere Dateien im Workdir.',
|
||||
'cleanup_reboot' => 'Server neu starten',
|
||||
'cleanup_reboot_desc' => 'Startet den Host kontrolliert neu.',
|
||||
'health_run' => 'Healthcheck ausfuehren',
|
||||
'health_run_desc' => 'Prueft Screens, Ports, Datenbankzugriff und Log-Warnungen.',
|
||||
'backup_db' => 'DB-Backup',
|
||||
'backup_db_desc' => 'Erstellt ein SQL-Backup ueber mysqldump.',
|
||||
'backup_oar' => 'OAR-Backup',
|
||||
'backup_oar_desc' => 'Sendet save oar an eine laufende Region.',
|
||||
'backup_full' => 'Vollbackup',
|
||||
'backup_full_desc' => 'Kombiniert SQL-Backup und optional OAR-Backup.',
|
||||
'restore_list' => 'Backups anzeigen',
|
||||
'restore_list_desc' => 'Listet aktuelle SQL- und OAR-Backups.',
|
||||
'restore_db' => 'DB-Restore',
|
||||
'restore_db_desc' => 'Spielt ein SQL-Backup in die Datenbank ein.',
|
||||
'restore_oar' => 'OAR-Restore',
|
||||
'restore_oar_desc' => 'Sendet einen load oar Befehl an eine Region.',
|
||||
'restore_full' => 'Vollrestore',
|
||||
'restore_full_desc' => 'Leitet je nach Dateityp SQL- und OAR-Restore ein.',
|
||||
'update_create_rollback' => 'Rollback erstellen',
|
||||
'update_create_rollback_desc' => 'Sichert aktuelle OpenSim- und Janus-Pfade als Snapshot.',
|
||||
'update_list_rollbacks' => 'Rollbacks anzeigen',
|
||||
'update_list_rollbacks_desc' => 'Listet vorhandene Archive und Manifeste.',
|
||||
'update_opensim' => 'OpenSim aktualisieren',
|
||||
'update_opensim_desc' => 'Erstellt Snapshot und aktualisiert OpenSim.',
|
||||
'update_janus' => 'Janus aktualisieren',
|
||||
'update_janus_desc' => 'Erstellt Snapshot und aktualisiert Janus.',
|
||||
'update_full' => 'Vollupdate',
|
||||
'update_full_desc' => 'Aktualisiert OpenSim und Janus in Folge.',
|
||||
'config_validate_file' => 'Datei validieren',
|
||||
'config_validate_file_desc' => 'Prueft einzelne INI- oder JSON-Dateien.',
|
||||
'config_validate_runtime' => 'Runtime validieren',
|
||||
'config_validate_runtime_desc' => 'Prueft die komplette Runtime-Konfiguration des Profils.',
|
||||
'report_generate' => 'Report erzeugen',
|
||||
'report_generate_desc' => 'Schreibt einen Statusreport fuer Runtime, Backups und Logs.',
|
||||
'smoke_run' => 'Smoke-Test ausfuehren',
|
||||
'smoke_run_desc' => 'Prueft Start, Restart und Stop des ausgewaehlten Ziels.',
|
||||
'cron_install' => 'Cron installieren',
|
||||
'cron_install_desc' => 'Installiert OSMTool Cronjobs mit Zeitplaenen.',
|
||||
'cron_list' => 'Cron anzeigen',
|
||||
'cron_list_desc' => 'Zeigt den aktuell installierten OSMTool Cron-Block.',
|
||||
'cron_remove' => 'Cron entfernen',
|
||||
'cron_remove_desc' => 'Entfernt den OSMTool Cron-Block aus crontab.',
|
||||
'field_target' => 'Ziel',
|
||||
'field_name' => 'Region / Session-Name',
|
||||
'field_db_root_user' => 'DB Root Benutzer',
|
||||
'field_db_root_pass' => 'DB Root Passwort',
|
||||
'field_db_user' => 'DB Benutzer',
|
||||
'field_db_pass' => 'DB Passwort',
|
||||
'field_db_name' => 'DB Name',
|
||||
'field_region' => 'Region',
|
||||
'field_session' => 'Screen-Session',
|
||||
'field_compress' => 'Komprimieren',
|
||||
'field_file' => 'Dateipfad',
|
||||
'field_dry_run' => 'Dry Run',
|
||||
'field_strict' => 'Strenge Pruefung',
|
||||
'field_output_file' => 'Report-Datei',
|
||||
'field_expected_screens' => 'Erwartete Screens',
|
||||
'field_ports' => 'Ports',
|
||||
'field_log_file' => 'Log-Datei',
|
||||
'field_warning_threshold' => 'Warnschwelle',
|
||||
'field_backup_schedule' => 'Backup Zeitplan',
|
||||
'field_restart_schedule' => 'Restart Zeitplan',
|
||||
'field_check_schedule' => 'Check Zeitplan',
|
||||
'field_report_schedule' => 'Report Zeitplan',
|
||||
'field_smoke_schedule' => 'Smoke Zeitplan',
|
||||
'field_opensim_dir' => 'OpenSim Verzeichnis',
|
||||
'field_opensim_repo' => 'OpenSim Repository',
|
||||
'field_opensim_branch' => 'OpenSim Branch',
|
||||
'field_repo_mode' => 'Repo Modus',
|
||||
'field_tsassets_dir' => 'TSAssets Verzeichnis',
|
||||
'field_currency_dir' => 'CurrencyServer Verzeichnis',
|
||||
'field_data_backup_dir' => 'Data Backup Verzeichnis',
|
||||
'field_deploy_binaries' => 'Binaries deployen',
|
||||
'field_legacy_patch_dir' => 'Patch Verzeichnis',
|
||||
'field_janus_prefix' => 'Janus Prefix',
|
||||
'field_janus_src' => 'Janus Source',
|
||||
'field_public_host' => 'Oeffentlicher Host',
|
||||
'field_http_port' => 'HTTP Port',
|
||||
'field_admin_port' => 'Admin Port',
|
||||
'field_rtp_range' => 'RTP Bereich',
|
||||
'field_enable_admin' => 'Admin API aktivieren',
|
||||
'field_api_secret' => 'API Secret',
|
||||
'field_admin_secret' => 'Admin Secret',
|
||||
'field_label' => 'Label',
|
||||
'field_wait_after_start' => 'Wartezeit nach Start',
|
||||
'field_wait_after_stop' => 'Wartezeit nach Stop',
|
||||
],
|
||||
'en' => [
|
||||
'title' => 'OSMTool Web Control',
|
||||
'title' => 'OSMTool Web Interface',
|
||||
'subtitle' => 'Secure web interface for OpenSim actions',
|
||||
'subtitle_full' => 'Central browser control for start, maintenance, backup, reporting and update tasks in the modular opensimMULTITOOLS-II stack.',
|
||||
'hero_badge' => 'OpenSim Control Desk',
|
||||
'login_title' => 'Login',
|
||||
'login_intro' => 'Sign in to run approved OSMTool modules from the browser.',
|
||||
'password' => 'Password',
|
||||
'login' => 'Sign in',
|
||||
'logout' => 'Sign out',
|
||||
@@ -54,16 +232,188 @@ function translations(): array
|
||||
'status_error' => 'Error',
|
||||
'invalid_login' => 'Login failed.',
|
||||
'csrf_error' => 'Invalid form token.',
|
||||
'not_allowed' => 'Action not allowed.',
|
||||
'run_failed' => 'Execution failed.',
|
||||
'hint' => 'Only whitelisted actions are executed.',
|
||||
'hint_full' => 'Only whitelisted OSMTool commands are started through the runner. Advanced parameters appear only when the selected module uses them.',
|
||||
'action_start' => 'Start',
|
||||
'action_stop' => 'Stop',
|
||||
'action_restart' => 'Restart',
|
||||
'action_smoke' => 'Smoke test',
|
||||
'action_report' => 'Generate report',
|
||||
'action_report' => 'Report',
|
||||
'action_health' => 'Health check',
|
||||
'action_cron_list' => 'List cron jobs',
|
||||
'stat_modules' => 'Modules',
|
||||
'stat_actions' => 'Actions',
|
||||
'stat_timeout' => 'Timeout',
|
||||
'stat_workdir' => 'Active path',
|
||||
'feature_title' => 'What this interface covers',
|
||||
'feature_modules' => 'Full module selection for operations, maintenance, backup, restore, reporting and updates.',
|
||||
'feature_history' => 'Short execution history per session.',
|
||||
'feature_reports' => 'Command preview and direct console output in the browser.',
|
||||
'feature_security' => 'Login, CSRF protection, action whitelist and timeout handling.',
|
||||
'quick_actions' => 'Quick actions',
|
||||
'quick_actions_hint' => 'Direct access to the most common operational commands.',
|
||||
'selected_action' => 'Current selection',
|
||||
'form_title' => 'Configure command',
|
||||
'advanced_fields' => 'Advanced parameters',
|
||||
'advanced_fields_hint' => 'Only fields required by the selected module/action are shown.',
|
||||
'field_module' => 'Module',
|
||||
'catalog_title' => 'Module catalog',
|
||||
'catalog_hint' => 'Overview of all modules and actions currently available in the web UI.',
|
||||
'execution_result' => 'Execution',
|
||||
'execution_result_hint' => 'Command preview and result output of the last run appear here.',
|
||||
'command_preview' => 'Command preview',
|
||||
'no_result_yet' => 'No execution in this session yet.',
|
||||
'history_title' => 'History',
|
||||
'history_hint' => 'Most recent web actions from this session.',
|
||||
'history_empty' => 'No history recorded yet.',
|
||||
'live_status_title' => 'Live status',
|
||||
'live_status_hint' => 'Current snapshot of runtime and ports. Last refresh:',
|
||||
'live_screens' => 'Screen sessions',
|
||||
'live_ports' => 'Ports',
|
||||
'live_unavailable' => 'Status data could not be collected on this host.',
|
||||
'live_up' => 'up',
|
||||
'live_down' => 'down',
|
||||
'live_listen' => 'listening',
|
||||
'live_closed' => 'closed',
|
||||
'artifact_title' => 'Latest artifacts',
|
||||
'artifact_hint' => 'Most recent backups, reports and rollback manifests in the workdir.',
|
||||
'artifact_missing' => 'not found',
|
||||
'module_install' => 'Installation',
|
||||
'module_install_desc' => 'Prepare the server, install OpenSim and set up Janus.',
|
||||
'module_startstop' => 'Start / Stop',
|
||||
'module_startstop_desc' => 'Start or stop grid, robust, regions, standalone or Janus.',
|
||||
'module_cleanup' => 'Cleanup',
|
||||
'module_cleanup_desc' => 'Remove cache, logs and temporary files in a controlled way.',
|
||||
'module_health' => 'Health',
|
||||
'module_health_desc' => 'Check screens, ports, database access and log warnings.',
|
||||
'module_backup' => 'Backup',
|
||||
'module_backup_desc' => 'Run SQL and OAR backups.',
|
||||
'module_restore' => 'Restore',
|
||||
'module_restore_desc' => 'List backups and trigger SQL or OAR restore actions.',
|
||||
'module_update' => 'Update',
|
||||
'module_update_desc' => 'Create rollbacks and update OpenSim or Janus.',
|
||||
'module_config' => 'Configuration',
|
||||
'module_config_desc' => 'Validate files or the full runtime configuration.',
|
||||
'module_report' => 'Report',
|
||||
'module_report_desc' => 'Generate runtime, backup and log reports.',
|
||||
'module_smoke' => 'Smoke',
|
||||
'module_smoke_desc' => 'Run automatic start/restart/stop smoke checks.',
|
||||
'module_cron' => 'Cron',
|
||||
'module_cron_desc' => 'Install, list and remove OSMTool cron jobs.',
|
||||
'install_server_check' => 'Server check',
|
||||
'install_server_check_desc' => 'Validate server readiness and required tools.',
|
||||
'install_prepare_ubuntu' => 'Prepare Ubuntu',
|
||||
'install_prepare_ubuntu_desc' => 'Run base system update and package preparation.',
|
||||
'install_opensim_deps' => 'OpenSim dependencies',
|
||||
'install_opensim_deps_desc' => 'Install runtime and build dependencies for OpenSim.',
|
||||
'install_dotnet8' => 'Install Dotnet 8',
|
||||
'install_dotnet8_desc' => 'Install the .NET 8 SDK for build and runtime.',
|
||||
'install_opensim' => 'Install OpenSim',
|
||||
'install_opensim_desc' => 'Clone, update, build and optionally deploy OpenSim.',
|
||||
'configure_opensim' => 'Configure OpenSim',
|
||||
'configure_opensim_desc' => 'Generate missing runtime ini files from templates.',
|
||||
'configure_database' => 'Configure database',
|
||||
'configure_database_desc' => 'Create databases, users and grants for the runtime.',
|
||||
'compile_janus' => 'Compile Janus',
|
||||
'compile_janus_desc' => 'Build Janus Gateway from source.',
|
||||
'configure_janus' => 'Configure Janus',
|
||||
'configure_janus_desc' => 'Write Janus configuration for OpenSim/WebRTC usage.',
|
||||
'install_janus' => 'Install Janus',
|
||||
'install_janus_desc' => 'Compile and configure Janus in one run.',
|
||||
'start_desc' => 'Start the selected target.',
|
||||
'stop_desc' => 'Stop the selected target.',
|
||||
'restart_desc' => 'Restart the selected target.',
|
||||
'cleanup_cache' => 'Clean cache',
|
||||
'cleanup_cache_desc' => 'Remove cache, MapTiles and asset cache directories.',
|
||||
'cleanup_logs' => 'Clean logs',
|
||||
'cleanup_logs_desc' => 'Delete log files below the workdir.',
|
||||
'cleanup_tmp' => 'Clean temp files',
|
||||
'cleanup_tmp_desc' => 'Delete temporary files below the workdir.',
|
||||
'cleanup_reboot' => 'Reboot server',
|
||||
'cleanup_reboot_desc' => 'Restart the host system in a controlled way.',
|
||||
'health_run' => 'Run health check',
|
||||
'health_run_desc' => 'Check screens, ports, database connectivity and log warnings.',
|
||||
'backup_db' => 'DB backup',
|
||||
'backup_db_desc' => 'Create a SQL backup using mysqldump.',
|
||||
'backup_oar' => 'OAR backup',
|
||||
'backup_oar_desc' => 'Send save oar to a running region session.',
|
||||
'backup_full' => 'Full backup',
|
||||
'backup_full_desc' => 'Combine SQL backup and optional OAR backup.',
|
||||
'restore_list' => 'List backups',
|
||||
'restore_list_desc' => 'Show current SQL and OAR backups.',
|
||||
'restore_db' => 'DB restore',
|
||||
'restore_db_desc' => 'Restore a SQL backup into the database.',
|
||||
'restore_oar' => 'OAR restore',
|
||||
'restore_oar_desc' => 'Send a load oar command to a region.',
|
||||
'restore_full' => 'Full restore',
|
||||
'restore_full_desc' => 'Route SQL and OAR restore based on the file type.',
|
||||
'update_create_rollback' => 'Create rollback',
|
||||
'update_create_rollback_desc' => 'Snapshot current OpenSim and Janus paths.',
|
||||
'update_list_rollbacks' => 'List rollbacks',
|
||||
'update_list_rollbacks_desc' => 'Show available rollback archives and manifests.',
|
||||
'update_opensim' => 'Update OpenSim',
|
||||
'update_opensim_desc' => 'Create snapshot and update OpenSim.',
|
||||
'update_janus' => 'Update Janus',
|
||||
'update_janus_desc' => 'Create snapshot and update Janus.',
|
||||
'update_full' => 'Full update',
|
||||
'update_full_desc' => 'Update OpenSim and Janus in sequence.',
|
||||
'config_validate_file' => 'Validate file',
|
||||
'config_validate_file_desc' => 'Check individual INI or JSON files.',
|
||||
'config_validate_runtime' => 'Validate runtime',
|
||||
'config_validate_runtime_desc' => 'Check the full runtime configuration for the selected profile.',
|
||||
'report_generate' => 'Generate report',
|
||||
'report_generate_desc' => 'Write a status report for runtime, backups and logs.',
|
||||
'smoke_run' => 'Run smoke test',
|
||||
'smoke_run_desc' => 'Check start, restart and stop for the selected target.',
|
||||
'cron_install' => 'Install cron',
|
||||
'cron_install_desc' => 'Install OSMTool cron jobs with schedules.',
|
||||
'cron_list' => 'List cron',
|
||||
'cron_list_desc' => 'Show the installed OSMTool cron block.',
|
||||
'cron_remove' => 'Remove cron',
|
||||
'cron_remove_desc' => 'Remove the OSMTool cron block from crontab.',
|
||||
'field_target' => 'Target',
|
||||
'field_name' => 'Region / session name',
|
||||
'field_db_root_user' => 'DB root user',
|
||||
'field_db_root_pass' => 'DB root password',
|
||||
'field_db_user' => 'DB user',
|
||||
'field_db_pass' => 'DB password',
|
||||
'field_db_name' => 'DB name',
|
||||
'field_region' => 'Region',
|
||||
'field_session' => 'Screen session',
|
||||
'field_compress' => 'Compress',
|
||||
'field_file' => 'File path',
|
||||
'field_dry_run' => 'Dry run',
|
||||
'field_strict' => 'Strict validation',
|
||||
'field_output_file' => 'Report file',
|
||||
'field_expected_screens' => 'Expected screens',
|
||||
'field_ports' => 'Ports',
|
||||
'field_log_file' => 'Log file',
|
||||
'field_warning_threshold' => 'Warning threshold',
|
||||
'field_backup_schedule' => 'Backup schedule',
|
||||
'field_restart_schedule' => 'Restart schedule',
|
||||
'field_check_schedule' => 'Check schedule',
|
||||
'field_report_schedule' => 'Report schedule',
|
||||
'field_smoke_schedule' => 'Smoke schedule',
|
||||
'field_opensim_dir' => 'OpenSim directory',
|
||||
'field_opensim_repo' => 'OpenSim repository',
|
||||
'field_opensim_branch' => 'OpenSim branch',
|
||||
'field_repo_mode' => 'Repository mode',
|
||||
'field_tsassets_dir' => 'TSAssets directory',
|
||||
'field_currency_dir' => 'Currency server directory',
|
||||
'field_data_backup_dir' => 'Data backup directory',
|
||||
'field_deploy_binaries' => 'Deploy binaries',
|
||||
'field_legacy_patch_dir' => 'Patch directory',
|
||||
'field_janus_prefix' => 'Janus prefix',
|
||||
'field_janus_src' => 'Janus source',
|
||||
'field_public_host' => 'Public host',
|
||||
'field_http_port' => 'HTTP port',
|
||||
'field_admin_port' => 'Admin port',
|
||||
'field_rtp_range' => 'RTP range',
|
||||
'field_enable_admin' => 'Enable admin API',
|
||||
'field_api_secret' => 'API secret',
|
||||
'field_admin_secret' => 'Admin secret',
|
||||
'field_label' => 'Label',
|
||||
'field_wait_after_start' => 'Wait after start',
|
||||
'field_wait_after_stop' => 'Wait after stop',
|
||||
],
|
||||
'fr' => [
|
||||
'title' => 'Controle Web OSMTool',
|
||||
@@ -82,16 +432,9 @@ function translations(): array
|
||||
'status_error' => 'Erreur',
|
||||
'invalid_login' => 'Echec de connexion.',
|
||||
'csrf_error' => 'Jeton de formulaire invalide.',
|
||||
'not_allowed' => 'Action non autorisee.',
|
||||
'run_failed' => 'Execution echouee.',
|
||||
'hint' => 'Seules les actions en liste blanche sont executees.',
|
||||
'action_start' => 'Demarrer',
|
||||
'action_stop' => 'Arreter',
|
||||
'action_restart' => 'Redemarrer',
|
||||
'action_smoke' => 'Test smoke',
|
||||
'action_report' => 'Generer un rapport',
|
||||
'action_health' => 'Controle de sante',
|
||||
'action_cron_list' => 'Lister les cron jobs',
|
||||
],
|
||||
'es' => [
|
||||
'title' => 'Control Web OSMTool',
|
||||
@@ -110,16 +453,9 @@ function translations(): array
|
||||
'status_error' => 'Error',
|
||||
'invalid_login' => 'Error de inicio de sesion.',
|
||||
'csrf_error' => 'Token de formulario invalido.',
|
||||
'not_allowed' => 'Accion no permitida.',
|
||||
'run_failed' => 'La ejecucion fallo.',
|
||||
'hint' => 'Solo se ejecutan acciones permitidas.',
|
||||
'action_start' => 'Iniciar',
|
||||
'action_stop' => 'Detener',
|
||||
'action_restart' => 'Reiniciar',
|
||||
'action_smoke' => 'Prueba smoke',
|
||||
'action_report' => 'Generar informe',
|
||||
'action_health' => 'Chequeo de salud',
|
||||
'action_cron_list' => 'Listar cron jobs',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
+587
-26
@@ -6,12 +6,533 @@ function allowed_profiles(): array
|
||||
return ['grid-sim', 'robust', 'standalone'];
|
||||
}
|
||||
|
||||
function allowed_actions(): array
|
||||
function allowed_targets_by_profile(): array
|
||||
{
|
||||
return ['start', 'stop', 'restart', 'smoke', 'report', 'health', 'cron-list'];
|
||||
return [
|
||||
'grid-sim' => ['grid', 'robust', 'sim1', 'region', 'janus'],
|
||||
'robust' => ['robust', 'janus'],
|
||||
'standalone' => ['standalone', 'janus'],
|
||||
];
|
||||
}
|
||||
|
||||
function build_command(array $config, string $action, string $profile, string $lang, string $workdir): array
|
||||
function web_module_catalog(): array
|
||||
{
|
||||
return [
|
||||
'install' => [
|
||||
'label_key' => 'module_install',
|
||||
'description_key' => 'module_install_desc',
|
||||
'actions' => [
|
||||
'server-check' => ['label_key' => 'install_server_check', 'description_key' => 'install_server_check_desc', 'fields' => []],
|
||||
'prepare-ubuntu' => ['label_key' => 'install_prepare_ubuntu', 'description_key' => 'install_prepare_ubuntu_desc', 'fields' => []],
|
||||
'install-opensim-deps' => ['label_key' => 'install_opensim_deps', 'description_key' => 'install_opensim_deps_desc', 'fields' => []],
|
||||
'install-dotnet8' => ['label_key' => 'install_dotnet8', 'description_key' => 'install_dotnet8_desc', 'fields' => []],
|
||||
'install-opensim' => ['label_key' => 'install_opensim', 'description_key' => 'install_opensim_desc', 'fields' => ['opensim_dir', 'opensim_repo', 'opensim_branch', 'repo_mode', 'tsassets_dir', 'currency_dir', 'data_backup_dir', 'deploy_binaries', 'legacy_patch_dir']],
|
||||
'configure-opensim' => ['label_key' => 'configure_opensim', 'description_key' => 'configure_opensim_desc', 'fields' => []],
|
||||
'configure-database' => ['label_key' => 'configure_database', 'description_key' => 'configure_database_desc', 'fields' => ['db_root_user', 'db_root_pass', 'db_user', 'db_pass']],
|
||||
'compile-janus' => ['label_key' => 'compile_janus', 'description_key' => 'compile_janus_desc', 'fields' => ['janus_prefix', 'janus_src']],
|
||||
'configure-janus' => ['label_key' => 'configure_janus', 'description_key' => 'configure_janus_desc', 'fields' => ['janus_prefix', 'public_host', 'http_port', 'admin_port', 'rtp_range', 'enable_admin', 'api_secret', 'admin_secret']],
|
||||
'install-janus' => ['label_key' => 'install_janus', 'description_key' => 'install_janus_desc', 'fields' => ['janus_prefix', 'janus_src', 'public_host', 'http_port', 'admin_port', 'rtp_range', 'enable_admin', 'api_secret', 'admin_secret']],
|
||||
],
|
||||
],
|
||||
'startstop' => [
|
||||
'label_key' => 'module_startstop',
|
||||
'description_key' => 'module_startstop_desc',
|
||||
'actions' => [
|
||||
'start' => ['label_key' => 'action_start', 'description_key' => 'start_desc', 'fields' => ['target', 'name', 'janus_prefix']],
|
||||
'stop' => ['label_key' => 'action_stop', 'description_key' => 'stop_desc', 'fields' => ['target', 'name', 'janus_prefix']],
|
||||
'restart' => ['label_key' => 'action_restart', 'description_key' => 'restart_desc', 'fields' => ['target', 'name', 'janus_prefix']],
|
||||
],
|
||||
],
|
||||
'cleanup' => [
|
||||
'label_key' => 'module_cleanup',
|
||||
'description_key' => 'module_cleanup_desc',
|
||||
'actions' => [
|
||||
'cache-clean' => ['label_key' => 'cleanup_cache', 'description_key' => 'cleanup_cache_desc', 'fields' => ['dry_run']],
|
||||
'log-clean' => ['label_key' => 'cleanup_logs', 'description_key' => 'cleanup_logs_desc', 'fields' => ['dry_run']],
|
||||
'tmp-clean' => ['label_key' => 'cleanup_tmp', 'description_key' => 'cleanup_tmp_desc', 'fields' => ['dry_run']],
|
||||
'reboot' => ['label_key' => 'cleanup_reboot', 'description_key' => 'cleanup_reboot_desc', 'fields' => []],
|
||||
],
|
||||
],
|
||||
'health' => [
|
||||
'label_key' => 'module_health',
|
||||
'description_key' => 'module_health_desc',
|
||||
'actions' => [
|
||||
'run' => ['label_key' => 'health_run', 'description_key' => 'health_run_desc', 'fields' => ['expected_screens', 'ports', 'db_user', 'db_pass', 'db_name', 'log_file', 'warning_threshold']],
|
||||
],
|
||||
],
|
||||
'backup' => [
|
||||
'label_key' => 'module_backup',
|
||||
'description_key' => 'module_backup_desc',
|
||||
'actions' => [
|
||||
'db-backup' => ['label_key' => 'backup_db', 'description_key' => 'backup_db_desc', 'fields' => ['db_user', 'db_pass', 'db_name', 'compress'], 'profiles' => ['grid-sim', 'robust']],
|
||||
'oar-backup' => ['label_key' => 'backup_oar', 'description_key' => 'backup_oar_desc', 'fields' => ['region', 'session'], 'profiles' => ['grid-sim', 'standalone']],
|
||||
'full-backup' => ['label_key' => 'backup_full', 'description_key' => 'backup_full_desc', 'fields' => ['db_user', 'db_pass', 'db_name', 'region', 'session', 'compress']],
|
||||
],
|
||||
],
|
||||
'restore' => [
|
||||
'label_key' => 'module_restore',
|
||||
'description_key' => 'module_restore_desc',
|
||||
'actions' => [
|
||||
'list-backups' => ['label_key' => 'restore_list', 'description_key' => 'restore_list_desc', 'fields' => []],
|
||||
'db-restore' => ['label_key' => 'restore_db', 'description_key' => 'restore_db_desc', 'fields' => ['file', 'db_user', 'db_pass', 'db_name', 'dry_run'], 'profiles' => ['grid-sim', 'robust']],
|
||||
'oar-restore' => ['label_key' => 'restore_oar', 'description_key' => 'restore_oar_desc', 'fields' => ['file', 'region', 'session', 'dry_run'], 'profiles' => ['grid-sim', 'standalone']],
|
||||
'full-restore' => ['label_key' => 'restore_full', 'description_key' => 'restore_full_desc', 'fields' => ['file', 'region', 'session', 'db_user', 'db_pass', 'db_name', 'dry_run']],
|
||||
],
|
||||
],
|
||||
'update' => [
|
||||
'label_key' => 'module_update',
|
||||
'description_key' => 'module_update_desc',
|
||||
'actions' => [
|
||||
'create-rollback' => ['label_key' => 'update_create_rollback', 'description_key' => 'update_create_rollback_desc', 'fields' => ['label', 'dry_run']],
|
||||
'list-rollbacks' => ['label_key' => 'update_list_rollbacks', 'description_key' => 'update_list_rollbacks_desc', 'fields' => []],
|
||||
'update-opensim' => ['label_key' => 'update_opensim', 'description_key' => 'update_opensim_desc', 'fields' => ['label', 'dry_run', 'opensim_dir', 'opensim_repo', 'opensim_branch', 'repo_mode', 'tsassets_dir', 'currency_dir', 'data_backup_dir', 'deploy_binaries', 'legacy_patch_dir']],
|
||||
'update-janus' => ['label_key' => 'update_janus', 'description_key' => 'update_janus_desc', 'fields' => ['label', 'dry_run', 'janus_prefix', 'janus_src', 'public_host', 'http_port', 'admin_port', 'rtp_range', 'enable_admin', 'api_secret', 'admin_secret']],
|
||||
'full-update' => ['label_key' => 'update_full', 'description_key' => 'update_full_desc', 'fields' => ['label', 'dry_run', 'opensim_dir', 'opensim_repo', 'opensim_branch', 'repo_mode', 'tsassets_dir', 'currency_dir', 'data_backup_dir', 'deploy_binaries', 'legacy_patch_dir', 'janus_prefix', 'janus_src', 'public_host', 'http_port', 'admin_port', 'rtp_range', 'enable_admin', 'api_secret', 'admin_secret']],
|
||||
],
|
||||
],
|
||||
'config' => [
|
||||
'label_key' => 'module_config',
|
||||
'description_key' => 'module_config_desc',
|
||||
'actions' => [
|
||||
'validate-file' => ['label_key' => 'config_validate_file', 'description_key' => 'config_validate_file_desc', 'fields' => ['file', 'strict']],
|
||||
'validate-runtime' => ['label_key' => 'config_validate_runtime', 'description_key' => 'config_validate_runtime_desc', 'fields' => ['strict']],
|
||||
],
|
||||
],
|
||||
'report' => [
|
||||
'label_key' => 'module_report',
|
||||
'description_key' => 'module_report_desc',
|
||||
'actions' => [
|
||||
'generate' => ['label_key' => 'report_generate', 'description_key' => 'report_generate_desc', 'fields' => ['output_file', 'expected_screens', 'ports', 'db_user', 'db_pass', 'db_name', 'log_file']],
|
||||
],
|
||||
],
|
||||
'smoke' => [
|
||||
'label_key' => 'module_smoke',
|
||||
'description_key' => 'module_smoke_desc',
|
||||
'actions' => [
|
||||
'run' => ['label_key' => 'smoke_run', 'description_key' => 'smoke_run_desc', 'fields' => ['target', 'name', 'expected_screens', 'wait_after_start', 'wait_after_stop', 'janus_prefix']],
|
||||
],
|
||||
],
|
||||
'cron' => [
|
||||
'label_key' => 'module_cron',
|
||||
'description_key' => 'module_cron_desc',
|
||||
'actions' => [
|
||||
'install' => ['label_key' => 'cron_install', 'description_key' => 'cron_install_desc', 'fields' => ['backup_schedule', 'restart_schedule', 'check_schedule', 'report_schedule', 'smoke_schedule', 'db_user', 'db_pass', 'db_name', 'region', 'session', 'compress']],
|
||||
'list' => ['label_key' => 'cron_list', 'description_key' => 'cron_list_desc', 'fields' => []],
|
||||
'remove' => ['label_key' => 'cron_remove', 'description_key' => 'cron_remove_desc', 'fields' => []],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function web_field_definitions(array $config): array
|
||||
{
|
||||
return [
|
||||
'target' => ['flag' => '--target', 'type' => 'select', 'label_key' => 'field_target', 'target_by_profile' => allowed_targets_by_profile()],
|
||||
'name' => ['flag' => '--name', 'type' => 'text', 'label_key' => 'field_name', 'placeholder' => 'sim2'],
|
||||
'db_root_user' => ['flag' => '--db-root-user', 'type' => 'text', 'label_key' => 'field_db_root_user', 'placeholder' => 'root'],
|
||||
'db_root_pass' => ['flag' => '--db-root-pass', 'type' => 'password', 'label_key' => 'field_db_root_pass'],
|
||||
'db_user' => ['flag' => '--db-user', 'type' => 'text', 'label_key' => 'field_db_user', 'placeholder' => 'opensim'],
|
||||
'db_pass' => ['flag' => '--db-pass', 'type' => 'password', 'label_key' => 'field_db_pass'],
|
||||
'db_name' => ['flag' => '--db-name', 'type' => 'text', 'label_key' => 'field_db_name', 'placeholder' => 'opensim'],
|
||||
'region' => ['flag' => '--region', 'type' => 'text', 'label_key' => 'field_region', 'placeholder' => 'sim1'],
|
||||
'session' => ['flag' => '--session', 'type' => 'text', 'label_key' => 'field_session', 'placeholder' => 'sim1'],
|
||||
'compress' => ['flag' => '--compress', 'type' => 'select', 'label_key' => 'field_compress', 'options' => ['true', 'false'], 'default' => 'true'],
|
||||
'file' => ['flag' => '--file', 'type' => 'text', 'label_key' => 'field_file', 'placeholder' => '/opt/backup/file.sql.gz'],
|
||||
'dry_run' => ['flag' => '--dry-run', 'type' => 'select', 'label_key' => 'field_dry_run', 'options' => ['false', 'true'], 'default' => 'false'],
|
||||
'strict' => ['flag' => '--strict', 'type' => 'select', 'label_key' => 'field_strict', 'options' => ['false', 'true'], 'default' => 'false'],
|
||||
'output_file' => ['flag' => '--output', 'type' => 'text', 'label_key' => 'field_output_file', 'placeholder' => '/opt/reports/manual_report.txt'],
|
||||
'expected_screens' => ['flag' => '--expected-screens', 'type' => 'text', 'label_key' => 'field_expected_screens', 'placeholder' => 'robustserver,sim1'],
|
||||
'ports' => ['flag' => '--ports', 'type' => 'text', 'label_key' => 'field_ports', 'placeholder' => '8002,9000,9001,8088'],
|
||||
'log_file' => ['flag' => '--log-file', 'type' => 'text', 'label_key' => 'field_log_file', 'placeholder' => '/opt/robust/bin/OpenSim.log'],
|
||||
'warning_threshold' => ['flag' => '--warning-threshold', 'type' => 'text', 'label_key' => 'field_warning_threshold', 'placeholder' => '5'],
|
||||
'backup_schedule' => ['flag' => '--backup-schedule', 'type' => 'text', 'label_key' => 'field_backup_schedule', 'placeholder' => '0 3 * * *'],
|
||||
'restart_schedule' => ['flag' => '--restart-schedule', 'type' => 'text', 'label_key' => 'field_restart_schedule', 'placeholder' => '0 5 * * 1'],
|
||||
'check_schedule' => ['flag' => '--check-schedule', 'type' => 'text', 'label_key' => 'field_check_schedule', 'placeholder' => '*/15 * * * *'],
|
||||
'report_schedule' => ['flag' => '--report-schedule', 'type' => 'text', 'label_key' => 'field_report_schedule', 'placeholder' => '30 6 * * *'],
|
||||
'smoke_schedule' => ['flag' => '--smoke-schedule', 'type' => 'text', 'label_key' => 'field_smoke_schedule', 'placeholder' => '45 6 * * *'],
|
||||
'opensim_dir' => ['flag' => '--opensim-dir', 'type' => 'text', 'label_key' => 'field_opensim_dir', 'placeholder' => '/opt/opensim'],
|
||||
'opensim_repo' => ['flag' => '--opensim-repo', 'type' => 'text', 'label_key' => 'field_opensim_repo', 'placeholder' => 'https://github.com/opensim/opensim'],
|
||||
'opensim_branch' => ['flag' => '--opensim-branch', 'type' => 'text', 'label_key' => 'field_opensim_branch', 'placeholder' => 'master'],
|
||||
'repo_mode' => ['flag' => '--repo-mode', 'type' => 'select', 'label_key' => 'field_repo_mode', 'options' => ['update', 'fresh'], 'default' => 'update'],
|
||||
'tsassets_dir' => ['flag' => '--tsassets-dir', 'type' => 'text', 'label_key' => 'field_tsassets_dir', 'placeholder' => '/opt/opensim-tsassets'],
|
||||
'currency_dir' => ['flag' => '--currency-dir', 'type' => 'text', 'label_key' => 'field_currency_dir', 'placeholder' => '/opt/opensimcurrencyserver-dotnet'],
|
||||
'data_backup_dir' => ['flag' => '--data-backup-dir', 'type' => 'text', 'label_key' => 'field_data_backup_dir', 'placeholder' => '/opt/os-data-backup'],
|
||||
'deploy_binaries' => ['flag' => '--deploy-binaries', 'type' => 'select', 'label_key' => 'field_deploy_binaries', 'options' => ['true', 'false'], 'default' => 'true'],
|
||||
'legacy_patch_dir' => ['flag' => '--legacy-patch-dir', 'type' => 'text', 'label_key' => 'field_legacy_patch_dir', 'placeholder' => '/opt/patches'],
|
||||
'janus_prefix' => ['flag' => '--janus-prefix', 'type' => 'text', 'label_key' => 'field_janus_prefix', 'placeholder' => '/opt/janus'],
|
||||
'janus_src' => ['flag' => '--janus-src', 'type' => 'text', 'label_key' => 'field_janus_src', 'placeholder' => '/opt/src/janus-gateway'],
|
||||
'public_host' => ['flag' => '--public-host', 'type' => 'text', 'label_key' => 'field_public_host', 'placeholder' => '127.0.0.1'],
|
||||
'http_port' => ['flag' => '--http-port', 'type' => 'text', 'label_key' => 'field_http_port', 'placeholder' => '8088'],
|
||||
'admin_port' => ['flag' => '--admin-port', 'type' => 'text', 'label_key' => 'field_admin_port', 'placeholder' => '7088'],
|
||||
'rtp_range' => ['flag' => '--rtp-range', 'type' => 'text', 'label_key' => 'field_rtp_range', 'placeholder' => '10000-10200'],
|
||||
'enable_admin' => ['flag' => '--enable-admin', 'type' => 'select', 'label_key' => 'field_enable_admin', 'options' => ['false', 'true'], 'default' => 'false'],
|
||||
'api_secret' => ['flag' => '--api-secret', 'type' => 'password', 'label_key' => 'field_api_secret'],
|
||||
'admin_secret' => ['flag' => '--admin-secret', 'type' => 'password', 'label_key' => 'field_admin_secret'],
|
||||
'label' => ['flag' => '--label', 'type' => 'text', 'label_key' => 'field_label', 'placeholder' => 'pre_update'],
|
||||
'wait_after_start' => ['flag' => '--wait-after-start', 'type' => 'text', 'label_key' => 'field_wait_after_start', 'placeholder' => '3'],
|
||||
'wait_after_stop' => ['flag' => '--wait-after-stop', 'type' => 'text', 'label_key' => 'field_wait_after_stop', 'placeholder' => '8'],
|
||||
];
|
||||
}
|
||||
|
||||
function translated_catalog(string $lang, array $config): array
|
||||
{
|
||||
$catalog = web_module_catalog();
|
||||
foreach ($catalog as $module => &$moduleData) {
|
||||
$moduleData['label'] = t($lang, $moduleData['label_key']);
|
||||
$moduleData['description'] = t($lang, $moduleData['description_key']);
|
||||
foreach ($moduleData['actions'] as $action => &$actionData) {
|
||||
$actionData['label'] = t($lang, $actionData['label_key']);
|
||||
$actionData['description'] = t($lang, $actionData['description_key']);
|
||||
$actionData['module'] = $module;
|
||||
$actionData['action'] = $action;
|
||||
$actionData['profiles'] = $actionData['profiles'] ?? allowed_profiles();
|
||||
}
|
||||
unset($actionData);
|
||||
}
|
||||
unset($moduleData);
|
||||
|
||||
return $catalog;
|
||||
}
|
||||
|
||||
function default_target_for_profile(string $profile): string
|
||||
{
|
||||
return allowed_targets_by_profile()[$profile][0] ?? 'grid';
|
||||
}
|
||||
|
||||
function field_value_from_request(string $field, array $source, array $definitions): string
|
||||
{
|
||||
$value = $source[$field] ?? ($definitions[$field]['default'] ?? '');
|
||||
return is_string($value) ? trim($value) : '';
|
||||
}
|
||||
|
||||
function validate_simple_value(string $field, string $value, array $definition, string $profile): string
|
||||
{
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (str_contains($value, "\0") || str_contains($value, "\r") || str_contains($value, "\n")) {
|
||||
throw new RuntimeException('Invalid value for field: ' . $field);
|
||||
}
|
||||
|
||||
if (($definition['type'] ?? '') === 'select') {
|
||||
if ($field === 'target') {
|
||||
$allowed = $definition['target_by_profile'][$profile] ?? [];
|
||||
if (!in_array($value, $allowed, true)) {
|
||||
throw new RuntimeException('Invalid target selected.');
|
||||
}
|
||||
} elseif (!in_array($value, $definition['options'] ?? [], true)) {
|
||||
throw new RuntimeException('Invalid selected option for field: ' . $field);
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array($field, ['http_port', 'admin_port', 'warning_threshold', 'wait_after_start', 'wait_after_stop'], true) && !ctype_digit($value)) {
|
||||
throw new RuntimeException('Field requires a numeric value: ' . $field);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
function normalize_run_request(array $config, array $source, string $lang): array
|
||||
{
|
||||
$catalog = web_module_catalog();
|
||||
$fields = web_field_definitions($config);
|
||||
|
||||
$module = (string)($source['module'] ?? 'startstop');
|
||||
$profile = (string)($source['profile'] ?? 'grid-sim');
|
||||
$workdir = trim((string)($source['workdir'] ?? (string)($config['default_workdir'] ?? '/opt')));
|
||||
$action = (string)($source['action'] ?? 'start');
|
||||
|
||||
if (!isset($catalog[$module])) {
|
||||
throw new RuntimeException('Unknown module selected.');
|
||||
}
|
||||
if (!in_array($profile, allowed_profiles(), true)) {
|
||||
throw new RuntimeException('Unknown profile selected.');
|
||||
}
|
||||
if (!isset($catalog[$module]['actions'][$action])) {
|
||||
throw new RuntimeException('Unknown action selected.');
|
||||
}
|
||||
$allowedProfiles = $catalog[$module]['actions'][$action]['profiles'] ?? allowed_profiles();
|
||||
if (!in_array($profile, $allowedProfiles, true)) {
|
||||
throw new RuntimeException('Action not allowed for selected profile.');
|
||||
}
|
||||
if ($workdir === '') {
|
||||
$workdir = (string)($config['default_workdir'] ?? '/opt');
|
||||
}
|
||||
if (!in_array($lang, supported_languages(), true)) {
|
||||
throw new RuntimeException('Unsupported language.');
|
||||
}
|
||||
|
||||
$selectedFields = $catalog[$module]['actions'][$action]['fields'];
|
||||
$values = [];
|
||||
foreach ($selectedFields as $field) {
|
||||
$value = field_value_from_request($field, $source, $fields);
|
||||
$value = validate_simple_value($field, $value, $fields[$field], $profile);
|
||||
if ($value !== '') {
|
||||
$values[$field] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array('target', $selectedFields, true) && !isset($values['target'])) {
|
||||
$values['target'] = default_target_for_profile($profile);
|
||||
}
|
||||
|
||||
foreach (['file' => ['restore', 'config'], 'db_pass' => ['backup', 'restore', 'cron', 'health', 'report', 'install'], 'db_user' => ['backup', 'restore', 'cron', 'health', 'report', 'install'], 'db_name' => ['backup', 'restore', 'cron', 'health', 'report']] as $field => $moduleWhitelist) {
|
||||
if (in_array($module, $moduleWhitelist, true) && isset($source[$field]) && !isset($values[$field])) {
|
||||
$value = field_value_from_request($field, $source, $fields);
|
||||
if ($value !== '') {
|
||||
$values[$field] = validate_simple_value($field, $value, $fields[$field], $profile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'module' => $module,
|
||||
'action' => $action,
|
||||
'profile' => $profile,
|
||||
'lang' => $lang,
|
||||
'workdir' => $workdir,
|
||||
'fields' => $values,
|
||||
];
|
||||
}
|
||||
|
||||
function default_status_ports(string $profile): array
|
||||
{
|
||||
return match ($profile) {
|
||||
'grid-sim' => ['8002', '9000', '9001', '8088'],
|
||||
'robust' => ['8002', '9000', '9001'],
|
||||
'standalone' => ['9000'],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
function default_status_screens(string $profile, string $workdir): array
|
||||
{
|
||||
if ($profile === 'robust') {
|
||||
return ['robustserver'];
|
||||
}
|
||||
|
||||
if ($profile === 'standalone') {
|
||||
return ['standalone'];
|
||||
}
|
||||
|
||||
$screens = [];
|
||||
if (is_dir($workdir . '/robust/bin')) {
|
||||
$screens[] = 'robustserver';
|
||||
}
|
||||
|
||||
$simBins = glob(rtrim($workdir, '/') . '/sim*/bin/OpenSim.ini') ?: [];
|
||||
foreach ($simBins as $simIni) {
|
||||
$screens[] = basename(dirname(dirname($simIni)));
|
||||
}
|
||||
|
||||
if ($screens === []) {
|
||||
$screens = ['robustserver', 'sim1'];
|
||||
}
|
||||
|
||||
return array_values(array_unique($screens));
|
||||
}
|
||||
|
||||
function configured_status_ports(array $config, string $profile): array
|
||||
{
|
||||
$ports = $config['status_ports'][$profile] ?? null;
|
||||
if (!is_array($ports) || $ports === []) {
|
||||
return default_status_ports($profile);
|
||||
}
|
||||
|
||||
return array_values(array_filter(array_map(static fn ($value): string => trim((string)$value), $ports), static fn (string $value): bool => $value !== ''));
|
||||
}
|
||||
|
||||
function configured_status_screens(array $config, string $profile, string $workdir): array
|
||||
{
|
||||
$screens = $config['status_screens'][$profile] ?? null;
|
||||
if (!is_array($screens) || $screens === []) {
|
||||
return default_status_screens($profile, $workdir);
|
||||
}
|
||||
|
||||
return array_values(array_filter(array_map(static fn ($value): string => trim((string)$value), $screens), static fn (string $value): bool => $value !== ''));
|
||||
}
|
||||
|
||||
function run_status_command(array $command, int $timeoutSeconds = 3): array
|
||||
{
|
||||
$descriptorSpec = [
|
||||
0 => ['pipe', 'r'],
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w'],
|
||||
];
|
||||
|
||||
$process = @proc_open($command, $descriptorSpec, $pipes);
|
||||
if (!is_resource($process)) {
|
||||
return ['ok' => false, 'output' => '', 'error' => 'process-start-failed'];
|
||||
}
|
||||
|
||||
fclose($pipes[0]);
|
||||
stream_set_blocking($pipes[1], false);
|
||||
stream_set_blocking($pipes[2], false);
|
||||
|
||||
$stdout = '';
|
||||
$stderr = '';
|
||||
$deadline = microtime(true) + $timeoutSeconds;
|
||||
|
||||
while (true) {
|
||||
$stdout .= (string)stream_get_contents($pipes[1]);
|
||||
$stderr .= (string)stream_get_contents($pipes[2]);
|
||||
$status = proc_get_status($process);
|
||||
if (!($status['running'] ?? false)) {
|
||||
break;
|
||||
}
|
||||
if (microtime(true) >= $deadline) {
|
||||
proc_terminate($process);
|
||||
break;
|
||||
}
|
||||
usleep(100000);
|
||||
}
|
||||
|
||||
$stdout .= (string)stream_get_contents($pipes[1]);
|
||||
$stderr .= (string)stream_get_contents($pipes[2]);
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
$exitCode = proc_close($process);
|
||||
|
||||
return [
|
||||
'ok' => $exitCode === 0,
|
||||
'output' => trim($stdout),
|
||||
'error' => trim($stderr),
|
||||
];
|
||||
}
|
||||
|
||||
function collect_screen_status(array $screens): array
|
||||
{
|
||||
$result = run_status_command(['screen', '-list']);
|
||||
if ($result['ok'] !== true && $result['output'] === '') {
|
||||
return ['available' => false, 'items' => []];
|
||||
}
|
||||
|
||||
$output = $result['output'] . "\n" . $result['error'];
|
||||
$items = [];
|
||||
foreach ($screens as $screenName) {
|
||||
$items[] = [
|
||||
'name' => $screenName,
|
||||
'ok' => (bool)preg_match('/\.' . preg_quote($screenName, '/') . '[[:space:]]/i', $output),
|
||||
];
|
||||
}
|
||||
|
||||
return ['available' => true, 'items' => $items];
|
||||
}
|
||||
|
||||
function collect_port_status(array $ports): array
|
||||
{
|
||||
$result = run_status_command(['ss', '-ltnH']);
|
||||
if ($result['ok'] !== true && $result['output'] === '') {
|
||||
$result = run_status_command(['netstat', '-ltn']);
|
||||
}
|
||||
if ($result['ok'] !== true && $result['output'] === '') {
|
||||
return ['available' => false, 'items' => []];
|
||||
}
|
||||
|
||||
$items = [];
|
||||
foreach ($ports as $port) {
|
||||
$items[] = [
|
||||
'name' => $port,
|
||||
'ok' => (bool)preg_match('/[:.]' . preg_quote($port, '/') . '$/m', $result['output']),
|
||||
];
|
||||
}
|
||||
|
||||
return ['available' => true, 'items' => $items];
|
||||
}
|
||||
|
||||
function latest_matching_file(string $directory, string $pattern): ?string
|
||||
{
|
||||
$files = glob(rtrim($directory, '/') . '/' . $pattern) ?: [];
|
||||
if ($files === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
usort($files, static fn (string $left, string $right): int => filemtime($right) <=> filemtime($left));
|
||||
return $files[0] ?? null;
|
||||
}
|
||||
|
||||
function format_age(?string $path): string
|
||||
{
|
||||
if ($path === null || !is_file($path)) {
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
$ageSeconds = max(0, time() - (int)filemtime($path));
|
||||
if ($ageSeconds < 3600) {
|
||||
return (string)max(1, (int)floor($ageSeconds / 60)) . 'm';
|
||||
}
|
||||
if ($ageSeconds < 86400) {
|
||||
return (string)(int)floor($ageSeconds / 3600) . 'h';
|
||||
}
|
||||
|
||||
return (string)(int)floor($ageSeconds / 86400) . 'd';
|
||||
}
|
||||
|
||||
function summarize_status_items(array $items): array
|
||||
{
|
||||
$ok = 0;
|
||||
foreach ($items as $item) {
|
||||
if (($item['ok'] ?? false) === true) {
|
||||
$ok++;
|
||||
}
|
||||
}
|
||||
|
||||
return ['ok' => $ok, 'total' => count($items)];
|
||||
}
|
||||
|
||||
function collect_live_status(array $config, string $profile, string $workdir): array
|
||||
{
|
||||
$screens = configured_status_screens($config, $profile, $workdir);
|
||||
$ports = configured_status_ports($config, $profile);
|
||||
$backupDir = rtrim($workdir, '/') . '/backup';
|
||||
$reportDir = rtrim($workdir, '/') . '/reports';
|
||||
$rollbackDir = rtrim($workdir, '/') . '/rollback';
|
||||
|
||||
$screenStatus = collect_screen_status($screens);
|
||||
$portStatus = collect_port_status($ports);
|
||||
$latestSql = latest_matching_file($backupDir, 'robust_*.sql*');
|
||||
$latestOar = latest_matching_file($backupDir, '*.oar');
|
||||
$latestReport = latest_matching_file($reportDir, '*.txt');
|
||||
$latestRollback = latest_matching_file($rollbackDir, '*.manifest');
|
||||
|
||||
return [
|
||||
'generatedAt' => date('Y-m-d H:i:s'),
|
||||
'screens' => $screenStatus,
|
||||
'ports' => $portStatus,
|
||||
'screensSummary' => summarize_status_items($screenStatus['items']),
|
||||
'portsSummary' => summarize_status_items($portStatus['items']),
|
||||
'artifacts' => [
|
||||
['label' => 'SQL', 'path' => $latestSql, 'age' => format_age($latestSql)],
|
||||
['label' => 'OAR', 'path' => $latestOar, 'age' => format_age($latestOar)],
|
||||
['label' => 'Report', 'path' => $latestReport, 'age' => format_age($latestReport)],
|
||||
['label' => 'Rollback', 'path' => $latestRollback, 'age' => format_age($latestRollback)],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function build_command_preview(array $request): string
|
||||
{
|
||||
$parts = [
|
||||
'osmtool_main.sh',
|
||||
'--mode', 'cli',
|
||||
'--module', $request['module'],
|
||||
'--action', $request['action'],
|
||||
'--profile', $request['profile'],
|
||||
'--lang', $request['lang'],
|
||||
'--workdir', $request['workdir'],
|
||||
];
|
||||
|
||||
$fieldDefs = web_field_definitions([]);
|
||||
foreach ($request['fields'] as $field => $value) {
|
||||
$parts[] = $fieldDefs[$field]['flag'];
|
||||
$parts[] = $field === 'db_pass' || $field === 'db_root_pass' || $field === 'api_secret' || $field === 'admin_secret' ? '******' : $value;
|
||||
}
|
||||
|
||||
return implode(' ', array_map(static fn (string $item): string => preg_match('/^[a-zA-Z0-9._:\/\-*]+$/', $item) ? $item : '"' . addslashes($item) . '"', $parts));
|
||||
}
|
||||
|
||||
function build_command(array $config, array $request): array
|
||||
{
|
||||
$runnerPath = (string)($config['runner_path'] ?? '');
|
||||
if ($runnerPath === '') {
|
||||
@@ -28,29 +549,27 @@ function build_command(array $config, string $action, string $profile, string $l
|
||||
$base = ['sudo', '-n', '-u', $sudoUser];
|
||||
}
|
||||
|
||||
return array_merge($base, [$runnerPath, $action, $profile, $lang, $workdir]);
|
||||
$command = array_merge($base, [
|
||||
$runnerPath,
|
||||
$request['module'],
|
||||
$request['action'],
|
||||
$request['profile'],
|
||||
$request['lang'],
|
||||
$request['workdir'],
|
||||
]);
|
||||
|
||||
$fieldDefs = web_field_definitions($config);
|
||||
foreach ($request['fields'] as $field => $value) {
|
||||
$command[] = $fieldDefs[$field]['flag'];
|
||||
$command[] = $value;
|
||||
}
|
||||
|
||||
return $command;
|
||||
}
|
||||
|
||||
function run_whitelisted_action(array $config, string $action, string $profile, string $lang, string $workdir): array
|
||||
function run_whitelisted_action(array $config, array $request): array
|
||||
{
|
||||
if (!in_array($action, allowed_actions(), true)) {
|
||||
return ['ok' => false, 'exitCode' => 2, 'output' => "ERROR: action not allowed\n"];
|
||||
}
|
||||
|
||||
if (!in_array($profile, allowed_profiles(), true)) {
|
||||
return ['ok' => false, 'exitCode' => 2, 'output' => "ERROR: invalid profile\n"];
|
||||
}
|
||||
|
||||
if (!in_array($lang, supported_languages(), true)) {
|
||||
return ['ok' => false, 'exitCode' => 2, 'output' => "ERROR: invalid language\n"];
|
||||
}
|
||||
|
||||
$workdirClean = trim($workdir);
|
||||
if ($workdirClean === '') {
|
||||
$workdirClean = (string)($config['default_workdir'] ?? '/opt');
|
||||
}
|
||||
|
||||
$cmd = build_command($config, $action, $profile, $lang, $workdirClean);
|
||||
$cmd = build_command($config, $request);
|
||||
$descriptorSpec = [
|
||||
0 => ['pipe', 'r'],
|
||||
1 => ['pipe', 'w'],
|
||||
@@ -64,14 +583,56 @@ function run_whitelisted_action(array $config, string $action, string $profile,
|
||||
|
||||
fclose($pipes[0]);
|
||||
|
||||
$stdout = stream_get_contents($pipes[1]);
|
||||
$stderr = stream_get_contents($pipes[2]);
|
||||
stream_set_blocking($pipes[1], false);
|
||||
stream_set_blocking($pipes[2], false);
|
||||
|
||||
$stdout = '';
|
||||
$stderr = '';
|
||||
$timeout = max(5, (int)($config['command_timeout_seconds'] ?? 120));
|
||||
$deadline = microtime(true) + $timeout;
|
||||
$timedOut = false;
|
||||
|
||||
while (true) {
|
||||
$stdout .= (string)stream_get_contents($pipes[1]);
|
||||
$stderr .= (string)stream_get_contents($pipes[2]);
|
||||
|
||||
$status = proc_get_status($process);
|
||||
if (!($status['running'] ?? false)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (microtime(true) >= $deadline) {
|
||||
$timedOut = true;
|
||||
proc_terminate($process);
|
||||
usleep(200000);
|
||||
$status = proc_get_status($process);
|
||||
if (($status['running'] ?? false) === true) {
|
||||
proc_terminate($process, 9);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
usleep(100000);
|
||||
}
|
||||
|
||||
$stdout .= (string)stream_get_contents($pipes[1]);
|
||||
$stderr .= (string)stream_get_contents($pipes[2]);
|
||||
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
|
||||
$exitCode = proc_close($process);
|
||||
if ($timedOut) {
|
||||
$exitCode = 124;
|
||||
$stderr .= "\nERROR: command timed out after {$timeout} seconds\n";
|
||||
}
|
||||
$output = (string)$stdout . (string)$stderr;
|
||||
|
||||
return ['ok' => $exitCode === 0, 'exitCode' => $exitCode, 'output' => $output];
|
||||
return [
|
||||
'ok' => $exitCode === 0,
|
||||
'exitCode' => $exitCode,
|
||||
'output' => $output,
|
||||
'commandPreview' => build_command_preview($request),
|
||||
'timedOut' => $timedOut,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$configFile = __DIR__ . '/config.php';
|
||||
if (!is_file($configFile)) {
|
||||
$configFile = __DIR__ . '/config.sample.php';
|
||||
}
|
||||
|
||||
$config = require $configFile;
|
||||
|
||||
if (!isset($config['session_key']) || !is_string($config['session_key'])) {
|
||||
throw new RuntimeException('Missing session_key in config.');
|
||||
}
|
||||
|
||||
function request_is_https(): bool
|
||||
{
|
||||
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (int)($_SERVER['SERVER_PORT'] ?? 0) === 443;
|
||||
}
|
||||
|
||||
session_name('osmtool_web');
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0,
|
||||
'path' => '/',
|
||||
'secure' => request_is_https(),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict',
|
||||
]);
|
||||
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['session_seed'])) {
|
||||
$_SESSION['session_seed'] = hash('sha256', $config['session_key'] . random_int(1000, 999999));
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/i18n.php';
|
||||
|
||||
function is_logged_in(): bool
|
||||
{
|
||||
return isset($_SESSION['auth']) && $_SESSION['auth'] === true;
|
||||
}
|
||||
|
||||
function require_login(): void
|
||||
{
|
||||
if (!is_logged_in()) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function csrf_token(): string
|
||||
{
|
||||
if (!isset($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return $_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
function verify_csrf(?string $token): bool
|
||||
{
|
||||
if (!isset($_SESSION['csrf_token']) || !is_string($token)) {
|
||||
return false;
|
||||
}
|
||||
return hash_equals($_SESSION['csrf_token'], $token);
|
||||
}
|
||||
|
||||
function h(string $value): string
|
||||
{
|
||||
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
|
||||
function execution_history(): array
|
||||
{
|
||||
$history = $_SESSION['execution_history'] ?? [];
|
||||
return is_array($history) ? $history : [];
|
||||
}
|
||||
|
||||
function push_execution_history(array $entry): void
|
||||
{
|
||||
$history = execution_history();
|
||||
array_unshift($history, $entry);
|
||||
$_SESSION['execution_history'] = array_slice($history, 0, 8);
|
||||
}
|
||||
Reference in New Issue
Block a user