initialize generic rms-software repository

Add the reusable RMS core application (server, web UI, plugins, tests, tools) with generic defaults, GPL licensing, and maintainer context documentation so deployments can consume this repo as software source independent of station-specific overlays.
This commit is contained in:
2026-03-16 03:31:08 +01:00
commit e1a4ce0b8b
58 changed files with 20611 additions and 0 deletions

519
public/index.html Normal file
View File

@@ -0,0 +1,519 @@
<!doctype html>
<html lang="de" data-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ARCG RemoteStation</title>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@500;700;800&family=Source+Sans+3:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<div class="page-bg"></div>
<main class="app-shell reveal">
<header class="topbar">
<div class="brand">
<a class="brand-home-link" href="/rms" aria-label="Zur RMS-Statusseite">
<div class="brand-mark" id="brandMark">
<img id="brandLogo" alt="ARCG Logo" hidden />
<span id="brandFallback">ARCG</span>
</div>
</a>
<div>
<p class="eyebrow">Amateur Radio Club Graz</p>
<h1>RemoteStation Control</h1>
<p id="pageCrumb" class="eyebrow">LOGIN</p>
</div>
</div>
<div class="topbar-actions">
<a id="currentUserLink" class="ghost-btn" href="/rms/user" hidden>👤 -</a>
<button id="userMenuButton" class="ghost-btn" type="button" aria-expanded="false" aria-label="Menue oeffnen"></button>
<button id="languageMenuButton" class="ghost-btn" type="button" aria-expanded="false" aria-label="Sprache waehlen">🌐</button>
<div id="languageMenu" class="language-menu" hidden>
<select id="menuLanguageSelect" aria-label="Sprache"></select>
</div>
<div id="userMenu" class="user-menu" hidden>
<button id="menuRms" type="button">📡 Status</button>
<button id="menuSwr" type="button">📈 SWR</button>
<button id="menuUser" type="button" hidden>⚙ Einstellungen</button>
<button id="menuHelp" type="button">❓ Hilfe</button>
<button id="menuPlugins" type="button">🧩 Plugins</button>
<button id="menuPluginConfig" type="button" hidden>⚙ Plugin Konfig</button>
<button id="menuProviders" type="button" hidden>🔌 Provider</button>
<button id="menuUsers" type="button" hidden>👥 User Admin</button>
<button id="menuApprovals" type="button" hidden>✅ Freigaben</button>
<button id="menuActivity" type="button" hidden>📜 Aktivitaet</button>
<button id="menuAdmin" type="button" hidden>🛠 Admin</button>
<hr class="menu-separator" />
<button id="logoutBtn" type="button" class="danger">Logout</button>
</div>
<button id="themeToggle" class="ghost-btn" type="button" aria-label="Theme wechseln"></button>
</div>
</header>
<section class="view auth-view" id="authView">
<article class="card login-card stagger" id="authCard">
<h2>Anmeldung</h2>
<p class="muted">Anmeldung nur per E-Mail-Adresse. Du bekommst einen Login- oder Bestaetigungslink.</p>
<p id="maintenanceBanner" class="message" hidden></p>
<form id="authForm" class="stack">
<label class="field">
<span>E-Mail</span>
<input id="email" type="email" autocomplete="email" required />
</label>
<label class="field">
<span>Bestaetigungsart</span>
<select id="authMethodSelect"></select>
</label>
<div class="actions">
<button type="submit" id="loginBtn">Link senden</button>
</div>
<div id="otpWrap" class="stack" hidden>
<label class="field">
<span>OTP-Code</span>
<input id="otpCode" type="text" inputmode="numeric" maxlength="6" placeholder="123456" />
</label>
<div class="actions">
<button type="button" id="verifyOtpBtn">Code bestaetigen</button>
</div>
</div>
<p id="authMessage" class="message"></p>
</form>
</article>
</section>
<section class="view" id="rmsView" hidden>
<article class="card page-overview">
<div class="section-head">
<h2 id="pageTitle">RMS Status</h2>
<span id="pageHint" class="pill">Station steuern</span>
</div>
</article>
<section class="grid-layout" id="pageRms" hidden>
<article class="card status-card stagger" id="statusCard">
<div class="section-head">
<h2>Stationsstatus</h2>
<span class="pill" id="stationOnlinePill">Pruefe...</span>
</div>
<dl class="status-grid">
<div>
<dt>Station</dt>
<dd id="stationName">-</dd>
</div>
<div>
<dt>Status</dt>
<dd id="usageStatus">-</dd>
</div>
<div>
<dt>Aktiver Benutzer</dt>
<dd id="activeBy">-</dd>
</div>
<div>
<dt>Seit</dt>
<dd id="startedAt">-</dd>
</div>
<div>
<dt>Bis</dt>
<dd id="endsAt">-</dd>
</div>
<div>
<dt>Restzeit</dt>
<dd id="remainingUsage">-</dd>
</div>
</dl>
<div class="actions" id="stationActions">
<button id="activateBtn" type="button">Station aktivieren</button>
<button id="deactivateBtn" type="button" class="danger">Station deaktivieren</button>
<button id="refreshBtn" type="button" class="ghost-btn">Aktualisieren</button>
</div>
<section id="activationProgress" class="progress-wrap" hidden>
<div class="progress-head">
<strong>SWR-Check laeuft</strong>
<span id="progressText">0%</span>
</div>
<div class="progress-track" aria-hidden="true">
<div id="progressFill" class="progress-fill"></div>
</div>
<p id="progressEta" class="muted">Geschaetzte Restzeit: -</p>
</section>
<section id="openwebrxPanel" class="links-wrap" hidden>
<div class="section-head">
<h3>OpenWebRX</h3>
</div>
<div class="actions">
<button id="openwebrxOpenBtn" type="button" class="ghost-btn">OpenWebRX laden</button>
<a id="openwebrxSessionLink" class="link-btn primary-btn" target="_blank" rel="noopener noreferrer" hidden>SDR oeffnen</a>
</div>
<p id="openwebrxMessage" class="message"></p>
<div id="openwebrxSessionAccess" class="stack" hidden>
<p class="muted">Session Key (Ticket): <code id="openwebrxSessionTicket">-</code></p>
</div>
</section>
<section id="reservationPanel" class="links-wrap" hidden>
<div class="section-head">
<h3>Reservierungen</h3>
</div>
<div class="actions">
<button id="reserveNextBtn" type="button" class="ghost-btn">Naechsten Slot reservieren</button>
</div>
<div id="reservationList" class="stack"></div>
<p id="reservationMessage" class="message"></p>
</section>
<section id="controlsPanel" class="links-wrap" hidden>
<div class="section-head">
<h3>Controls</h3>
<span id="openwebrxTxStatePill" class="pill">TX: unbekannt</span>
</div>
<div class="controls-grid">
<div class="rotor-layout">
<div class="rotor-main">
<p id="rotorCurrent" class="muted controls-inline">Rotor: -</p>
<div class="actions controls-inline">
<input id="rotorTarget" type="number" min="0" max="360" step="1" placeholder="Azimuth (0-360)" />
<button id="rotorSetBtn" type="button" class="ghost-btn">Rotor setzen</button>
</div>
<div id="rotorPresets" class="actions controls-inline">
<button type="button" class="ghost-btn" data-azimuth="0">N</button>
<button type="button" class="ghost-btn" data-azimuth="45">NE</button>
<button type="button" class="ghost-btn" data-azimuth="90">E</button>
<button type="button" class="ghost-btn" data-azimuth="135">SE</button>
<button type="button" class="ghost-btn" data-azimuth="180">S</button>
<button type="button" class="ghost-btn" data-azimuth="225">SW</button>
<button type="button" class="ghost-btn" data-azimuth="270">W</button>
<button type="button" class="ghost-btn" data-azimuth="315">NW</button>
</div>
</div>
<div id="rotorCompass" class="rotor-compass" aria-label="Rotor Kompass">
<span class="rotor-mark rotor-mark-n">0</span>
<span class="rotor-mark rotor-mark-e">90</span>
<span class="rotor-mark rotor-mark-s">180</span>
<span class="rotor-mark rotor-mark-w">270</span>
<div id="rotorCompassArrow" class="rotor-arrow" aria-hidden="true"></div>
<div class="rotor-center" aria-hidden="true"></div>
</div>
</div>
</div>
<p id="controlsMessage" class="message"></p>
</section>
<p id="statusMessage" class="message"></p>
</article>
<article class="card stagger" id="swrSummaryCard">
<div class="section-head">
<h2>SWR Uebersicht</h2>
<button id="refreshSwrBtn" type="button" class="ghost-btn">Neu laden</button>
</div>
<p class="muted" id="swrSummaryGeneratedAt">Stand: -</p>
<p class="muted" id="swrSummaryOverall">Gesamt: -</p>
<div id="swrSummaryBands" class="stack"></div>
<div class="actions">
<button id="runSwrCheckBtn" type="button">SWR Test starten</button>
</div>
<p id="swrSummaryMessage" class="message"></p>
</article>
</section>
<section class="grid-layout" id="pageSwr" hidden>
<article class="card admin-card stagger">
<div class="section-head swr-page-head">
<h2>SWR Test-Daten</h2>
<div class="swr-page-actions">
<div class="actions">
<button id="runSwrCheckPageBtn" type="button">SWR Test starten</button>
<button id="refreshSwrPageBtn" type="button" class="ghost-btn">Neu laden</button>
</div>
<p id="swrPageMessage" class="message swr-page-head-message"></p>
</div>
</div>
<section id="activationProgressSwr" class="progress-wrap" hidden>
<div class="progress-head">
<strong>SWR-Check laeuft</strong>
<span id="progressTextSwr">0%</span>
</div>
<div class="progress-track" aria-hidden="true">
<div id="progressFillSwr" class="progress-fill"></div>
</div>
<p id="progressEtaSwr" class="muted">Geschaetzte Restzeit: -</p>
</section>
<p id="swrPageGeneratedAt" class="muted">Stand: -</p>
<p id="swrPageOverall" class="muted">Gesamt: -</p>
<div id="swrPageBands" class="stack"></div>
</article>
</section>
<section class="grid-layout" id="pageUser" hidden>
<article class="card stagger" id="userSettingsCard">
<h2>Benutzer-Einstellungen</h2>
<p class="muted">Hier findest du nur deine persoenlichen Einstellungen.</p>
<dl class="status-grid">
<div>
<dt>E-Mail</dt>
<dd id="settingsEmail">-</dd>
</div>
<div>
<dt>Rolle</dt>
<dd id="settingsRole">-</dd>
</div>
</dl>
<label class="field" style="margin-top: 0.6rem">
<span>Praeferierte Authentifizierung</span>
<select id="settingsAuthMethodSelect"></select>
</label>
<label class="field" style="margin-top: 0.6rem">
<span>Sprache</span>
<select id="settingsLanguageSelect"></select>
</label>
<div class="actions">
<button id="settingsSaveAuthMethodBtn" type="button">Authentifizierung speichern</button>
<button id="settingsSaveLanguageBtn" type="button" class="ghost-btn">Sprache speichern</button>
<button id="settingsThemeBtn" type="button">Theme wechseln</button>
<button id="settingsRefreshBtn" type="button" class="ghost-btn">Status neu laden</button>
</div>
</article>
</section>
<section class="grid-layout" id="pageHelp" hidden>
<article class="card admin-card stagger" id="helpCard">
<div class="section-head">
<h2 id="helpTitle">Hilfe</h2>
<button id="refreshHelpBtn" type="button" class="ghost-btn">Neu laden</button>
</div>
<section class="plugin-block">
<h3 id="helpQuickStartTitle">Schnellstart</h3>
<ol id="helpQuickStartSteps" class="stack"></ol>
</section>
<div id="helpSections" class="stack"></div>
<p id="helpMessage" class="message"></p>
</article>
</section>
<section class="grid-layout" id="pagePlugins" hidden>
<article class="card admin-card stagger" id="pluginControlsCard">
<div class="section-head">
<h2>Plugin Controls</h2>
<span class="pill">Dynamisch</span>
</div>
<div id="pluginControls" class="stack"></div>
<p id="pluginMessage" class="message"></p>
</article>
</section>
<section class="grid-layout" id="pagePluginConfig" hidden>
<article class="card admin-card stagger" id="pluginsConfigCard" hidden>
<div class="section-head">
<h2>Plugin Konfiguration</h2>
<button id="refreshPluginsPageBtn" type="button" class="ghost-btn">Neu laden</button>
</div>
<h3>Plugin Verwaltung</h3>
<div id="pluginsAdminConfig" class="stack"></div>
</article>
</section>
<section class="grid-layout" id="pageProviders" hidden>
<article class="card admin-card stagger" id="providersCard">
<div class="section-head">
<h2>Provider Verwaltung</h2>
<button id="refreshProvidersBtn" type="button" class="ghost-btn">Neu laden</button>
</div>
<h3>Capability Matrix</h3>
<div id="providersCapabilityMatrix" class="stack"></div>
<hr class="separator" />
<h3>Provider Zuordnung</h3>
<div id="providersAdminConfig" class="stack"></div>
<p id="providersMessage" class="message"></p>
</article>
</section>
<section class="grid-layout" id="pageAdmin" hidden>
<article class="card admin-card stagger" id="adminCard" hidden>
<div class="section-head">
<h2>Admin</h2>
<span class="pill">Steuerung</span>
</div>
<div class="actions">
<button id="setOnlineBtn" type="button">Online setzen</button>
<button id="setOfflineBtn" type="button" class="danger">Offline setzen</button>
<button id="forceReleaseBtn" type="button" class="ghost-btn">Force Release</button>
<button id="refreshAuditBtn" type="button" class="ghost-btn">Audit laden</button>
</div>
<div class="stack" style="margin-top: 1rem">
<label class="field">
<span>Benutzerrolle aendern</span>
<input id="roleEmail" type="email" placeholder="user@example.com" />
</label>
<div class="actions">
<button id="setRoleAdminBtn" type="button">Als Admin setzen</button>
<button id="setRoleOperatorBtn" type="button" class="ghost-btn">Als Operator setzen</button>
</div>
</div>
<p id="adminMessage" class="message"></p>
<pre id="auditLog" class="audit-log"></pre>
<hr class="separator" />
<div class="section-head">
<h3>Branding</h3>
<span class="pill">Logo Upload</span>
</div>
<div class="stack" style="margin-top: 0.6rem">
<label class="field">
<span>Logo Light Theme</span>
<input id="logoLightFile" type="file" accept=".png,.jpg,.jpeg,.svg,.webp,image/png,image/jpeg,image/svg+xml,image/webp" />
</label>
<div class="actions">
<button id="uploadLogoLightBtn" type="button" class="ghost-btn">Light-Logo hochladen</button>
<button id="removeLogoLightBtn" type="button" class="danger">Light-Logo entfernen</button>
</div>
<label class="field">
<span>Logo Dark Theme</span>
<input id="logoDarkFile" type="file" accept=".png,.jpg,.jpeg,.svg,.webp,image/png,image/jpeg,image/svg+xml,image/webp" />
</label>
<div class="actions">
<button id="uploadLogoDarkBtn" type="button" class="ghost-btn">Dark-Logo hochladen</button>
<button id="removeLogoDarkBtn" type="button" class="danger">Dark-Logo entfernen</button>
</div>
</div>
<hr class="separator" />
<div class="section-head">
<h3>Wartungsmodus</h3>
<span class="pill" id="maintenanceStatePill">Unbekannt</span>
</div>
<label class="field" style="margin-top: 0.6rem">
<span>Hinweistext</span>
<input id="maintenanceMessageInput" type="text" placeholder="Wartungsmodus aktiv. Login ist derzeit deaktiviert." />
</label>
<div class="actions">
<button id="maintenanceEnableBtn" type="button" class="danger">Wartungsmodus aktivieren</button>
<button id="maintenanceDisableBtn" type="button" class="ghost-btn">Wartungsmodus deaktivieren</button>
</div>
</article>
</section>
<section class="grid-layout" id="pageUsers" hidden>
<article class="card admin-card stagger">
<div class="section-head">
<h2>Benutzerverwaltung</h2>
<div class="actions">
<label class="field compact-field">
<span>Suche</span>
<input id="usersFilterQuery" type="text" placeholder="mail@domain" />
</label>
<label class="field compact-field">
<span>Rolle</span>
<select id="usersFilterRole">
<option value="all">Alle</option>
<option value="admin">Admin</option>
<option value="approver">Approver</option>
<option value="operator">Operator</option>
</select>
</label>
<label class="field compact-field">
<span>Status</span>
<select id="usersFilterStatus">
<option value="all">Alle</option>
<option value="active">Aktiv</option>
<option value="pending_approval">Pending Approval</option>
<option value="pending_verification">Pending Verification</option>
<option value="denied">Denied</option>
</select>
</label>
<button id="refreshUsersBtn" type="button" class="ghost-btn">Neu laden</button>
</div>
</div>
<div id="usersAdmin" class="stack"></div>
<p id="usersMessage" class="message"></p>
</article>
</section>
<section class="grid-layout" id="pageApprovals" hidden>
<article class="card admin-card stagger">
<div class="section-head">
<h2>Freigaben</h2>
<div class="actions">
<label class="field compact-field">
<span>Suche</span>
<input id="approvalsFilterQuery" type="text" placeholder="mail@domain" />
</label>
<label class="field compact-field">
<span>Status</span>
<select id="approvalsFilterStatus">
<option value="all">Alle</option>
<option value="pending">Pending</option>
<option value="approved">Approved</option>
<option value="rejected">Rejected</option>
</select>
</label>
<button id="approvalsFilterOpenBtn" type="button" class="ghost-btn">Nur offen</button>
<button id="approvalsFilterAllBtn" type="button" class="ghost-btn">Alle</button>
<button id="refreshApprovalsBtn" type="button" class="ghost-btn">Neu laden</button>
</div>
</div>
<div id="approvalsList" class="stack"></div>
<p id="approvalsMessage" class="message"></p>
</article>
</section>
<section class="grid-layout" id="pageActivity" hidden>
<article class="card admin-card stagger">
<div class="section-head">
<h2>Aktivitaetslog</h2>
<div class="actions">
<label class="field compact-field">
<span>Suche</span>
<input id="activityFilterQuery" type="text" placeholder="mail oder text" />
</label>
<label class="field compact-field">
<span>Typ</span>
<select id="activityFilterType">
<option value="all">Alle</option>
<option value="auth.request_access">Link angefordert</option>
<option value="station.activate.start">Start Aktivierung</option>
<option value="station.activate.done">Aktivierung ok</option>
<option value="station.activate.failed">Aktivierung Fehler</option>
<option value="station.deactivate">Manuell beendet</option>
<option value="station.deactivate.timeout">Automatisch beendet</option>
</select>
</label>
<button id="refreshActivityBtn" type="button" class="ghost-btn">Neu laden</button>
</div>
</div>
<div id="activityLogList" class="stack"></div>
<p id="activityMessage" class="message"></p>
</article>
</section>
</section>
<nav id="mobileNav" class="mobile-nav" hidden>
<button id="mobileNavRms" type="button">📡 RMS</button>
<button id="mobileNavSwr" type="button">📈 SWR</button>
<button id="mobileNavUser" type="button" hidden>⚙ Einst.</button>
<button id="mobileNavHelp" type="button">❓ Hilfe</button>
<button id="mobileNavPlugins" type="button">🧩 Plugins</button>
<button id="mobileNavPluginConfig" type="button" hidden>⚙ PluginCfg</button>
<button id="mobileNavUsers" type="button" hidden>👥 User</button>
<button id="mobileNavApprovals" type="button" hidden>✅ Freigaben</button>
<button id="mobileNavActivity" type="button" hidden>📜 Log</button>
<button id="mobileNavAdmin" type="button" hidden>🛠 Admin</button>
</nav>
</main>
<script src="/app.js" defer></script>
</body>
</html>