Compare commits
19
Commits
d1e226a687
..
v1.1.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
499676569e | ||
|
|
35dbd198b8 | ||
|
|
e3f8a2579a | ||
|
|
9919966070 | ||
|
|
859f25f0c1 | ||
|
|
4c22c2363b | ||
|
|
bbf53d089a | ||
|
|
4476a5e0e2 | ||
|
|
36b77e3f0a | ||
|
|
78ae9e78ed | ||
|
|
61ef286420 | ||
|
|
ca1a5c9eb8 | ||
|
|
6d32bebdc6 | ||
|
|
e6078b9c89 | ||
|
|
ea9d4f21d7 | ||
|
|
d164d423ad | ||
|
|
2013df55bd | ||
|
|
1d46d036c0 | ||
|
|
9ae4e34fe0 |
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.1.5] - 2026-09-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Hub: don't claim the OS "keeps itself current" on the dashboard
|
||||||
|
- Hub: point RTL credentials at /rtl/ and bump dev version to 0.15.12
|
||||||
|
- Make dashboard cards uniform and symmetric; stop clipping update text
|
||||||
|
- Add boot splash; separate Systems Operational and Security icons
|
||||||
|
- Extend welcome dashboard background to the panel edges
|
||||||
|
- Add welcome dashboard as default view
|
||||||
|
- Monochrome updater glyph; fix oversized dialog header icon
|
||||||
|
- Merge Bitcoin categories into one; rename Self-Hosted Apps to Personal Apps
|
||||||
|
- Neutral graphite theme; branded updater icon
|
||||||
|
- Simplify Systems Operational, reword Domain Status, blue restart buttons
|
||||||
|
- Polish pass: sysops wording, QR size, NWC toolbar, logo, diagnostics placement
|
||||||
|
- Rework Update System dialog to the approved design; match rebuild dialog
|
||||||
|
- Redesign The Hub web UI: softer dark theme, sidebar nav, status widgets
|
||||||
|
- Nixpkgs update
|
||||||
|
- Switch LibreOffice from fresh to stable version
|
||||||
|
- Ui: simplify element-calling port modal — trim verbose steps and remove extra verification task
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated nixpkgs
|
||||||
|
- Updated Sovran-Bitcoin
|
||||||
|
- Updated flake to build through the new sovran-bitcoin input
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix role fit-and-finish: Desktop-only router note, narrow-viewport layout
|
||||||
|
- Self-heal truncated/corrupt Nix downloads and keep failed updates retryable
|
||||||
|
[1.1.5]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.1.5
|
||||||
|
|
||||||
|
|
||||||
## [1.1.4] - 2026-09-02
|
## [1.1.4] - 2026-09-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ Lightning infrastructure, private cloud, and communications platform when you
|
|||||||
are ready.
|
are ready.
|
||||||
|
|
||||||
[Visit the Website](https://sovransystems.com) ·
|
[Visit the Website](https://sovransystems.com) ·
|
||||||
[Download the ISO](https://downloads.sovransystems.com/Sovran_SystemsOS-1.1.4.iso) ·
|
[Download the ISO](https://downloads.sovransystems.com/Sovran_SystemsOS-1.1.5.iso) ·
|
||||||
[Try it safely in a VM](#try-it-first-in-a-virtual-machine) ·
|
[Try it safely in a VM](#try-it-first-in-a-virtual-machine) ·
|
||||||
[Verify the Download](https://downloads.sovransystems.com/Sovran_SystemsOS-1.1.4.iso.sha256) ·
|
[Verify the Download](https://downloads.sovransystems.com/Sovran_SystemsOS-1.1.5.iso.sha256) ·
|
||||||
[Build from Source](#build-from-source)
|
[Build from Source](#build-from-source)
|
||||||
|
|
||||||
<img src="assets/desktop-screenshot.webp" alt="Sovran_SystemsOS private Bitcoin desktop" width="800" />
|
<img src="assets/desktop-screenshot.webp" alt="Sovran_SystemsOS private Bitcoin desktop" width="800" />
|
||||||
@@ -345,8 +345,8 @@ with an imaging application such as [Balena Etcher](https://etcher.balena.io).
|
|||||||
|
|
||||||
### 1. Download the ISO and checksum
|
### 1. Download the ISO and checksum
|
||||||
|
|
||||||
- [Download Sovran_SystemsOS-1.1.4.iso](https://downloads.sovransystems.com/Sovran_SystemsOS-1.1.4.iso)
|
- [Download Sovran_SystemsOS-1.1.5.iso](https://downloads.sovransystems.com/Sovran_SystemsOS-1.1.5.iso)
|
||||||
- [Download Sovran_SystemsOS-1.1.4.iso.sha256](https://downloads.sovransystems.com/Sovran_SystemsOS-1.1.4.iso.sha256)
|
- [Download Sovran_SystemsOS-1.1.5.iso.sha256](https://downloads.sovransystems.com/Sovran_SystemsOS-1.1.5.iso.sha256)
|
||||||
|
|
||||||
The download may take some time. Do not rename or modify the ISO before
|
The download may take some time. Do not rename or modify the ISO before
|
||||||
verifying it, and keep both files in the same folder.
|
verifying it, and keep both files in the same folder.
|
||||||
@@ -364,16 +364,16 @@ checksum exactly.
|
|||||||
Open a terminal in the download folder and run:
|
Open a terminal in the download folder and run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sha256sum --check Sovran_SystemsOS-1.1.4.iso.sha256
|
sha256sum --check Sovran_SystemsOS-1.1.5.iso.sha256
|
||||||
```
|
```
|
||||||
|
|
||||||
A successful comparison reports:
|
A successful comparison reports:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Sovran_SystemsOS-1.1.4.iso: OK
|
Sovran_SystemsOS-1.1.5.iso: OK
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also run `sha256sum Sovran_SystemsOS-1.1.4.iso` and compare the output
|
You can also run `sha256sum Sovran_SystemsOS-1.1.5.iso` and compare the output
|
||||||
against the checksum file manually.
|
against the checksum file manually.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -384,11 +384,11 @@ against the checksum file manually.
|
|||||||
Open Terminal in the download folder and run:
|
Open Terminal in the download folder and run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
shasum -a 256 Sovran_SystemsOS-1.1.4.iso
|
shasum -a 256 Sovran_SystemsOS-1.1.5.iso
|
||||||
```
|
```
|
||||||
|
|
||||||
Compare the value shown in Terminal with the value inside
|
Compare the value shown in Terminal with the value inside
|
||||||
`Sovran_SystemsOS-1.1.4.iso.sha256`.
|
`Sovran_SystemsOS-1.1.5.iso.sha256`.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -398,7 +398,7 @@ Compare the value shown in Terminal with the value inside
|
|||||||
Open PowerShell in the download folder and run:
|
Open PowerShell in the download folder and run:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Get-FileHash .\Sovran_SystemsOS-1.1.4.iso -Algorithm SHA256
|
Get-FileHash .\Sovran_SystemsOS-1.1.5.iso -Algorithm SHA256
|
||||||
```
|
```
|
||||||
|
|
||||||
Compare the value under `Hash` with the published checksum.
|
Compare the value under `Hash` with the published checksum.
|
||||||
@@ -413,7 +413,7 @@ match exactly.
|
|||||||
|
|
||||||
1. Download and install [Balena Etcher](https://etcher.balena.io), then
|
1. Download and install [Balena Etcher](https://etcher.balena.io), then
|
||||||
connect the USB drive.
|
connect the USB drive.
|
||||||
2. Choose **Flash from file** and select `Sovran_SystemsOS-1.1.4.iso`.
|
2. Choose **Flash from file** and select `Sovran_SystemsOS-1.1.5.iso`.
|
||||||
3. Choose **Select target**, select the USB drive, and review your selection
|
3. Choose **Select target**, select the USB drive, and review your selection
|
||||||
carefully.
|
carefully.
|
||||||
4. Choose **Flash** and wait for the writing and verification process to
|
4. Choose **Flash** and wait for the writing and verification process to
|
||||||
@@ -856,7 +856,7 @@ primary location for collaboration. Please read our
|
|||||||
## Privacy. Sovereignty. Bitcoin.
|
## Privacy. Sovereignty. Bitcoin.
|
||||||
|
|
||||||
[Visit Sovran Systems](https://sovransystems.com) ·
|
[Visit Sovran Systems](https://sovransystems.com) ·
|
||||||
[Download Sovran_SystemsOS](https://downloads.sovransystems.com/Sovran_SystemsOS-1.1.4.iso) ·
|
[Download Sovran_SystemsOS](https://downloads.sovransystems.com/Sovran_SystemsOS-1.1.5.iso) ·
|
||||||
[View the License](LICENSE)
|
[View the License](LICENSE)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -234,10 +234,9 @@ _support_expiry_timer_lock = Lock()
|
|||||||
|
|
||||||
CATEGORY_ORDER = [
|
CATEGORY_ORDER = [
|
||||||
("infrastructure", "Infrastructure"),
|
("infrastructure", "Infrastructure"),
|
||||||
("bitcoin-base", "Bitcoin Base"),
|
("bitcoin", "Bitcoin"),
|
||||||
("bitcoin-apps", "Bitcoin Apps"),
|
|
||||||
("communication", "Communication"),
|
("communication", "Communication"),
|
||||||
("apps", "Self-Hosted Apps"),
|
("apps", "Personal Apps"),
|
||||||
("nostr", "Nostr"),
|
("nostr", "Nostr"),
|
||||||
("support", "Support"),
|
("support", "Support"),
|
||||||
("feature-manager", "Feature Manager"),
|
("feature-manager", "Feature Manager"),
|
||||||
@@ -452,7 +451,7 @@ ROLE_LABELS = {
|
|||||||
ROLE_CATEGORIES: dict[str, set[str] | None] = {
|
ROLE_CATEGORIES: dict[str, set[str] | None] = {
|
||||||
"server_plus_desktop": None,
|
"server_plus_desktop": None,
|
||||||
"desktop": {"infrastructure", "support", "feature-manager"},
|
"desktop": {"infrastructure", "support", "feature-manager"},
|
||||||
"node": {"infrastructure", "bitcoin-base", "bitcoin-apps", "support", "feature-manager"},
|
"node": {"infrastructure", "bitcoin", "support", "feature-manager"},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Features shown per role (None = show all)
|
# Features shown per role (None = show all)
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
/* Sovran_SystemsOS Hub — Web UI Stylesheet
|
/* Sovran_SystemsOS Hub — Web UI Stylesheet
|
||||||
Dark theme — near-black with green accents matching the Sovran Hub icon
|
The Hub redesign — softer dark theme, GNOME 50 / libadwaita surfaces,
|
||||||
v8 — Black-forward, green used for accents/borders/highlights only */
|
Sovran green reserved for status and actions.
|
||||||
|
|
||||||
|
Design tokens are defined once here. Legacy variable names from the
|
||||||
|
previous theme are aliased to the new values so every stylesheet
|
||||||
|
(support, security, domain-setup, onboarding, …) re-skins without
|
||||||
|
needing per-rule edits. */
|
||||||
|
|
||||||
*, *::before, *::after {
|
*, *::before, *::after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -9,22 +14,48 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg-color: #080a09;
|
color-scheme: dark;
|
||||||
--surface-color: rgba(14, 16, 15, 0.7);
|
|
||||||
--card-color: rgba(20, 22, 21, 0.6);
|
/* ── The Hub palette ─────────────────────────────────────────── */
|
||||||
--border-color: rgba(255, 255, 255, 0.06);
|
--bg: #17191d;
|
||||||
--text-primary: #ecf3ef;
|
--surface: #1c1f24;
|
||||||
--text-secondary: #8aaa9a;
|
--card: #23272c;
|
||||||
--text-dim: #4a6658;
|
--card-hover: #292e34;
|
||||||
--accent-color: #5EAD8A;
|
--elevated: #26292e;
|
||||||
--green: #6DBF8B;
|
--inset: #121417;
|
||||||
--yellow: #e5a50a;
|
--border: rgba(255, 255, 255, 0.07);
|
||||||
--red: #e01b24;
|
--border-strong: rgba(255, 255, 255, 0.14);
|
||||||
--grey: #5E7A6A;
|
--text: #e9edec;
|
||||||
--radius-card: 18px;
|
--text-2: #a9b0b3;
|
||||||
--radius-btn: 8px;
|
--text-3: #7a8388;
|
||||||
--shadow-card: 0 4px 16px rgba(0, 0, 0, 0.4);
|
--accent: #3ecf8e;
|
||||||
--shadow-hover: 0 8px 32px rgba(0, 0, 0, 0.5);
|
--accent-strong: #42f39a;
|
||||||
|
--accent-deep: #1aa45d;
|
||||||
|
--accent-dim: rgba(66, 243, 154, 0.12);
|
||||||
|
--amber: #e9b64a;
|
||||||
|
--red: #f66151;
|
||||||
|
--blue: #78aeed;
|
||||||
|
--mono: 'JetBrains Mono', 'Fira Code', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||||
|
--radius-card: 20px;
|
||||||
|
--radius-dialog: 26px;
|
||||||
|
--shadow-card: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||||
|
--shadow-hover: 0 16px 36px rgba(0, 0, 0, 0.38);
|
||||||
|
--shadow-pop: 0 30px 80px rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
|
/* ── Legacy aliases (previous theme) — keep every old sheet working */
|
||||||
|
--bg-color: var(--bg);
|
||||||
|
--surface-color: var(--surface);
|
||||||
|
--card-color: var(--card);
|
||||||
|
--border-color: var(--border);
|
||||||
|
--text-primary: var(--text);
|
||||||
|
--text-secondary: var(--text-2);
|
||||||
|
--text-dim: var(--text-3);
|
||||||
|
--accent-color: var(--accent);
|
||||||
|
--green: var(--accent);
|
||||||
|
--yellow: var(--amber);
|
||||||
|
--red: var(--red);
|
||||||
|
--grey: var(--text-3);
|
||||||
|
--radius-btn: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
@@ -32,19 +63,27 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
font-family: Inter, Cantarell, 'Segoe UI', system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif;
|
||||||
background:
|
background: var(--bg);
|
||||||
radial-gradient(ellipse at top, rgba(94, 173, 138, 0.04) 0%, transparent 50%),
|
color: var(--text);
|
||||||
var(--bg-color);
|
font-size: 15px;
|
||||||
color: var(--text-primary);
|
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Login page ──────────────────────────────────────────────────── */
|
button { font-family: inherit; }
|
||||||
|
|
||||||
|
::selection { background: rgba(66, 243, 154, 0.25); }
|
||||||
|
|
||||||
|
/* ── Scrollbars ─────────────────────────────────────────────────── */
|
||||||
|
::-webkit-scrollbar { width: 10px; height: 10px; }
|
||||||
|
::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.10); border-radius: 99px; border: 2px solid transparent; background-clip: padding-box; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.18); background-clip: padding-box; }
|
||||||
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
|
||||||
|
/* ── Login page ─────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.login-wrapper {
|
.login-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -52,86 +91,103 @@ body {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card {
|
.login-card {
|
||||||
background-color: rgba(14, 16, 15, 0.75);
|
|
||||||
backdrop-filter: blur(16px);
|
|
||||||
-webkit-backdrop-filter: blur(16px);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 48px 40px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 402px;
|
||||||
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
text-align: center;
|
||||||
|
background: var(--elevated);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 28px;
|
||||||
|
padding: 46px 42px 38px;
|
||||||
|
box-shadow: var(--shadow-pop);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-header {
|
.login-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-logo {
|
.login-logo {
|
||||||
height: 64px;
|
width: 78px;
|
||||||
margin-bottom: 16px;
|
height: 78px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
filter: drop-shadow(0 8px 24px rgba(28, 196, 120, 0.3));
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-title {
|
.login-title {
|
||||||
font-size: 1.25rem;
|
font-size: 1.3rem;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: var(--text-primary);
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title em { font-style: normal; color: var(--accent); }
|
||||||
|
|
||||||
|
.login-sub {
|
||||||
|
color: var(--text-2);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin: 7px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form {
|
.login-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group label {
|
.form-group label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.82rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 600;
|
font-weight: 650;
|
||||||
color: var(--text-secondary);
|
color: var(--text-2);
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input {
|
.form-group input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px 14px;
|
padding: 11px 16px;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-strong);
|
||||||
border-radius: var(--radius-btn);
|
border-radius: 14px;
|
||||||
background-color: var(--card-color);
|
background: var(--inset);
|
||||||
color: var(--text-primary);
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
font-size: 0.92rem;
|
font-size: 0.92rem;
|
||||||
|
outline: 0;
|
||||||
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input:focus {
|
.form-group input:focus {
|
||||||
outline: none;
|
border-color: rgba(66, 243, 154, 0.55);
|
||||||
border-color: var(--accent-color);
|
box-shadow: 0 0 0 3px rgba(66, 243, 154, 0.13);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-login {
|
.btn-login {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: var(--radius-btn);
|
border-radius: 99px;
|
||||||
background-color: var(--accent-color);
|
background: linear-gradient(180deg, #3fd68e, #1ea263);
|
||||||
color: #0A1A10;
|
color: #04220f;
|
||||||
font-size: 0.95rem;
|
font-size: 0.92rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.28), 0 6px 18px rgba(35, 199, 124, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-login:hover {
|
.btn-login:hover:not(:disabled) {
|
||||||
opacity: 0.88;
|
filter: brightness(1.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-error {
|
.login-error {
|
||||||
background-color: rgba(224, 27, 36, 0.12);
|
background: rgba(246, 97, 81, 0.10);
|
||||||
border: 1px solid var(--red);
|
border: 1px solid rgba(246, 97, 81, 0.35);
|
||||||
color: #f87171;
|
color: #f8a39b;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,34 +5,134 @@ button {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: opacity 0.15s, box-shadow 0.15s, background-color 0.15s;
|
transition: background 0.16s, border-color 0.16s, color 0.16s, transform 0.16s, filter 0.16s, opacity 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
opacity: 0.45;
|
opacity: 0.55;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
padding: 7px 16px;
|
display: inline-flex;
|
||||||
border-radius: var(--radius-btn);
|
align-items: center;
|
||||||
font-size: 0.88rem;
|
justify-content: center;
|
||||||
font-weight: 600;
|
gap: 8px;
|
||||||
|
border-radius: 99px;
|
||||||
|
padding: 8px 18px;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn svg { width: 15px; height: 15px; }
|
||||||
|
.btn:active { transform: scale(0.97); }
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background-color: var(--accent-color);
|
background: linear-gradient(180deg, #3fd68e, #1ea263);
|
||||||
color: #0A1A10;
|
color: #04220f;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.28), 0 6px 18px rgba(35, 199, 124, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
.btn-primary:hover:not(:disabled) {
|
||||||
opacity: 0.88;
|
filter: brightness(1.08);
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Update System button: uses accent green by default */
|
.btn-primary:active:not(:disabled) { transform: scale(0.97); }
|
||||||
|
|
||||||
|
/* Ghost — the default secondary action (legacy: btn-close-modal / btn-save) */
|
||||||
|
.btn-ghost,
|
||||||
|
.btn-close-modal,
|
||||||
|
.btn-save {
|
||||||
|
background: transparent;
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
color: var(--text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost:hover:not(:disabled),
|
||||||
|
.btn-close-modal:hover:not(:disabled),
|
||||||
|
.btn-save:hover:not(:disabled) {
|
||||||
|
color: var(--text);
|
||||||
|
border-color: rgba(255, 255, 255, 0.26);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blue — restart / retry actions (amber reads as an error) */
|
||||||
|
.btn-amber,
|
||||||
|
.btn-reboot,
|
||||||
|
.btn-restart-amber,
|
||||||
|
.btn-warning {
|
||||||
|
background: rgba(120, 174, 237, 0.10);
|
||||||
|
border-color: rgba(120, 174, 237, 0.35);
|
||||||
|
color: #a5cbf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-amber:hover:not(:disabled),
|
||||||
|
.btn-reboot:hover:not(:disabled),
|
||||||
|
.btn-restart-amber:hover:not(:disabled),
|
||||||
|
.btn-warning:hover:not(:disabled) {
|
||||||
|
background: rgba(120, 174, 237, 0.18);
|
||||||
|
border-color: rgba(120, 174, 237, 0.55);
|
||||||
|
color: #c2dcf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: rgba(246, 97, 81, 0.10);
|
||||||
|
border-color: rgba(246, 97, 81, 0.35);
|
||||||
|
color: #f8a39b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover:not(:disabled) {
|
||||||
|
background: rgba(246, 97, 81, 0.18);
|
||||||
|
border-color: rgba(246, 97, 81, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm { padding: 6px 14px; font-size: 0.77rem; }
|
||||||
|
|
||||||
|
/* ── Header / topbar buttons ────────────────────────────────────── */
|
||||||
|
|
||||||
|
.btn-header-reboot {
|
||||||
|
background: rgba(120, 174, 237, 0.10);
|
||||||
|
border: 1px solid rgba(120, 174, 237, 0.35);
|
||||||
|
color: #a5cbf2;
|
||||||
|
font-size: 0.77rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 99px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-header-reboot:hover:not(:disabled) {
|
||||||
|
background: rgba(120, 174, 237, 0.18);
|
||||||
|
border-color: rgba(120, 174, 237, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-logout {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
color: var(--text-2);
|
||||||
|
font-size: 0.77rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 99px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-logout:hover {
|
||||||
|
color: var(--text);
|
||||||
|
border-color: rgba(255, 255, 255, 0.26);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Update System button (sidebar) ─────────────────────────────── */
|
||||||
|
|
||||||
.btn-update {
|
.btn-update {
|
||||||
background-color: #4A9474;
|
background: transparent;
|
||||||
color: #E0F2EA;
|
border: 1px solid var(--border-strong);
|
||||||
|
color: var(--text-2);
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -40,24 +140,22 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-update:hover:not(:disabled) {
|
.btn-update:hover:not(:disabled) {
|
||||||
opacity: 0.88;
|
color: var(--text);
|
||||||
|
border-color: rgba(255, 255, 255, 0.26);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Update System button: brighter green when updates are available */
|
|
||||||
.btn-update.has-updates {
|
.btn-update.has-updates {
|
||||||
background-color: #5EAD8A;
|
background: rgba(66, 243, 154, 0.12);
|
||||||
color: #0A1A10;
|
border-color: rgba(66, 243, 154, 0.4);
|
||||||
}
|
color: var(--accent);
|
||||||
|
|
||||||
.btn-update.has-updates:hover:not(:disabled) {
|
|
||||||
background-color: #78C8A2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-badge {
|
.update-badge {
|
||||||
display: none;
|
display: none;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
background-color: var(--yellow);
|
background-color: var(--amber);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: pulse-badge 1.4s ease-in-out infinite;
|
animation: pulse-badge 1.4s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
@@ -71,9 +169,11 @@ button:disabled {
|
|||||||
50% { opacity: 0.5; transform: scale(1.35); }
|
50% { opacity: 0.5; transform: scale(1.35); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Icon buttons ───────────────────────────────────────────────── */
|
||||||
|
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
background: none;
|
background: none;
|
||||||
color: var(--text-secondary);
|
color: var(--text-2);
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
@@ -81,6 +181,6 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-icon:hover:not(:disabled) {
|
.btn-icon:hover:not(:disabled) {
|
||||||
background-color: var(--border-color);
|
background-color: rgba(255, 255, 255, 0.06);
|
||||||
color: var(--text-primary);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ domain-field-label {
|
|||||||
|
|
||||||
domain-field-input {
|
domain-field-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #0c0f0e;
|
background-color: var(--inset);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -75,7 +75,7 @@ domain-field-actions {
|
|||||||
|
|
||||||
.domain-field-input {
|
.domain-field-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #0c0f0e;
|
background-color: var(--inset);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -198,21 +198,21 @@ domain-field-actions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.port-proto-badge--tcp {
|
.port-proto-badge--tcp {
|
||||||
color: #6dbf8b;
|
color: var(--accent);
|
||||||
background: rgba(109, 191, 139, 0.12);
|
background: rgba(66, 243, 154, 0.12);
|
||||||
border: 1px solid rgba(109, 191, 139, 0.4);
|
border: 1px solid rgba(66, 243, 154, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.port-proto-badge--udp {
|
.port-proto-badge--udp {
|
||||||
color: #6aa9e0;
|
color: var(--blue);
|
||||||
background: rgba(106, 169, 224, 0.12);
|
background: rgba(106, 169, 224, 0.12);
|
||||||
border: 1px solid rgba(106, 169, 224, 0.4);
|
border: 1px solid rgba(106, 169, 224, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.port-proto-badge--both {
|
.port-proto-badge--both {
|
||||||
color: #e5a50a;
|
color: var(--amber);
|
||||||
background: rgba(229, 165, 10, 0.12);
|
background: rgba(233, 182, 74, 0.12);
|
||||||
border: 1px solid rgba(229, 165, 10, 0.45);
|
border: 1px solid rgba(233, 182, 74, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
.port-proto-note {
|
.port-proto-note {
|
||||||
@@ -230,7 +230,7 @@ domain-field-actions {
|
|||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
background: rgba(255, 180, 0, 0.10);
|
background: rgba(255, 180, 0, 0.10);
|
||||||
border: 1px solid var(--warning-color, #f59e0b);
|
border: 1px solid var(--warning-color, var(--amber));
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
|||||||
@@ -1,143 +1,196 @@
|
|||||||
/* ── Feature Manager styles ──────────────────────────────────────── */
|
/* ── Sidebar: Feature Manager + Preferences ─────────────────────── */
|
||||||
|
|
||||||
.feature-manager-section {
|
.category-section.autolaunch-section,
|
||||||
margin-bottom: 32px;
|
.category-section.feature-manager-section {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-subcategory {
|
/* The old "Preferences" header + hr becomes a small sidebar label.
|
||||||
margin-bottom: 16px;
|
features.js renders: .section-header + hr.section-divider + cards */
|
||||||
|
.autolaunch-section .section-header,
|
||||||
|
.feature-manager-section .section-header {
|
||||||
|
font-size: 0.66rem;
|
||||||
|
font-weight: 750;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-3);
|
||||||
|
margin: 0;
|
||||||
|
padding: 14px 10px 7px;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.autolaunch-section .section-divider,
|
||||||
|
.feature-manager-section .section-divider {
|
||||||
|
background: none;
|
||||||
|
height: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-cards-wrap { display: flex; flex-direction: column; }
|
||||||
|
|
||||||
|
.feature-subcategory { display: flex; flex-direction: column; }
|
||||||
|
|
||||||
.feature-subcategory-header {
|
.feature-subcategory-header {
|
||||||
font-size: 0.78rem;
|
font-size: 0.66rem;
|
||||||
font-weight: 700;
|
font-weight: 750;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.06em;
|
color: var(--text-3);
|
||||||
color: var(--text-dim);
|
padding: 12px 10px 6px;
|
||||||
margin-bottom: 8px;
|
|
||||||
padding-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-cards-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Feature card — sidebar preference row with a switch */
|
||||||
.feature-card {
|
.feature-card {
|
||||||
background-color: var(--card-color);
|
display: flex;
|
||||||
border: 1px solid var(--border-color);
|
gap: 10px;
|
||||||
border-radius: 12px;
|
align-items: flex-start;
|
||||||
padding: 14px 16px;
|
padding: 9px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.feature-card:hover { background: rgba(255, 255, 255, 0.05); }
|
||||||
|
|
||||||
.feature-card-top {
|
.feature-card-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
margin-bottom: 8px;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-card-info {
|
.feature-card-info { min-width: 0; flex: 1; }
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card-name {
|
.feature-card-name {
|
||||||
font-size: 0.9rem;
|
font-size: 0.84rem;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text);
|
||||||
margin-bottom: 4px;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-card-desc {
|
.feature-card-desc {
|
||||||
font-size: 0.78rem;
|
margin-top: 2px;
|
||||||
color: var(--text-secondary);
|
font-size: 0.68rem;
|
||||||
line-height: 1.5;
|
color: var(--text-3);
|
||||||
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-card-status {
|
.feature-card-status {
|
||||||
font-size: 0.72rem;
|
font-size: 0.66rem;
|
||||||
color: var(--text-dim);
|
font-weight: 700;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 99px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card-status.status-on {
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--accent-dim);
|
||||||
|
border-color: rgba(66, 243, 154, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-conflict-warning {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
color: var(--amber);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Domain badges inside feature cards */
|
||||||
|
.feature-domain-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 0.64rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 99px;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.feature-domain-badge.configured {
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--accent-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-domain-badge.not-configured {
|
||||||
|
color: var(--amber);
|
||||||
|
background: rgba(233, 182, 74, 0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-domain-icon { font-size: 0.8rem; }
|
||||||
|
|
||||||
|
.feature-domain-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 0.66rem;
|
||||||
|
font-weight: 650;
|
||||||
|
margin-top: 5px;
|
||||||
|
color: var(--text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-domain-label--ok { color: var(--accent); }
|
||||||
|
.feature-domain-label--warn { color: var(--amber); }
|
||||||
|
.feature-domain-label--error { color: var(--red); }
|
||||||
|
.feature-domain-label--checking { color: var(--blue); }
|
||||||
|
|
||||||
|
/* ── Toggle switch (libadwaita style) ───────────────────────────── */
|
||||||
|
|
||||||
.feature-toggle {
|
.feature-toggle {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 44px;
|
width: 46px;
|
||||||
height: 24px;
|
height: 26px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-top: 2px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-toggle-input {
|
.feature-toggle-input {
|
||||||
opacity: 0;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-toggle-slider {
|
.feature-toggle-slider {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background-color: var(--border-color);
|
border-radius: 99px;
|
||||||
border-radius: 24px;
|
background: rgba(255, 255, 255, 0.12);
|
||||||
transition: background-color 0.2s;
|
border: 1px solid var(--border-strong);
|
||||||
|
transition: 0.2s;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-toggle-slider::before {
|
.feature-toggle-slider::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 18px;
|
top: 2px;
|
||||||
height: 18px;
|
left: 2px;
|
||||||
left: 3px;
|
width: 20px;
|
||||||
top: 3px;
|
height: 20px;
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transition: transform 0.2s;
|
background: #cfd8d2;
|
||||||
|
transition: 0.2s cubic-bezier(0.3, 0.8, 0.4, 1.2);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-toggle.active .feature-toggle-slider {
|
.feature-toggle-input:checked + .feature-toggle-slider {
|
||||||
background-color: var(--green);
|
background: var(--accent);
|
||||||
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-toggle.active .feature-toggle-slider::before {
|
.feature-toggle-input:checked + .feature-toggle-slider::after {
|
||||||
transform: translateX(20px);
|
left: 23px;
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-domain-badge {
|
.feature-toggle-input:focus-visible + .feature-toggle-slider {
|
||||||
display: flex;
|
box-shadow: 0 0 0 3px rgba(66, 243, 154, 0.3);
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
margin-top: 6px;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-domain-icon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-domain-label {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-domain-label--checking {
|
|
||||||
color: var(--text-dim);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-domain-label--ok {
|
|
||||||
color: var(--green);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-domain-label--warn {
|
|
||||||
color: var(--yellow);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-domain-label--error {
|
|
||||||
color: var(--red);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,93 @@
|
|||||||
/* ── Header bar ─────────────────────────────────────────────────── */
|
/* ── Topbar (replaces the old header-bar + ip-bar) ──────────────── */
|
||||||
|
|
||||||
.header-bar {
|
.topbar {
|
||||||
backdrop-filter: blur(14px);
|
|
||||||
-webkit-backdrop-filter: blur(14px);
|
|
||||||
background-color: rgba(10, 12, 11, 0.82);
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
||||||
padding: 8px 24px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: 14px;
|
||||||
gap: 16px;
|
padding: 13px 26px;
|
||||||
position: sticky;
|
background: var(--surface);
|
||||||
top: 0;
|
border-bottom: 1px solid var(--border);
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-logo {
|
|
||||||
height: 80px;
|
|
||||||
width: auto;
|
|
||||||
display: block;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-bar .title {
|
.page-title {
|
||||||
font-size: 1.15rem;
|
font-size: 1.02rem;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: var(--text-primary);
|
letter-spacing: -0.01em;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-group {
|
/* ── Search ─────────────────────────────────────────────────────── */
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-buttons {
|
.search-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 9px;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 99px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
flex: 1;
|
||||||
|
max-width: 400px;
|
||||||
|
min-width: 140px;
|
||||||
|
margin-left: 18px;
|
||||||
|
color: var(--text-3);
|
||||||
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-box:focus-within {
|
||||||
|
border-color: rgba(66, 243, 154, 0.35);
|
||||||
|
box-shadow: 0 0 0 3px rgba(66, 243, 154, 0.09);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box svg { width: 15px; height: 15px; flex-shrink: 0; }
|
||||||
|
|
||||||
|
.search-box input {
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
color: var(--text);
|
||||||
|
width: 100%;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.87rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box input::placeholder { color: var(--text-3); }
|
||||||
|
|
||||||
|
.top-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 9px;
|
||||||
|
margin-left: auto;
|
||||||
|
order: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Legacy badges (still used inside dialogs) ──────────────────── */
|
||||||
|
|
||||||
.role-badge {
|
.role-badge {
|
||||||
background-color: var(--accent-color);
|
display: inline-flex;
|
||||||
color: #0A1A10;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 7px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
color: var(--text-2);
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
font-weight: 700;
|
font-weight: 750;
|
||||||
padding: 3px 10px;
|
padding: 6px 12px;
|
||||||
border-radius: 20px;
|
border-radius: 99px;
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── OS Version Badge — identical styling to the version badge ────
|
|
||||||
── shown next to titles in the service modal windows ──────────── */
|
|
||||||
.os-version-badge {
|
.os-version-badge {
|
||||||
background-color: rgba(255, 255, 255, 0.06);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
color: var(--text-secondary);
|
color: var(--text-2);
|
||||||
font-size: 0.72rem;
|
font-size: 0.66rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 2px 10px;
|
padding: 2px 9px;
|
||||||
border-radius: 12px;
|
border-radius: 99px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid var(--border);
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -71,76 +95,13 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── IP bar ─────────────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.ip-bar {
|
|
||||||
background-color: rgba(10, 12, 11, 0.7);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
||||||
padding: 8px 24px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 32px;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ip-bar .ip-label {
|
|
||||||
color: var(--text-dim);
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ip-bar .ip-value {
|
|
||||||
font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace;
|
|
||||||
color: var(--accent-color);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ip-separator {
|
|
||||||
color: var(--border-color);
|
|
||||||
}
|
|
||||||
.btn-logout {
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.78rem;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: var(--radius-btn);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.15s, color 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-logout:hover {
|
|
||||||
border-color: var(--accent-color);
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Header reboot button ───────────────────────────────────────── */
|
|
||||||
|
|
||||||
.btn-header-reboot {
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid rgba(184, 125, 0, 0.35);
|
|
||||||
color: #c98d08;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: var(--radius-btn);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.15s, color 0.15s, background-color 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-header-reboot:hover {
|
|
||||||
border-color: #b87d00;
|
|
||||||
color: #e0a010;
|
|
||||||
background-color: rgba(184, 125, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.btn-header-reboot {
|
.btn-header-reboot {
|
||||||
padding: 4px 8px;
|
padding: 5px 10px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
}
|
||||||
|
.btn-logout {
|
||||||
|
padding: 5px 10px;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,138 +1,533 @@
|
|||||||
/* ── Main content ───────────────────────────────────────────────── */
|
/* ── App shell: sidebar + main column ───────────────────────────── */
|
||||||
|
|
||||||
.main-content {
|
.app {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
display: flex;
|
||||||
max-width: 1400px;
|
flex-direction: column;
|
||||||
width: 100%;
|
min-width: 0;
|
||||||
margin-left: auto;
|
}
|
||||||
margin-right: auto;
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 26px 30px 70px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Sidebar ────────────────────────────────────────────────────── */
|
/* ── Sidebar ────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 270px;
|
width: 250px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
height: 100%;
|
background: var(--surface);
|
||||||
overflow-y: auto;
|
border-right: 1px solid var(--border);
|
||||||
border-right: 1px solid rgba(255, 255, 255, 0.06);
|
|
||||||
background-color: rgba(12, 14, 13, 0.65);
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
-webkit-backdrop-filter: blur(12px);
|
|
||||||
padding: 20px 14px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0;
|
padding: 20px 14px 14px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Sidebar: Tech Support button ───────────────────────────────── */
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 2px 8px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
filter: drop-shadow(0 4px 14px rgba(28, 196, 120, 0.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text { display: flex; flex-direction: column; min-width: 0; }
|
||||||
|
|
||||||
|
.brand-title {
|
||||||
|
font-size: 1.02rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-title em { font-style: normal; color: var(--accent); }
|
||||||
|
|
||||||
|
.brand-sub {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-3);
|
||||||
|
font-family: var(--mono);
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
font-size: 0.66rem;
|
||||||
|
font-weight: 750;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-3);
|
||||||
|
padding: 14px 10px 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Category nav (dashboard.js) */
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 11px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
color: var(--text-2);
|
||||||
|
font-size: 0.87rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item svg { width: 17px; height: 17px; flex-shrink: 0; }
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
color: var(--text);
|
||||||
|
box-shadow: inset 2.5px 0 0 var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-text {
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-count {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 0.67rem;
|
||||||
|
font-weight: 750;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-3);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 99px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active .nav-count {
|
||||||
|
background: rgba(255, 255, 255, 0.10);
|
||||||
|
color: var(--text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* System items (tiles.js renders these as .sidebar-support-btn) */
|
||||||
.sidebar-support-btn {
|
.sidebar-support-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 11px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--card-color);
|
padding: 8px 10px;
|
||||||
border: 1px solid var(--border-color);
|
border-radius: 10px;
|
||||||
border-radius: 12px;
|
border: 0;
|
||||||
padding: 12px 14px;
|
background: none;
|
||||||
color: var(--text-primary);
|
color: var(--text-2);
|
||||||
|
font-size: 0.87rem;
|
||||||
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-style 0.15s, border-color 0.15s, background-color 0.15s;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-support-btn:hover {
|
.sidebar-support-btn:hover {
|
||||||
border-color: var(--accent-color);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
border-style: solid;
|
color: var(--text);
|
||||||
background-color: #162320;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-support-btn + .sidebar-support-btn {
|
.sidebar-support-btn + .sidebar-support-btn {
|
||||||
margin-top: 8px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-support-icon {
|
.sidebar-support-icon {
|
||||||
font-size: 1.5rem;
|
font-size: 1rem;
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-support-icon svg { width: 17px; height: 17px; }
|
||||||
|
|
||||||
.sidebar-support-text {
|
.sidebar-support-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 1px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-support-title {
|
.sidebar-support-title {
|
||||||
font-size: 0.88rem;
|
font-size: 0.87rem;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
line-height: 1.3;
|
||||||
|
color: var(--text);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-support-hint {
|
.sidebar-support-hint {
|
||||||
font-size: 0.72rem;
|
font-size: 0.68rem;
|
||||||
color: var(--accent-color);
|
font-weight: 650;
|
||||||
font-weight: 600;
|
color: var(--text-3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-divider {
|
.sidebar-divider {
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border);
|
||||||
margin: 16px 0;
|
margin: 14px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Upgrade modal ──────────────────────────────────────────────── */
|
.sidebar-spacer { flex: 1; min-height: 18px; }
|
||||||
|
|
||||||
.upgrade-dialog {
|
/* ── Welcome dashboard (default view) ───────────────────────────── */
|
||||||
max-width: 480px;
|
|
||||||
|
.welcome {
|
||||||
|
position: relative;
|
||||||
|
/* Full-bleed: cancel .content's padding so the glow reaches
|
||||||
|
every edge of the panel (sidebar border, topbar, viewport). */
|
||||||
|
margin: -26px -30px -70px;
|
||||||
|
padding: 70px 38px 70px;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: calc(100% + 96px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.upgrade-info-box {
|
/* Soft drifting glow — calm, GPU-cheap (transforms only) */
|
||||||
background-color: var(--card-color);
|
.welcome-bg {
|
||||||
border: 1px solid var(--border-color);
|
position: absolute;
|
||||||
border-radius: 10px;
|
inset: 0;
|
||||||
padding: 14px 18px;
|
pointer-events: none;
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.upgrade-info-title {
|
.orb {
|
||||||
font-size: 0.88rem;
|
position: absolute;
|
||||||
font-weight: 700;
|
border-radius: 50%;
|
||||||
color: var(--text-primary);
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orb-a {
|
||||||
|
width: 560px; height: 560px;
|
||||||
|
left: -140px; top: -180px;
|
||||||
|
background: radial-gradient(circle, rgba(62, 207, 142, 0.10), transparent 70%);
|
||||||
|
animation: orb-drift-a 26s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orb-b {
|
||||||
|
width: 680px; height: 680px;
|
||||||
|
right: -200px; top: 20%;
|
||||||
|
background: radial-gradient(circle, rgba(120, 174, 237, 0.07), transparent 70%);
|
||||||
|
animation: orb-drift-b 34s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orb-c {
|
||||||
|
width: 460px; height: 460px;
|
||||||
|
left: 34%; bottom: -200px;
|
||||||
|
background: radial-gradient(circle, rgba(66, 243, 154, 0.06), transparent 70%);
|
||||||
|
animation: orb-drift-c 42s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes orb-drift-a { to { transform: translate(70px, 50px) scale(1.14); } }
|
||||||
|
@keyframes orb-drift-b { to { transform: translate(-60px, -70px) scale(1.10); } }
|
||||||
|
@keyframes orb-drift-c { to { transform: translate(50px, -40px) scale(1.18); } }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.orb { animation: none; }
|
||||||
|
.welcome-inner { animation: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Boot splash ────────────────────────────────────────────────── */
|
||||||
|
/* Covers the shell while the first services/config data loads; the
|
||||||
|
inline script in index.html lifts it (window.__liftAppSplash). */
|
||||||
|
#app-splash {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2000;
|
||||||
|
background: var(--bg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: opacity 0.45s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app-splash.done { opacity: 0; pointer-events: none; }
|
||||||
|
|
||||||
|
.splash-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-ring {
|
||||||
|
position: relative;
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upgrade-info-list {
|
.splash-ring img { width: 60px; height: 60px; opacity: 0.95; }
|
||||||
padding-left: 20px;
|
|
||||||
font-size: 0.85rem;
|
.splash-ring-arc {
|
||||||
color: var(--text-secondary);
|
position: absolute;
|
||||||
line-height: 1.7;
|
inset: 0;
|
||||||
margin: 0;
|
border-radius: 50%;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.10);
|
||||||
|
border-top-color: var(--accent);
|
||||||
|
animation: splash-spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upgrade-info-list a {
|
@keyframes splash-spin { to { transform: rotate(360deg); } }
|
||||||
color: var(--accent-color);
|
|
||||||
|
.splash-title {
|
||||||
|
font-size: 1.02rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.upgrade-rebuild-note {
|
.splash-sub {
|
||||||
font-style: italic;
|
font-size: 0.84rem;
|
||||||
color: var(--text-dim);
|
color: var(--text-3);
|
||||||
font-size: 0.82rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.splash-ring-arc { animation-duration: 2.5s; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-inner {
|
||||||
|
position: relative;
|
||||||
|
max-width: 1040px;
|
||||||
|
margin: 0 auto;
|
||||||
|
animation: welcome-in 0.5s ease both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes welcome-in {
|
||||||
|
from { opacity: 0; transform: translateY(14px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-greeting {
|
||||||
|
font-size: 0.98rem;
|
||||||
|
font-weight: 650;
|
||||||
|
color: var(--text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-title {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: clamp(1.7rem, 2.8vw, 2.25rem);
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
line-height: 1.18;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-title em { font-style: normal; color: var(--accent); }
|
||||||
|
|
||||||
|
.welcome-meta {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-3);
|
||||||
|
font-family: var(--mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-meta-sep { margin: 0 8px; opacity: 0.6; }
|
||||||
|
|
||||||
|
.welcome-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-auto-rows: 1fr; /* both rows share the taller height → uniform cards */
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Three cards (Desktop-only role, no Bitcoin card) → one symmetric row */
|
||||||
|
@media (min-width: 681px) {
|
||||||
|
.welcome-cards:not(:has(#w-btc)) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* wrappers whose children become grid items */
|
||||||
|
.welcome-dyn { display: contents; }
|
||||||
|
|
||||||
|
.w-network .net-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 6px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-network .net-k {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--text-3);
|
||||||
|
width: 30px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-network .ip-value {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-network .sub { margin-top: 8px; }
|
||||||
|
|
||||||
|
.welcome-browse {
|
||||||
|
margin-top: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 17px 19px;
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03), var(--shadow-card);
|
||||||
|
transition: border-color 0.18s;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 88px; /* uniform card height everywhere */
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget:hover { border-color: var(--border-strong); }
|
||||||
|
.widget.clickable:hover { transform: translateY(-1px); }
|
||||||
|
|
||||||
|
.widget.clickable { cursor: pointer; }
|
||||||
|
.widget.clickable:focus-visible {
|
||||||
|
outline: 2px solid rgba(66, 243, 154, 0.45);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-chip {
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
border-radius: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 6px 16px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-chip svg { width: 22px; height: 22px; }
|
||||||
|
.widget-chip img { width: 46px; height: 46px; display: block; object-fit: contain; }
|
||||||
|
|
||||||
|
.widget-chip.chip-green {
|
||||||
|
background: linear-gradient(160deg, #2f9f6b, #17683f);
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-chip.chip-btc {
|
||||||
|
background: linear-gradient(160deg, #f7931a, #b96a0a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-chip.chip-amber {
|
||||||
|
background: linear-gradient(160deg, #b98426, #8a5f16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-chip.chip-sovran {
|
||||||
|
background: linear-gradient(160deg, #42f39a, #1aa45d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-chip.chip-neutral {
|
||||||
|
background: linear-gradient(160deg, #2e333a, #23272c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-body { min-width: 0; }
|
||||||
|
|
||||||
|
.widget h3 {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.005em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget .sub {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: var(--text-2);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget .sub b { color: var(--text); font-weight: 650; }
|
||||||
|
.widget .sub .good { color: var(--accent); font-weight: 650; }
|
||||||
|
.widget .sub .warn { color: var(--amber); font-weight: 650; }
|
||||||
|
|
||||||
|
.w-bar {
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 99px;
|
||||||
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 8px 0 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 99px;
|
||||||
|
background: linear-gradient(90deg, #42f39a, #1aa45d);
|
||||||
|
transition: width 0.6s cubic-bezier(0.3, 0.7, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ── Tiles area ─────────────────────────────────────────────────── */
|
/* ── Tiles area ─────────────────────────────────────────────────── */
|
||||||
|
|
||||||
#tiles-area {
|
#tiles-area {
|
||||||
flex: 1;
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 24px 20px 48px;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 72px 24px;
|
||||||
|
color: var(--text-3);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-loading-spinner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2.5px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-top-color: var(--accent);
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
/* ── Category sections ──────────────────────────────────────────── */
|
/* ── Category sections ──────────────────────────────────────────── */
|
||||||
|
|
||||||
.category-section {
|
.category-section {
|
||||||
@@ -140,25 +535,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
font-size: 0.82rem;
|
display: flex;
|
||||||
font-weight: 700;
|
align-items: center;
|
||||||
letter-spacing: 0.08em;
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 750;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--text-secondary);
|
color: var(--text-2);
|
||||||
margin-bottom: 4px;
|
white-space: nowrap;
|
||||||
padding-left: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-divider {
|
.section-divider {
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid var(--border-color);
|
flex: 1;
|
||||||
margin-bottom: 16px;
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, var(--border), transparent);
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiles-grid {
|
.tiles-grid {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: repeat(auto-fill, minmax(158px, 1fr));
|
||||||
gap: 14px;
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Empty state ────────────────────────────────────────────────── */
|
/* ── Empty state ────────────────────────────────────────────────── */
|
||||||
@@ -166,10 +566,103 @@
|
|||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 64px 24px;
|
padding: 64px 24px;
|
||||||
color: var(--text-dim);
|
color: var(--text-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state p {
|
.empty-state p {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Upgrade modal (kept from previous layout) ──────────────────── */
|
||||||
|
|
||||||
|
.upgrade-dialog {
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgrade-info-box {
|
||||||
|
background: rgba(0, 0, 0, 0.16);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
margin: 14px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgrade-info-title {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgrade-info-list {
|
||||||
|
padding-left: 20px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-2);
|
||||||
|
line-height: 1.7;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgrade-info-list a {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgrade-rebuild-note {
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--text-3);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── First-login security banner (inside .content) ──────────────── */
|
||||||
|
|
||||||
|
.security-first-login-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
background: rgba(233, 182, 74, 0.08);
|
||||||
|
border: 1px solid rgba(233, 182, 74, 0.30);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: var(--text-2);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Narrow viewports (phone / half-screen RDP) ──────────────────── */
|
||||||
|
/* The sidebar becomes an icon rail so the content keeps room, and the
|
||||||
|
topbar wraps its search onto a second row instead of overflowing. */
|
||||||
|
@media (max-width: 920px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 76px;
|
||||||
|
padding: 16px 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand { justify-content: center; padding: 2px 0 18px; gap: 0; }
|
||||||
|
.brand-text,
|
||||||
|
.nav-label,
|
||||||
|
.nav-text,
|
||||||
|
.nav-count,
|
||||||
|
.sidebar-support-text,
|
||||||
|
.sidebar-divider,
|
||||||
|
#sidebar-features { display: none; }
|
||||||
|
|
||||||
|
.nav-item { justify-content: center; gap: 0; padding: 10px 0; }
|
||||||
|
.sidebar-support-btn { justify-content: center; gap: 0; padding: 10px 0; }
|
||||||
|
|
||||||
|
.content { padding: 18px 18px 40px; }
|
||||||
|
|
||||||
|
/* keep the welcome background full-bleed against the new padding */
|
||||||
|
.welcome {
|
||||||
|
margin: -18px -18px -40px;
|
||||||
|
padding: 48px 20px 40px;
|
||||||
|
min-height: calc(100% + 58px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.topbar { flex-wrap: wrap; row-gap: 10px; padding: 12px 16px; }
|
||||||
|
.search-box { order: 5; flex-basis: 100%; }
|
||||||
|
|
||||||
|
.welcome { padding: 40px 16px 36px; }
|
||||||
|
.welcome-cards { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background:
|
background:
|
||||||
radial-gradient(ellipse at top, rgba(94, 173, 138, 0.04) 0%, transparent 50%),
|
radial-gradient(ellipse at top, rgba(62, 207, 142, 0.04) 0%, transparent 50%),
|
||||||
var(--bg-color);
|
var(--bg-color);
|
||||||
padding: 24px 16px 48px;
|
padding: 24px 16px 48px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -70,13 +70,13 @@
|
|||||||
.onboarding-step-dot.active {
|
.onboarding-step-dot.active {
|
||||||
background-color: var(--accent-color);
|
background-color: var(--accent-color);
|
||||||
border-color: var(--accent-color);
|
border-color: var(--accent-color);
|
||||||
color: #0A1A10;
|
color: #04220f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.onboarding-step-dot.completed {
|
.onboarding-step-dot.completed {
|
||||||
background-color: var(--green);
|
background-color: var(--green);
|
||||||
border-color: var(--green);
|
border-color: var(--green);
|
||||||
color: #0A1A10;
|
color: #04220f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.onboarding-step-connector {
|
.onboarding-step-connector {
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
/* Cards */
|
/* Cards */
|
||||||
|
|
||||||
.onboarding-card {
|
.onboarding-card {
|
||||||
background-color: rgba(14, 16, 15, 0.65);
|
background-color: rgba(18, 24, 20, 0.65);
|
||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(14px);
|
||||||
-webkit-backdrop-filter: blur(14px);
|
-webkit-backdrop-filter: blur(14px);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
@@ -184,10 +184,10 @@
|
|||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
background-color: rgba(94, 173, 138, 0.10);
|
background-color: rgba(62, 207, 142, 0.10);
|
||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
border: 1px solid rgba(94, 173, 138, 0.25);
|
border: 1px solid rgba(62, 207, 142, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Step header */
|
/* Step header */
|
||||||
@@ -371,8 +371,8 @@
|
|||||||
|
|
||||||
.onboarding-port-warn {
|
.onboarding-port-warn {
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
background-color: rgba(229, 165, 10, 0.1);
|
background-color: rgba(233, 182, 74, 0.1);
|
||||||
border: 1px solid rgba(229, 165, 10, 0.35);
|
border: 1px solid rgba(233, 182, 74, 0.35);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--yellow);
|
color: var(--yellow);
|
||||||
@@ -409,8 +409,8 @@
|
|||||||
|
|
||||||
.onboarding-creds-notice {
|
.onboarding-creds-notice {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
background-color: rgba(94, 173, 138, 0.08);
|
background-color: rgba(62, 207, 142, 0.08);
|
||||||
border: 1px solid rgba(94, 173, 138, 0.20);
|
border: 1px solid rgba(62, 207, 142, 0.20);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
@@ -503,8 +503,8 @@
|
|||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: rgba(94, 173, 138, 0.08);
|
background-color: rgba(62, 207, 142, 0.08);
|
||||||
border: 1px solid rgba(94, 173, 138, 0.20);
|
border: 1px solid rgba(62, 207, 142, 0.20);
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -512,7 +512,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.onboarding-cred-reveal-btn:hover {
|
.onboarding-cred-reveal-btn:hover {
|
||||||
background-color: rgba(94, 173, 138, 0.15);
|
background-color: rgba(62, 207, 142, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Completion checklist (Step 5) */
|
/* Completion checklist (Step 5) */
|
||||||
@@ -671,7 +671,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.onboarding-password-toggle:hover {
|
.onboarding-password-toggle:hover {
|
||||||
background-color: rgba(94, 173, 138, 0.10);
|
background-color: rgba(62, 207, 142, 0.10);
|
||||||
border-color: var(--accent-color);
|
border-color: var(--accent-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,8 +683,8 @@
|
|||||||
|
|
||||||
.onboarding-password-warning {
|
.onboarding-password-warning {
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
background-color: rgba(229, 165, 10, 0.1);
|
background-color: rgba(233, 182, 74, 0.1);
|
||||||
border: 1px solid rgba(229, 165, 10, 0.35);
|
border: 1px solid rgba(233, 182, 74, 0.35);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--yellow);
|
color: var(--yellow);
|
||||||
@@ -694,8 +694,8 @@
|
|||||||
|
|
||||||
.onboarding-password-success {
|
.onboarding-password-success {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
background-color: rgba(94, 173, 138, 0.10);
|
background-color: rgba(62, 207, 142, 0.10);
|
||||||
border: 1px solid rgba(94, 173, 138, 0.30);
|
border: 1px solid rgba(62, 207, 142, 0.30);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 0.92rem;
|
font-size: 0.92rem;
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
@@ -748,7 +748,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.reboot-card {
|
.reboot-card {
|
||||||
background-color: rgba(14, 16, 15, 0.8);
|
background-color: rgba(18, 24, 20, 0.8);
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
-webkit-backdrop-filter: blur(20px);
|
-webkit-backdrop-filter: blur(20px);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
@@ -823,57 +823,8 @@
|
|||||||
|
|
||||||
/* ── Responsive ─────────────────────────────────────────────────── */
|
/* ── Responsive ─────────────────────────────────────────────────── */
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
/* Legacy pre-redesign narrow-screen rules removed: they targeted the old
|
||||||
body {
|
DOM (.main-content, .header-bar, .ip-bar) and their .sidebar /
|
||||||
overflow: auto;
|
#tiles-area / .service-tile / .creds-qr-img overrides fought the new
|
||||||
}
|
responsive layout in layout.css (sidebar icon rail, wrapping topbar).
|
||||||
.main-content {
|
Narrow-viewport behavior now lives in layout.css + header.css. */
|
||||||
flex-direction: column;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
.sidebar {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
padding: 14px 12px;
|
|
||||||
}
|
|
||||||
#tiles-area {
|
|
||||||
height: auto;
|
|
||||||
overflow-y: visible;
|
|
||||||
padding: 16px 12px 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.header-bar {
|
|
||||||
padding: 10px 14px;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
.header-bar .title {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
.ip-bar {
|
|
||||||
gap: 16px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 8px 14px;
|
|
||||||
}
|
|
||||||
.tiles-grid {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.service-tile {
|
|
||||||
width: 140px;
|
|
||||||
min-height: 130px;
|
|
||||||
}
|
|
||||||
.reboot-card {
|
|
||||||
padding: 36px 28px;
|
|
||||||
margin: 0 16px;
|
|
||||||
}
|
|
||||||
.creds-dialog {
|
|
||||||
margin: 0 12px;
|
|
||||||
}
|
|
||||||
.creds-qr-img {
|
|
||||||
width: 200px;
|
|
||||||
height: 200px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
.security-warning-box {
|
.security-warning-box {
|
||||||
background-color: rgba(180, 40, 40, 0.10);
|
background-color: rgba(180, 40, 40, 0.10);
|
||||||
border-left: 3px solid #c94040;
|
border-left: 3px solid var(--red);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
background-color: #c94040;
|
background-color: var(--red);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger:hover:not(:disabled) {
|
.btn-danger:hover:not(:disabled) {
|
||||||
background-color: #a83030;
|
background-color: var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger:disabled {
|
.btn-danger:disabled {
|
||||||
@@ -105,8 +105,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.security-status-info { color: var(--text-secondary); }
|
.security-status-info { color: var(--text-secondary); }
|
||||||
.security-status-ok { color: #6DBF8B; }
|
.security-status-ok { color: var(--accent); }
|
||||||
.security-status-error { color: #e05252; }
|
.security-status-error { color: var(--red); }
|
||||||
|
|
||||||
/* ── Verify System Integrity ─────────────────────────────────────── */
|
/* ── Verify System Integrity ─────────────────────────────────────── */
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
|
|
||||||
.security-verify-link {
|
.security-verify-link {
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
color: var(--accent-color, #6DBF8B);
|
color: var(--accent-color, var(--accent));
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,13 +174,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.security-verify-pass {
|
.security-verify-pass {
|
||||||
background-color: rgba(109, 191, 139, 0.15);
|
background-color: rgba(66, 243, 154, 0.15);
|
||||||
color: #6DBF8B;
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.security-verify-fail {
|
.security-verify-fail {
|
||||||
background-color: rgba(224, 82, 82, 0.15);
|
background-color: rgba(224, 82, 82, 0.15);
|
||||||
color: #e05252;
|
color: var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.security-verify-errors {
|
.security-verify-errors {
|
||||||
@@ -221,12 +221,11 @@
|
|||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background-color: rgba(6, 8, 7, 0.94);
|
background: radial-gradient(1000px 640px at 50% 18%, #202429, #0f1113 72%);
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
-webkit-backdrop-filter: blur(8px);
|
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
padding: 26px;
|
||||||
animation: security-reset-fade-in 0.35s ease-out;
|
animation: security-reset-fade-in 0.35s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,13 +252,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.security-reset-password-box {
|
.security-reset-password-box {
|
||||||
font-family: monospace;
|
font-family: var(--mono);
|
||||||
font-size: 1.35rem;
|
font-size: 1.35rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
background: rgba(109, 191, 139, 0.10);
|
background: var(--accent-dim);
|
||||||
border: 1.5px solid rgba(109, 191, 139, 0.35);
|
border: 1.5px solid rgba(66, 243, 154, 0.35);
|
||||||
border-radius: 8px;
|
border-radius: 14px;
|
||||||
padding: 14px 24px;
|
padding: 14px 24px;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -277,10 +276,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.security-reset-reboot-btn {
|
.security-reset-reboot-btn {
|
||||||
background-color: #6DBF8B;
|
background: linear-gradient(180deg, #3fd68e, #1ea263);
|
||||||
color: #0a0c0b;
|
color: #04220f;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 7px;
|
border-radius: 99px;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.28), 0 6px 18px rgba(35, 199, 124, 0.22);
|
||||||
padding: 11px 22px;
|
padding: 11px 22px;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -290,7 +290,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.security-reset-reboot-btn:hover:not(:disabled) {
|
.security-reset-reboot-btn:hover:not(:disabled) {
|
||||||
background-color: #5aab78;
|
background-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.security-reset-reboot-btn:disabled {
|
.security-reset-reboot-btn:disabled {
|
||||||
@@ -303,11 +303,12 @@
|
|||||||
.security-first-login-banner {
|
.security-first-login-banner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 12px 18px;
|
padding: 12px 16px;
|
||||||
background-color: rgba(94, 173, 138, 0.08);
|
margin-bottom: 24px;
|
||||||
border-bottom: 2px solid rgba(94, 173, 138, 0.25);
|
background: rgba(233, 182, 74, 0.08);
|
||||||
|
border: 1px solid rgba(233, 182, 74, 0.30);
|
||||||
|
border-radius: 16px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,19 +334,21 @@
|
|||||||
|
|
||||||
.security-banner-dismiss {
|
.security-banner-dismiss {
|
||||||
background: none;
|
background: none;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-strong);
|
||||||
border-radius: 4px;
|
border-radius: 99px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
padding: 2px 7px;
|
padding: 4px 10px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
transition: background-color 0.15s;
|
transition: 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.security-banner-dismiss:hover {
|
.security-banner-dismiss:hover {
|
||||||
background-color: rgba(0,0,0,0.08);
|
color: var(--text);
|
||||||
|
border-color: rgba(255, 255, 255, 0.26);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Legacy security inline warning banner ───────────────────────── */
|
/* ── Legacy security inline warning banner ───────────────────────── */
|
||||||
@@ -357,14 +360,14 @@
|
|||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
background-color: rgba(180, 100, 0, 0.12);
|
background-color: rgba(180, 100, 0, 0.12);
|
||||||
border-left: 3px solid #c97a00;
|
border-left: 3px solid #c98d08;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.security-inline-icon {
|
.security-inline-icon {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: #e69000;
|
color: #e0a010;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,9 +381,9 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #e69000;
|
color: #e0a010;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border: 1px solid #c97a00;
|
border: 1px solid #c98d08;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
@@ -447,9 +450,9 @@
|
|||||||
|
|
||||||
.pw-credentials-note {
|
.pw-credentials-note {
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
color: #c97a00;
|
color: #c98d08;
|
||||||
background-color: rgba(180, 100, 0, 0.10);
|
background-color: rgba(180, 100, 0, 0.10);
|
||||||
border-left: 2px solid #c97a00;
|
border-left: 2px solid #c98d08;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.support-steps code {
|
.support-steps code {
|
||||||
background-color: rgba(94, 173, 138, 0.10);
|
background-color: rgba(62, 207, 142, 0.10);
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: var(--radius-btn);
|
border-radius: var(--radius-btn);
|
||||||
background-color: var(--accent-color);
|
background-color: var(--accent-color);
|
||||||
color: #0A1A10;
|
color: #04220f;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: var(--radius-btn);
|
border-radius: var(--radius-btn);
|
||||||
background-color: var(--accent-color);
|
background-color: var(--accent-color);
|
||||||
color: #0A1A10;
|
color: #04220f;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
@@ -168,7 +168,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.support-btn-auditlog:hover:not(:disabled) {
|
.support-btn-auditlog:hover:not(:disabled) {
|
||||||
background-color: #1c2a24;
|
background-color: var(--card-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.support-fine-print {
|
.support-fine-print {
|
||||||
@@ -219,13 +219,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.support-wallet-protected {
|
.support-wallet-protected {
|
||||||
background-color: rgba(109, 191, 139, 0.06);
|
background-color: rgba(66, 243, 154, 0.06);
|
||||||
border-color: rgba(109, 191, 139, 0.3);
|
border-color: rgba(66, 243, 154, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.support-wallet-unlocked {
|
.support-wallet-unlocked {
|
||||||
background-color: rgba(229, 165, 10, 0.06);
|
background-color: rgba(233, 182, 74, 0.06);
|
||||||
border-color: rgba(229, 165, 10, 0.3);
|
border-color: rgba(233, 182, 74, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.support-wallet-warning {
|
.support-wallet-warning {
|
||||||
@@ -290,7 +290,7 @@
|
|||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: var(--radius-btn);
|
border-radius: var(--radius-btn);
|
||||||
background-color: var(--yellow);
|
background-color: var(--yellow);
|
||||||
color: #0A1A10;
|
color: #04220f;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
@@ -310,7 +310,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.support-btn-wallet-lock:hover:not(:disabled) {
|
.support-btn-wallet-lock:hover:not(:disabled) {
|
||||||
background-color: #529E7E;
|
background-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Audit log ───────────────────────────────────────────────────── */
|
/* ── Audit log ───────────────────────────────────────────────────── */
|
||||||
@@ -324,7 +324,7 @@
|
|||||||
.support-audit-log {
|
.support-audit-log {
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: #0c0f0e;
|
background-color: var(--inset);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,473 +1,178 @@
|
|||||||
/* ── Service tile card (status-only) ─────────────────────────────── */
|
/* ── Service tiles ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
.dashboard-loading {
|
|
||||||
min-height: 180px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-loading-spinner {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border: 2px solid var(--border-color);
|
|
||||||
border-top-color: var(--accent-color);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: dashboard-loading-spin 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes dashboard-loading-spin {
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-tile {
|
.service-tile {
|
||||||
width: 160px;
|
background: var(--card);
|
||||||
min-height: 130px;
|
border: 1px solid var(--border);
|
||||||
background-color: var(--card-color);
|
border-radius: 20px;
|
||||||
border: 1px solid var(--border-color);
|
overflow: hidden;
|
||||||
backdrop-filter: blur(8px);
|
padding: 21px 12px 17px;
|
||||||
-webkit-backdrop-filter: blur(8px);
|
|
||||||
border-radius: var(--radius-card);
|
|
||||||
box-shadow: var(--shadow-card);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
padding: 20px 12px 18px;
|
|
||||||
gap: 0;
|
|
||||||
transition: box-shadow 0.2s, border-color 0.2s;
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: transform 0.18s, border-color 0.18s, background 0.18s, box-shadow 0.18s, opacity 0.18s;
|
||||||
|
animation: tileIn 0.45s cubic-bezier(0.2, 0.8, 0.3, 1) backwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tileIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px) scale(0.96); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-tile:hover {
|
.service-tile:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
background: var(--card-hover);
|
||||||
box-shadow: var(--shadow-hover);
|
box-shadow: var(--shadow-hover);
|
||||||
border-color: var(--accent-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-tile.disabled {
|
.service-tile:active { transform: translateY(-1px) scale(0.985); }
|
||||||
opacity: 0.45;
|
|
||||||
|
.service-tile:focus-visible {
|
||||||
|
outline: 2px solid rgba(66, 243, 154, 0.45);
|
||||||
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.service-tile.disabled { opacity: 0.55; }
|
||||||
|
|
||||||
.tile-icon {
|
.tile-icon {
|
||||||
width: 48px;
|
width: 62px;
|
||||||
height: 48px;
|
height: 62px;
|
||||||
|
display: block;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-icon-fallback {
|
.tile-icon-fallback {
|
||||||
width: 48px;
|
width: 62px;
|
||||||
height: 48px;
|
height: 62px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: var(--border-color);
|
color: var(--text-3);
|
||||||
border-radius: 12px;
|
font-size: 1.2rem;
|
||||||
color: var(--text-dim);
|
font-weight: 700;
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-name {
|
.tile-name {
|
||||||
font-size: 0.88rem;
|
margin-top: 13px;
|
||||||
font-weight: 600;
|
font-size: 0.86rem;
|
||||||
|
font-weight: 650;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--text-primary);
|
line-height: 1.25;
|
||||||
line-height: 1.3;
|
min-height: 2.5em;
|
||||||
max-width: 140px;
|
|
||||||
word-break: break-word;
|
|
||||||
hyphens: auto;
|
|
||||||
min-height: 1.3em;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
color: var(--text);
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Status pill — dot + label inside a rounded pill */
|
||||||
.tile-status {
|
.tile-status {
|
||||||
font-size: 0.75rem;
|
margin-top: 9px;
|
||||||
margin-top: 8px;
|
display: inline-flex;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 6px;
|
||||||
color: var(--text-secondary);
|
font-size: 0.69rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 99px;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: var(--text-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-version {
|
.status-text { line-height: 1; }
|
||||||
font-size: 0.7rem;
|
|
||||||
color: var(--text-dim);
|
|
||||||
margin-top: 2px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot {
|
.status-dot {
|
||||||
width: 8px;
|
width: 6px;
|
||||||
height: 8px;
|
height: 6px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
background: var(--text-3);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background-color: var(--grey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.active { background-color: var(--green); }
|
.status-dot.active { background: var(--accent); }
|
||||||
.status-dot.inactive { background-color: var(--red); }
|
.status-dot.needs-attention { background: var(--amber); }
|
||||||
.status-dot.loading { background-color: var(--yellow); animation: pulse-badge 1s infinite; }
|
.status-dot.failed { background: var(--red); }
|
||||||
.status-dot.failed { background-color: var(--red); }
|
.status-dot.inactive,
|
||||||
.status-dot.disabled { background-color: var(--grey); }
|
.status-dot.disabled { background: var(--text-3); }
|
||||||
.status-dot.needs-attention { background-color: var(--yellow); }
|
.status-dot.syncing,
|
||||||
.status-dot.syncing { background-color: #f5a623; animation: pulse-badge 1.5s infinite; }
|
.status-dot.loading,
|
||||||
.status-dot.checking-reachability { background-color: var(--accent-color); animation: pulse-badge 1s infinite; }
|
.status-dot.checking-reachability { background: var(--blue); animation: tile-dot-pulse 1.4s ease-in-out infinite; }
|
||||||
|
.status-dot.unknown { background: var(--text-3); }
|
||||||
|
|
||||||
/* ── Bitcoin IBD sync progress bar ──────────────────────────────── */
|
@keyframes tile-dot-pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.35; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progressive enhancement: tint the whole pill on modern browsers */
|
||||||
|
.tile-status:has(.status-dot.active) { background: var(--accent-dim); color: var(--accent); }
|
||||||
|
.tile-status:has(.status-dot.needs-attention) { background: rgba(233, 182, 74, 0.10); color: var(--amber); }
|
||||||
|
.tile-status:has(.status-dot.failed) { background: rgba(246, 97, 81, 0.10); color: var(--red); }
|
||||||
|
.tile-status:has(.status-dot.syncing),
|
||||||
|
.tile-status:has(.status-dot.loading),
|
||||||
|
.tile-status:has(.status-dot.checking-reachability) { background: rgba(120, 174, 237, 0.10); color: var(--blue); }
|
||||||
|
|
||||||
|
.support-status-label {
|
||||||
|
font-size: 0.69rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Bitcoin IBD sync tile ──────────────────────────────────────── */
|
||||||
|
|
||||||
.tile-sync-container {
|
.tile-sync-container {
|
||||||
display: flex;
|
margin-top: 12px;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 6px;
|
padding: 0 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-sync-label {
|
.tile-sync-label {
|
||||||
font-size: 0.72rem;
|
font-size: 0.66rem;
|
||||||
color: #f5a623;
|
font-weight: 750;
|
||||||
font-weight: 600;
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--amber);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-sync-bar-row {
|
.tile-sync-bar-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 8px;
|
||||||
width: 100%;
|
margin-top: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-sync-bar-track {
|
.tile-sync-bar-track {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 6px;
|
height: 7px;
|
||||||
background-color: var(--border-color);
|
border-radius: 99px;
|
||||||
border-radius: 3px;
|
background: rgba(255, 255, 255, 0.07);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-sync-bar-fill {
|
.tile-sync-bar-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #f5a623;
|
border-radius: 99px;
|
||||||
border-radius: 3px;
|
background: linear-gradient(90deg, #42f39a, #1aa45d);
|
||||||
transition: width 0.6s ease;
|
transition: width 0.6s cubic-bezier(0.3, 0.7, 0.3, 1);
|
||||||
min-width: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-sync-percent {
|
.tile-sync-percent {
|
||||||
font-size: 0.72rem;
|
font-family: var(--mono);
|
||||||
|
font-size: 0.7rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #f5a623;
|
color: var(--amber);
|
||||||
white-space: nowrap;
|
flex-shrink: 0;
|
||||||
min-width: 2.5em;
|
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-sync-eta {
|
.tile-sync-eta {
|
||||||
font-size: 0.68rem;
|
margin-top: 6px;
|
||||||
color: var(--text-dim);
|
font-family: var(--mono);
|
||||||
|
font-size: 0.66rem;
|
||||||
|
color: var(--text-3);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ── Service detail modal sections ───────────────────────────────── */
|
|
||||||
|
|
||||||
.svc-detail-section {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-section:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
margin-bottom: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-section-title {
|
|
||||||
font-size: 0.78rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
color: var(--text-dim);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-desc {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Service detail: Domain ──────────────────────────────────────── */
|
|
||||||
|
|
||||||
.svc-detail-domain-value {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile-domain-label--ok {
|
|
||||||
color: var(--green);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile-domain-label--warn {
|
|
||||||
color: var(--yellow);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile-domain-label--error {
|
|
||||||
color: var(--red);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Service detail: Port table ──────────────────────────────────── */
|
|
||||||
|
|
||||||
.svc-detail-port-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-port-table th {
|
|
||||||
text-align: left;
|
|
||||||
color: var(--text-dim);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
padding: 6px 10px;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-port-table td {
|
|
||||||
padding: 8px 10px;
|
|
||||||
border-bottom: 1px solid rgba(30, 45, 39, 0.6);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-port-table tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-port-table-port {
|
|
||||||
font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-port-table-proto {
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-port-table-desc {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-port-table-status {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.port-status-listening { color: var(--green); }
|
|
||||||
.port-status-open { color: var(--yellow); }
|
|
||||||
.port-status-closed { color: var(--red); }
|
|
||||||
.port-status-unknown { color: var(--text-dim); }
|
|
||||||
|
|
||||||
/* ── Service detail: Troubleshoot box ────────────────────────────── */
|
|
||||||
|
|
||||||
.svc-detail-troubleshoot {
|
|
||||||
margin-top: 12px;
|
|
||||||
padding: 14px 16px;
|
|
||||||
background-color: rgba(229, 165, 10, 0.08);
|
|
||||||
border: 1px solid rgba(229, 165, 10, 0.3);
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-troubleshoot strong {
|
|
||||||
color: var(--yellow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-troubleshoot ol {
|
|
||||||
margin-top: 8px;
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-troubleshoot li {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-troubleshoot code {
|
|
||||||
background-color: rgba(94, 173, 138, 0.10);
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-troubleshoot a {
|
|
||||||
color: var(--accent-color);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-troubleshoot a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Service detail: Domain configure button ─────────────────────── */
|
|
||||||
|
|
||||||
.svc-detail-domain-btn {
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Service detail: Addon feature toggle ────────────────────────── */
|
|
||||||
|
|
||||||
.svc-detail-addon-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 14px;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-addon-status {
|
|
||||||
font-size: 0.88rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.addon-status--on {
|
|
||||||
color: var(--green);
|
|
||||||
}
|
|
||||||
|
|
||||||
.addon-status--off {
|
|
||||||
color: var(--text-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-option-card {
|
|
||||||
padding: 16px;
|
|
||||||
background: rgba(94, 173, 138, 0.06);
|
|
||||||
border: 1px solid rgba(94, 173, 138, 0.24);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-option-list {
|
|
||||||
margin: 10px 0 0;
|
|
||||||
padding-left: 20px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.84rem;
|
|
||||||
line-height: 1.55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-option-list li {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-option-privacy {
|
|
||||||
margin-top: 12px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border-left: 3px solid var(--accent-color);
|
|
||||||
background: rgba(94, 173, 138, 0.08);
|
|
||||||
border-radius: 6px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.82rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-option-privacy strong {
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-related-feature-btn:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-conflict-warning {
|
|
||||||
margin-top: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
padding: 10px 14px;
|
|
||||||
background-color: rgba(229, 165, 10, 0.1);
|
|
||||||
border: 1px solid rgba(229, 165, 10, 0.3);
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
color: var(--yellow);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-warning {
|
|
||||||
background-color: #d97706;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-warning:hover:not(:disabled) {
|
|
||||||
background-color: #b45309;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-restart-section {
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
padding-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-restart-btn {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-restart-result {
|
|
||||||
margin-top: 12px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-restart-result.success {
|
|
||||||
background-color: rgba(109, 191, 139, 0.12);
|
|
||||||
border: 1px solid var(--green);
|
|
||||||
color: var(--green);
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-restart-result.error {
|
|
||||||
background-color: rgba(239, 68, 68, 0.12);
|
|
||||||
border: 1px solid #ef4444;
|
|
||||||
color: #f87171;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ── Desktop launch buttons ──────────────────────────────────────── */
|
|
||||||
|
|
||||||
.svc-detail-launch-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svc-detail-launch-btn {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
padding: 8px 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,13 +23,19 @@ const SUPPORT_TIMER_INTERVAL = 1000;
|
|||||||
|
|
||||||
const CATEGORY_ORDER = [
|
const CATEGORY_ORDER = [
|
||||||
"infrastructure",
|
"infrastructure",
|
||||||
"bitcoin-base",
|
"bitcoin",
|
||||||
"bitcoin-apps",
|
|
||||||
"communication",
|
"communication",
|
||||||
"apps",
|
"apps",
|
||||||
"nostr",
|
"nostr",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/* The service catalog still distinguishes the Bitcoin foundation services
|
||||||
|
from the Bitcoin apps; the Hub shows them as one "Bitcoin" section. */
|
||||||
|
const CATEGORY_ALIASES = {
|
||||||
|
"bitcoin-base": "bitcoin",
|
||||||
|
"bitcoin-apps": "bitcoin",
|
||||||
|
};
|
||||||
|
|
||||||
const FEATURE_SUBCATEGORY_LABELS = {
|
const FEATURE_SUBCATEGORY_LABELS = {
|
||||||
"infrastructure": "🔧 Infrastructure",
|
"infrastructure": "🔧 Infrastructure",
|
||||||
"bitcoin": "₿ Bitcoin",
|
"bitcoin": "₿ Bitcoin",
|
||||||
|
|||||||
@@ -0,0 +1,451 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
/* ── The Hub dashboard chrome ──────────────────────────────────────
|
||||||
|
Welcome dashboard (default view), category navigation, service
|
||||||
|
search, status cards, and the Systems Operational modal.
|
||||||
|
|
||||||
|
Everything lives in an IIFE so no globals leak into the other Hub
|
||||||
|
scripts; tiles.js calls window.dashboardServicesUpdated() whenever
|
||||||
|
the service list refreshes. */
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
|
||||||
|
var $nav = document.getElementById("sidebar-nav");
|
||||||
|
var $pageTitle = document.getElementById("page-title");
|
||||||
|
var $search = document.getElementById("search-input");
|
||||||
|
var $sysModal = document.getElementById("systems-modal");
|
||||||
|
var $sysBody = document.getElementById("systems-body");
|
||||||
|
var $welcome = document.getElementById("welcome-view");
|
||||||
|
var $tilesArea = document.getElementById("tiles-area");
|
||||||
|
var $wcSystems = document.getElementById("wc-systems");
|
||||||
|
var $wcMore = document.getElementById("wc-more");
|
||||||
|
var $greeting = document.getElementById("welcome-greeting");
|
||||||
|
var $welcomeRole = document.getElementById("welcome-role");
|
||||||
|
var $browseBtn = document.getElementById("welcome-browse-btn");
|
||||||
|
|
||||||
|
var _view = "dashboard"; // "dashboard" | "services"
|
||||||
|
var _cat = "all";
|
||||||
|
var _query = "";
|
||||||
|
|
||||||
|
var CAT_ICONS = {
|
||||||
|
"all": "g-grid",
|
||||||
|
"infrastructure": "g-server",
|
||||||
|
"bitcoin": "g-btc-sym",
|
||||||
|
"communication": "g-chat",
|
||||||
|
"apps": "g-dots",
|
||||||
|
"nostr": "g-antenna",
|
||||||
|
"other": "g-dots"
|
||||||
|
};
|
||||||
|
|
||||||
|
var CAT_FALLBACK_LABELS = {
|
||||||
|
"infrastructure": "Infrastructure",
|
||||||
|
"bitcoin": "Bitcoin",
|
||||||
|
"communication": "Communication",
|
||||||
|
"apps": "Personal Apps",
|
||||||
|
"nostr": "Nostr",
|
||||||
|
"other": "Other"
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Units that serve a domain — used to find a live diagnostics
|
||||||
|
checklist for the Systems Operational modal (order matters only
|
||||||
|
for which service is polled first). */
|
||||||
|
var DOMAIN_UNITS = [
|
||||||
|
"matrix-synapse.service",
|
||||||
|
"btcpayserver.service",
|
||||||
|
"vaultwarden.service",
|
||||||
|
"phpfpm-nextcloud.service",
|
||||||
|
"phpfpm-wordpress.service",
|
||||||
|
"haven-relay.service",
|
||||||
|
"livekit.service",
|
||||||
|
"albyhub.service"
|
||||||
|
];
|
||||||
|
|
||||||
|
function icon(id) {
|
||||||
|
return '<svg><use href="#' + id + '"/></svg>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleServices() {
|
||||||
|
var services = (typeof _servicesCache !== "undefined" && _servicesCache) ? _servicesCache : [];
|
||||||
|
return services.filter(function (s) {
|
||||||
|
return s.category !== "support" && s.type !== "support";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Views ────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
function setTitle(t) {
|
||||||
|
if ($pageTitle) $pageTitle.textContent = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncNav() {
|
||||||
|
if (!$nav) return;
|
||||||
|
$nav.querySelectorAll(".nav-item").forEach(function (b) {
|
||||||
|
var isActive;
|
||||||
|
if (b.dataset.cat === "__dash") isActive = (_view === "dashboard");
|
||||||
|
else isActive = (_view === "services" && b.dataset.cat === _cat);
|
||||||
|
b.classList.toggle("active", isActive);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDashboard() {
|
||||||
|
_view = "dashboard";
|
||||||
|
_cat = "all";
|
||||||
|
if ($welcome) $welcome.style.display = "";
|
||||||
|
if ($tilesArea) $tilesArea.style.display = "none";
|
||||||
|
setTitle("Dashboard");
|
||||||
|
syncNav();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showServices(cat) {
|
||||||
|
_view = "services";
|
||||||
|
if (cat) _cat = cat;
|
||||||
|
if ($welcome) $welcome.style.display = "none";
|
||||||
|
if ($tilesArea) $tilesArea.style.display = "";
|
||||||
|
setTitle(_cat === "all" ? "All services" : catLabel(_cat));
|
||||||
|
syncNav();
|
||||||
|
applyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Category navigation ──────────────────────────────────────── */
|
||||||
|
|
||||||
|
function renderNav() {
|
||||||
|
if (!$nav) return;
|
||||||
|
var counts = {};
|
||||||
|
var order = [];
|
||||||
|
visibleServices().forEach(function (s) {
|
||||||
|
var cat = s.category || "other";
|
||||||
|
if (CATEGORY_ALIASES[cat]) cat = CATEGORY_ALIASES[cat];
|
||||||
|
if (!counts[cat]) { counts[cat] = 0; order.push(cat); }
|
||||||
|
counts[cat]++;
|
||||||
|
});
|
||||||
|
var total = visibleServices().length;
|
||||||
|
|
||||||
|
var html = '<div class="nav-label">Menu</div>';
|
||||||
|
html += '<button class="nav-item' + (_view === "dashboard" ? " active" : "") + '" data-cat="__dash" type="button">' +
|
||||||
|
icon("g-home") +
|
||||||
|
'<span class="nav-text">Dashboard</span>' +
|
||||||
|
'</button>';
|
||||||
|
html += '<div class="nav-label">Services</div>';
|
||||||
|
html += navItem("all", "All services", total);
|
||||||
|
order.forEach(function (cat) {
|
||||||
|
html += navItem(cat, catLabel(cat), counts[cat]);
|
||||||
|
});
|
||||||
|
$nav.innerHTML = html;
|
||||||
|
|
||||||
|
$nav.querySelectorAll(".nav-item").forEach(function (btn) {
|
||||||
|
btn.addEventListener("click", function () {
|
||||||
|
if (btn.dataset.cat === "__dash") showDashboard();
|
||||||
|
else showServices(btn.dataset.cat);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function navItem(cat, label, count) {
|
||||||
|
return '<button class="nav-item' + (_view === "services" && cat === _cat ? " active" : "") + '" data-cat="' + escHtml(cat) + '" type="button">' +
|
||||||
|
icon(CAT_ICONS[cat] || "g-dots") +
|
||||||
|
'<span class="nav-text">' + escHtml(label) + '</span>' +
|
||||||
|
'<span class="nav-count">' + count + '</span>' +
|
||||||
|
'</button>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function catLabel(cat) {
|
||||||
|
if (typeof _categoryLabels !== "undefined" && _categoryLabels[cat]) return _categoryLabels[cat];
|
||||||
|
return CAT_FALLBACK_LABELS[cat] || cat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Filtering (applies in the services view) ─────────────────── */
|
||||||
|
|
||||||
|
function applyFilter() {
|
||||||
|
if (_view !== "services") return;
|
||||||
|
var area = $tilesArea;
|
||||||
|
if (!area) return;
|
||||||
|
var q = _query.trim().toLowerCase();
|
||||||
|
area.querySelectorAll(".category-section").forEach(function (section) {
|
||||||
|
var catMatch = (_cat === "all") || (section.dataset.category === _cat);
|
||||||
|
var anyVisible = false;
|
||||||
|
section.querySelectorAll(".service-tile").forEach(function (tile) {
|
||||||
|
var nameEl = tile.querySelector(".tile-name");
|
||||||
|
var name = nameEl ? nameEl.textContent.toLowerCase() : "";
|
||||||
|
var match = catMatch && (!q || name.indexOf(q) !== -1);
|
||||||
|
tile.style.display = match ? "" : "none";
|
||||||
|
if (match) anyVisible = true;
|
||||||
|
});
|
||||||
|
section.style.display = (catMatch && (anyVisible || !q)) ? "" : "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$search.addEventListener("input", function () {
|
||||||
|
// Searching implies browsing services — leave the welcome view.
|
||||||
|
if (_view === "dashboard" && $search.value) showServices("all");
|
||||||
|
_query = $search.value;
|
||||||
|
applyFilter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($browseBtn) {
|
||||||
|
$browseBtn.addEventListener("click", function () { showServices("all"); });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Welcome header ───────────────────────────────────────────── */
|
||||||
|
|
||||||
|
function updateWelcomeMeta() {
|
||||||
|
if ($greeting) {
|
||||||
|
var h = new Date().getHours();
|
||||||
|
var g;
|
||||||
|
if (h >= 5 && h < 12) g = "Good morning";
|
||||||
|
else if (h >= 12 && h < 17) g = "Good afternoon";
|
||||||
|
else g = "Good evening";
|
||||||
|
$greeting.textContent = g;
|
||||||
|
}
|
||||||
|
if ($welcomeRole) {
|
||||||
|
$welcomeRole.textContent = (typeof window._roleLabel !== "undefined" && window._roleLabel) ? window._roleLabel : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status cards ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
function serviceCounts() {
|
||||||
|
var services = visibleServices();
|
||||||
|
var running = 0, attention = 0, off = 0;
|
||||||
|
var attentionNames = [], offNames = [];
|
||||||
|
services.forEach(function (s) {
|
||||||
|
if (!s.enabled) { off++; offNames.push(s.name); return; }
|
||||||
|
var h = s.health || s.status;
|
||||||
|
if (h === "needs_attention" || h === "failed") { attention++; attentionNames.push(s.name); }
|
||||||
|
else running++;
|
||||||
|
});
|
||||||
|
return { services: services, running: running, attention: attention, off: off, attentionNames: attentionNames, offNames: offNames };
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWidgets() {
|
||||||
|
if (!$wcSystems) return;
|
||||||
|
var c = serviceCounts();
|
||||||
|
if (!c.services.length) { $wcSystems.innerHTML = ""; if ($wcMore) $wcMore.innerHTML = ""; return; }
|
||||||
|
|
||||||
|
/* Systems operational */
|
||||||
|
var sub = '<b>' + c.running + '</b> running';
|
||||||
|
if (c.attention) sub += ' · <span class="warn">' + c.attention + ' needs attention</span>';
|
||||||
|
if (c.off) sub += ' · ' + c.off + ' off';
|
||||||
|
var attentionTitle = c.attention ? "Systems need attention" : "Systems operational";
|
||||||
|
$wcSystems.innerHTML =
|
||||||
|
'<div class="widget clickable" id="w-systems" role="button" tabindex="0" title="System status and router setup">' +
|
||||||
|
'<div class="widget-chip chip-green">' + icon("g-pulse") + '</div>' +
|
||||||
|
'<div class="w-body"><h3>' + attentionTitle + '</h3><div class="sub">' + sub + '</div></div>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
var wSys = document.getElementById("w-systems");
|
||||||
|
if (wSys) {
|
||||||
|
wSys.addEventListener("click", openSystemsModal);
|
||||||
|
wSys.addEventListener("keydown", function (e) { if (e.key === "Enter") openSystemsModal(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$wcMore) return;
|
||||||
|
var more = "";
|
||||||
|
|
||||||
|
/* Bitcoin Core sync */
|
||||||
|
var btc = null;
|
||||||
|
c.services.forEach(function (s) {
|
||||||
|
if (s.sync_ibd && s.enabled) btc = s;
|
||||||
|
});
|
||||||
|
if (btc) {
|
||||||
|
var pct = Math.round((btc.sync_progress || 0) * 100);
|
||||||
|
var blocks = btc.sync_blocks ? btc.sync_blocks.toLocaleString() : "—";
|
||||||
|
var eta = (typeof _calcBtcEta === "function") ? _calcBtcEta(btc.unit + "::" + btc.name, btc.sync_progress || 0) : "";
|
||||||
|
more +=
|
||||||
|
'<div class="widget clickable" id="w-btc" role="button" tabindex="0" title="' + escHtml(btc.name) + ' details">' +
|
||||||
|
'<div class="widget-chip chip-btc"><img src="/static/icons/' + escHtml(btc.icon) + '.svg" alt="" style="width:46px;height:46px;display:block;object-fit:contain"/></div>' +
|
||||||
|
'<div class="w-body"><h3>' + escHtml(btc.name) + ' — syncing timechain</h3>' +
|
||||||
|
'<div class="w-bar"><div class="w-bar-fill" style="width:' + pct + '%"></div></div>' +
|
||||||
|
'<div class="sub">Block <b>' + blocks + '</b> · ' + pct + '% · <span class="warn">' + escHtml(eta) + '</span></div></div>' +
|
||||||
|
'</div>';
|
||||||
|
} else {
|
||||||
|
var btcDone = null;
|
||||||
|
c.services.forEach(function (s) { if (s.unit === "bitcoind.service" && s.enabled) btcDone = s; });
|
||||||
|
if (btcDone) {
|
||||||
|
var blk = btcDone.sync_blocks ? btcDone.sync_blocks.toLocaleString() : "";
|
||||||
|
more +=
|
||||||
|
'<div class="widget clickable" id="w-btc" role="button" tabindex="0" title="' + escHtml(btcDone.name) + ' details">' +
|
||||||
|
'<div class="widget-chip chip-btc"><img src="/static/icons/' + escHtml(btcDone.icon) + '.svg" alt="" style="width:46px;height:46px;display:block;object-fit:contain"/></div>' +
|
||||||
|
'<div class="w-body"><h3>' + escHtml(btcDone.name) + '</h3><div class="sub"><span class="good">Fully synced</span>' + (blk ? ' · Block <b>' + blk + '</b>' : '') + '</div></div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Updates — mirror the sidebar's read of the update state, so the card
|
||||||
|
never claims "up to date" while an update failed or needs a restart */
|
||||||
|
var upd = (typeof window._lastUpdateCheck === "object" && window._lastUpdateCheck) ? window._lastUpdateCheck : null;
|
||||||
|
var updStatus = (upd && upd.status) || "idle";
|
||||||
|
var updTitle, updSub, updChip;
|
||||||
|
if (updStatus === "failed") {
|
||||||
|
updTitle = "Update failed"; updSub = "Click to retry the update"; updChip = "chip-amber";
|
||||||
|
} else if (updStatus === "reboot_required") {
|
||||||
|
updTitle = "Restart required"; updSub = "Click to restart and finish the update"; updChip = "chip-amber";
|
||||||
|
} else if (updStatus === "running") {
|
||||||
|
updTitle = "Update in progress"; updSub = "Installing the new system generation"; updChip = "chip-amber";
|
||||||
|
} else if (upd && upd.available) {
|
||||||
|
updTitle = "Updates available"; updSub = "Click to review and update"; updChip = "chip-amber";
|
||||||
|
} else if (!upd) {
|
||||||
|
/* First check hasn't returned yet — never claim "up to date" before
|
||||||
|
a check has actually completed */
|
||||||
|
updTitle = "Checking for updates"; updSub = "Comparing with the latest Sovran_SystemsOS release"; updChip = "chip-green";
|
||||||
|
} else {
|
||||||
|
/* Updates are applied by the user, not automatically — the card must
|
||||||
|
not imply the OS updates itself */
|
||||||
|
updTitle = "System is up to date"; updSub = "Last check found no updates · Click to check again"; updChip = "chip-green";
|
||||||
|
}
|
||||||
|
more +=
|
||||||
|
'<div class="widget clickable" id="w-updates" role="button" tabindex="0" title="Check for system updates">' +
|
||||||
|
'<div class="widget-chip ' + updChip + '">' + icon("g-update") + '</div>' +
|
||||||
|
'<div class="w-body"><h3>' + updTitle + '</h3>' +
|
||||||
|
'<div class="sub">' + updSub + '</div></div>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
$wcMore.innerHTML = more;
|
||||||
|
|
||||||
|
var wBtc = document.getElementById("w-btc");
|
||||||
|
if (wBtc) {
|
||||||
|
var svc = btc || btcDone;
|
||||||
|
if (svc) {
|
||||||
|
wBtc.addEventListener("click", function () { openServiceDetailModal(svc.unit, svc.name, svc.icon); });
|
||||||
|
wBtc.addEventListener("keydown", function (e) { if (e.key === "Enter") openServiceDetailModal(svc.unit, svc.name, svc.icon); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var wUpd = document.getElementById("w-updates");
|
||||||
|
if (wUpd) {
|
||||||
|
wUpd.addEventListener("click", function () { openUpdateModal(); });
|
||||||
|
wUpd.addEventListener("keydown", function (e) { if (e.key === "Enter") openUpdateModal(); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Systems Operational modal ────────────────────────────────── */
|
||||||
|
|
||||||
|
function isNodeRole() {
|
||||||
|
return (typeof _currentRole !== "undefined" && _currentRole === "node");
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasEnabledDomainService(services) {
|
||||||
|
return DOMAIN_UNITS.some(function (u) {
|
||||||
|
return services.some(function (s) { return s.unit === u && s.enabled; });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function whoUsesPorts() {
|
||||||
|
if (isNodeRole()) {
|
||||||
|
return 'On this <strong>Bitcoin Node</strong> install, <strong>BTCPay Server</strong> and <strong>Lightning Wallet Connections (LNURL)</strong> are the domain services that use these ports.';
|
||||||
|
}
|
||||||
|
return 'All your domain services share ports 80 and 443 — Matrix, BTCPay Server, VaultWarden, Nextcloud, WordPress, Haven Relay, Lightning Wallet Connections, and Element Calling.';
|
||||||
|
}
|
||||||
|
|
||||||
|
function step(n, title, sub, value) {
|
||||||
|
return '<div class="sysstep"><div class="sysnum">' + n + '</div><div class="sysstep-x">' +
|
||||||
|
'<div class="sysstep-t">' + title + (sub ? ' <span class="sysstep-sub">· ' + sub + '</span>' : '') + '</div>' +
|
||||||
|
(value ? '<div class="sysval"><span class="sysval-text">' + value + '</span></div>' : '') +
|
||||||
|
'</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSystemsModal() {
|
||||||
|
if (!$sysModal || !$sysBody) return;
|
||||||
|
var c = serviceCounts();
|
||||||
|
|
||||||
|
var html = "";
|
||||||
|
|
||||||
|
/* System status */
|
||||||
|
html += '<div class="sysmodal-card">' +
|
||||||
|
'<div class="sysmodal-card-title">' + icon("g-pulse") + 'System Status</div>' +
|
||||||
|
step(1, "Services running", "", String(c.running)) +
|
||||||
|
step(2, "Needs attention", "", c.attention ? escHtml(c.attentionNames.join(", ")) : "None") +
|
||||||
|
step(3, "Turned off", "", c.off ? escHtml(c.offNames.join(", ")) : "None") +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
/* Router — a simple open / not-open verdict. How to open the ports is
|
||||||
|
covered during onboarding, so the modal does not repeat instructions.
|
||||||
|
When no domain service is enabled (a fresh Desktop-only install, a
|
||||||
|
Node with BTCPay/LNURL off, or everything turned off), there is
|
||||||
|
nothing to check on the router — say so instead of listing services
|
||||||
|
this machine does not have. */
|
||||||
|
if (!hasEnabledDomainService(c.services)) {
|
||||||
|
var noRouterDesc = isNodeRole()
|
||||||
|
? 'Ports 80 and 443 only need to be forwarded on your router if you turn on <strong>BTCPay Server</strong> or <strong>Lightning Wallet Connections (LNURL)</strong>. If you enable one of them, come back here to check your ports.'
|
||||||
|
: 'None of your services need ports forwarded from your router right now. If you turn on a service that uses a domain, come back here to check your ports.';
|
||||||
|
html += '<div class="sysmodal-card">' +
|
||||||
|
'<div class="sysmodal-card-title">' + icon("g-wifi") + 'Router</div>' +
|
||||||
|
'<div class="sysnote"><div class="sysnote-title">' + icon("g-check") + 'No router setup needed yet</div>' +
|
||||||
|
'<div class="sysnote-desc">' + noRouterDesc + '</div></div>' +
|
||||||
|
'</div>';
|
||||||
|
} else {
|
||||||
|
html += '<div class="sysmodal-card" id="sys-ports-card" style="display:none">' +
|
||||||
|
'<div class="sysmodal-card-title">' + icon("g-wifi") + 'Router</div>' +
|
||||||
|
'<div id="sys-ports-status"><div class="sysfineprint">Checking…</div></div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Who uses these ports (only meaningful when a domain service is
|
||||||
|
actually enabled — otherwise the router note above covers it) */
|
||||||
|
if (hasEnabledDomainService(c.services)) {
|
||||||
|
html += '<div class="sysnote" style="margin-top:14px">' +
|
||||||
|
'<div class="sysnote-title">' + icon("g-antenna") + 'Who uses these ports</div>' +
|
||||||
|
'<div class="sysnote-desc">' + whoUsesPorts() + '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$sysBody.innerHTML = html;
|
||||||
|
$sysModal.classList.add("open");
|
||||||
|
|
||||||
|
/* Poll the first configured domain service and reduce its diagnostics
|
||||||
|
to one verdict: the ports are open or they are not. */
|
||||||
|
var portsCard = document.getElementById("sys-ports-card");
|
||||||
|
var portsEl = document.getElementById("sys-ports-status");
|
||||||
|
var units = DOMAIN_UNITS.filter(function (u) {
|
||||||
|
return c.services.some(function (s) { return s.unit === u && s.enabled; });
|
||||||
|
});
|
||||||
|
if (!units.length || !portsCard || !portsEl) return;
|
||||||
|
portsCard.style.display = "";
|
||||||
|
|
||||||
|
apiFetch("/api/service-detail/" + encodeURIComponent(units[0]))
|
||||||
|
.then(function (data) {
|
||||||
|
var steps = (data && data.domain_check_steps) || [];
|
||||||
|
var portsStep = null;
|
||||||
|
steps.forEach(function (s) {
|
||||||
|
if (Number(s.step) === 3 || /ports?\s*80/i.test(s.label || "")) portsStep = s;
|
||||||
|
});
|
||||||
|
if (!portsStep) { portsCard.style.display = "none"; return; }
|
||||||
|
if (portsStep.status === "ok") {
|
||||||
|
portsEl.innerHTML = '<div class="svc-detail-status" style="font-size:0.92rem"><span class="status-dot active"></span>Ports 80 and 443 are open</div>';
|
||||||
|
} else {
|
||||||
|
portsEl.innerHTML = '<div class="svc-detail-status" style="font-size:0.92rem"><span class="status-dot failed"></span>Ports 80 and 443 are not open</div>';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
portsCard.style.display = "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSystemsModal() {
|
||||||
|
if ($sysModal) $sysModal.classList.remove("open");
|
||||||
|
}
|
||||||
|
|
||||||
|
var sysClose = document.getElementById("systems-close-btn");
|
||||||
|
if (sysClose) sysClose.addEventListener("click", closeSystemsModal);
|
||||||
|
if ($sysModal) {
|
||||||
|
$sysModal.addEventListener("click", function (e) {
|
||||||
|
if (e.target === $sysModal) closeSystemsModal();
|
||||||
|
});
|
||||||
|
$sysModal.addEventListener("keydown", function (e) {
|
||||||
|
if (e.key === "Escape") closeSystemsModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Public hook for tiles.js ────────────────────────────────── */
|
||||||
|
|
||||||
|
window.dashboardServicesUpdated = function () {
|
||||||
|
renderNav();
|
||||||
|
updateWelcomeMeta();
|
||||||
|
renderWidgets();
|
||||||
|
applyFilter();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial view
|
||||||
|
showDashboard();
|
||||||
|
updateWelcomeMeta();
|
||||||
|
|
||||||
|
})();
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
// if ($updateBtn) $updateBtn.addEventListener("click", openUpdateModal); // moved to sidebar in tiles.js
|
// if ($updateBtn) $updateBtn.addEventListener("click", openUpdateModal); // moved to sidebar in tiles.js
|
||||||
if ($btnCloseModal) $btnCloseModal.addEventListener("click", closeUpdateModal);
|
if ($btnCloseModal) $btnCloseModal.addEventListener("click", closeUpdateModal);
|
||||||
|
if ($btnCheckAgain) $btnCheckAgain.addEventListener("click", function() { openUpdateModal(); });
|
||||||
|
if ($updateCloseBtn) $updateCloseBtn.addEventListener("click", closeUpdateModal);
|
||||||
if ($btnReboot) $btnReboot.addEventListener("click", doReboot);
|
if ($btnReboot) $btnReboot.addEventListener("click", doReboot);
|
||||||
if ($btnSave) $btnSave.addEventListener("click", saveErrorReport);
|
if ($btnSave) $btnSave.addEventListener("click", saveErrorReport);
|
||||||
if ($btnRetryUpdate) $btnRetryUpdate.addEventListener("click", retryUpdateStatus);
|
if ($btnRetryUpdate) $btnRetryUpdate.addEventListener("click", retryUpdateStatus);
|
||||||
@@ -28,6 +30,7 @@ if ($logoutBtn) $logoutBtn.addEventListener("click", function () {
|
|||||||
|
|
||||||
// Rebuild modal
|
// Rebuild modal
|
||||||
if ($rebuildClose) $rebuildClose.addEventListener("click", closeRebuildModal);
|
if ($rebuildClose) $rebuildClose.addEventListener("click", closeRebuildModal);
|
||||||
|
if ($rebuildCloseHdr) $rebuildCloseHdr.addEventListener("click", closeRebuildModal);
|
||||||
if ($rebuildReboot) $rebuildReboot.addEventListener("click", doReboot);
|
if ($rebuildReboot) $rebuildReboot.addEventListener("click", doReboot);
|
||||||
if ($rebuildSave) $rebuildSave.addEventListener("click", saveRebuildErrorReport);
|
if ($rebuildSave) $rebuildSave.addEventListener("click", saveRebuildErrorReport);
|
||||||
if ($rebuildModal) $rebuildModal.addEventListener("click", function(e) { if (e.target === $rebuildModal) closeRebuildModal(); });
|
if ($rebuildModal) $rebuildModal.addEventListener("click", function(e) { if (e.target === $rebuildModal) closeRebuildModal(); });
|
||||||
@@ -175,9 +178,9 @@ function showSecurityBanner() {
|
|||||||
'</div>' +
|
'</div>' +
|
||||||
'<button class="security-banner-dismiss" id="security-banner-dismiss-btn" title="Dismiss">\u2715</button>';
|
'<button class="security-banner-dismiss" id="security-banner-dismiss-btn" title="Dismiss">\u2715</button>';
|
||||||
|
|
||||||
var mainContent = document.querySelector(".main-content");
|
var contentArea = document.querySelector(".content");
|
||||||
if (mainContent) {
|
if (contentArea) {
|
||||||
mainContent.insertAdjacentElement("beforebegin", banner);
|
contentArea.insertAdjacentElement("afterbegin", banner);
|
||||||
} else {
|
} else {
|
||||||
document.body.insertAdjacentElement("afterbegin", banner);
|
document.body.insertAdjacentElement("afterbegin", banner);
|
||||||
}
|
}
|
||||||
@@ -239,10 +242,11 @@ async function init() {
|
|||||||
}
|
}
|
||||||
var badge = document.getElementById("role-badge");
|
var badge = document.getElementById("role-badge");
|
||||||
if (badge && cfg.role_label) badge.textContent = cfg.role_label;
|
if (badge && cfg.role_label) badge.textContent = cfg.role_label;
|
||||||
|
window._roleLabel = cfg.role_label || "";
|
||||||
|
|
||||||
await refreshServices();
|
|
||||||
loadNetwork();
|
loadNetwork();
|
||||||
checkUpdates();
|
checkUpdates();
|
||||||
|
await refreshServices();
|
||||||
|
|
||||||
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
|
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
|
||||||
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
|
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
|
||||||
@@ -252,9 +256,9 @@ async function init() {
|
|||||||
}
|
}
|
||||||
loadAutolaunchToggle();
|
loadAutolaunchToggle();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
await refreshServices();
|
|
||||||
loadNetwork();
|
loadNetwork();
|
||||||
checkUpdates();
|
checkUpdates();
|
||||||
|
await refreshServices();
|
||||||
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
|
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
|
||||||
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
|
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
|
||||||
loadAutolaunchToggle();
|
loadAutolaunchToggle();
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ function openDomainSetupModal(feat, onSaved) {
|
|||||||
nwcWarning +
|
nwcWarning +
|
||||||
renderDomainNeedsHtml({ serviceName: feat.name, hostExample: hostExample, purpose: purpose }) +
|
renderDomainNeedsHtml({ serviceName: feat.name, hostExample: hostExample, purpose: purpose }) +
|
||||||
renderNjallaStepsHtml({ hostExample: hostExample, pasteHint: "below" }) +
|
renderNjallaStepsHtml({ hostExample: hostExample, pasteHint: "below" }) +
|
||||||
'<div class="onboarding-port-warn" id="domain-router-box" style="margin-top:12px;"></div>' +
|
|
||||||
'<p style="margin-top:10px;">Enter the address for this service and paste the update command from Njal.la.</p>' +
|
'<p style="margin-top:10px;">Enter the address for this service and paste the update command from Njal.la.</p>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service address (e.g. ' + domainLabelExample + '):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="' + domainPlaceholder + '" /></div>' +
|
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service address (e.g. ' + domainLabelExample + '):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="' + domainPlaceholder + '" /></div>' +
|
||||||
@@ -144,8 +143,6 @@ function openDomainSetupModal(feat, onSaved) {
|
|||||||
|
|
||||||
$domainSetupModal.classList.add("open");
|
$domainSetupModal.classList.add("open");
|
||||||
|
|
||||||
// Fill the router port-forwarding box with this computer's LAN IP (best-effort)
|
|
||||||
renderRouterPortsBox("domain-router-box");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openDomainReconfigureModal(feat, existingDomain, onSaved) {
|
function openDomainReconfigureModal(feat, existingDomain, onSaved) {
|
||||||
@@ -193,9 +190,7 @@ function openDomainReconfigureModal(feat, existingDomain, onSaved) {
|
|||||||
'<span style="display:inline-block;margin-top:4px;padding:4px 10px;background:var(--card-color);border:1px solid var(--border-color);border-radius:6px;font-family:monospace;font-size:1em;font-weight:700;">' + escHtml(externalIp) + '</span></li>' +
|
'<span style="display:inline-block;margin-top:4px;padding:4px 10px;background:var(--card-color);border:1px solid var(--border-color);border-radius:6px;font-family:monospace;font-size:1em;font-weight:700;">' + escHtml(externalIp) + '</span></li>' +
|
||||||
'<li>If the IP is wrong or the record is missing, update it</li>' +
|
'<li>If the IP is wrong or the record is missing, update it</li>' +
|
||||||
'<li>If you changed the DDNS curl command, paste the updated one below</li>' +
|
'<li>If you changed the DDNS curl command, paste the updated one below</li>' +
|
||||||
'<li>Confirm ports <strong>80</strong> and <strong>443</strong> (TCP) are still forwarded on your router to this computer — see the reminder below:</li>' +
|
|
||||||
'</ol>' +
|
'</ol>' +
|
||||||
'<div class="onboarding-port-warn" id="domain-router-box" style="margin-top:12px;"></div>' +
|
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service domain (e.g. ' + domainLabelExample + '):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="' + domainPlaceholder + '" value="' + escHtml(currentDomain) + '" /></div>' +
|
'<div class="domain-field-group"><label class="domain-field-label" for="domain-subdomain-input">Service domain (e.g. ' + domainLabelExample + '):</label><input class="domain-field-input" type="text" id="domain-subdomain-input" placeholder="' + domainPlaceholder + '" value="' + escHtml(currentDomain) + '" /></div>' +
|
||||||
'<div class="domain-field-group"><label class="domain-field-label" for="domain-ddns-input">Njal.la Dynamic DNS Update Command:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="curl "https://njal.la/update/?h=' + domainPlaceholder + '&k=abc123&auto"" /><p class="domain-field-hint">ℹ Paste the full curl command from your Njal.la dashboard\'s Dynamic record</p></div>' +
|
'<div class="domain-field-group"><label class="domain-field-label" for="domain-ddns-input">Njal.la Dynamic DNS Update Command:</label><input class="domain-field-input" type="text" id="domain-ddns-input" placeholder="curl "https://njal.la/update/?h=' + domainPlaceholder + '&k=abc123&auto"" /><p class="domain-field-hint">ℹ Paste the full curl command from your Njal.la dashboard\'s Dynamic record</p></div>' +
|
||||||
@@ -241,8 +236,6 @@ function openDomainReconfigureModal(feat, existingDomain, onSaved) {
|
|||||||
|
|
||||||
$domainSetupModal.classList.add("open");
|
$domainSetupModal.classList.add("open");
|
||||||
|
|
||||||
// Fill the router port-forwarding box with this computer's LAN IP (best-effort)
|
|
||||||
renderRouterPortsBox("domain-router-box");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDomainSetupModal() {
|
function closeDomainSetupModal() {
|
||||||
@@ -635,16 +628,16 @@ async function loadAutolaunchToggle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAutolaunchToggle(enabled) {
|
function renderAutolaunchToggle(enabled) {
|
||||||
// Remove existing section if any
|
// The preference lives in the Systems Operational modal (moved from the sidebar)
|
||||||
var old = $sidebarFeatures.querySelector(".autolaunch-section");
|
var slot = document.getElementById("autolaunch-slot");
|
||||||
|
if (!slot) return;
|
||||||
|
var old = slot.querySelector(".autolaunch-section");
|
||||||
if (old) old.parentNode.removeChild(old);
|
if (old) old.parentNode.removeChild(old);
|
||||||
|
|
||||||
var section = document.createElement("div");
|
var section = document.createElement("div");
|
||||||
section.className = "category-section autolaunch-section";
|
section.className = "category-section autolaunch-section";
|
||||||
|
|
||||||
section.innerHTML =
|
section.innerHTML =
|
||||||
'<div class="section-header">Preferences</div>' +
|
|
||||||
'<hr class="section-divider" />' +
|
|
||||||
'<div class="feature-card">' +
|
'<div class="feature-card">' +
|
||||||
'<div class="feature-card-top">' +
|
'<div class="feature-card-top">' +
|
||||||
'<div class="feature-card-info">' +
|
'<div class="feature-card-info">' +
|
||||||
@@ -658,7 +651,7 @@ function renderAutolaunchToggle(enabled) {
|
|||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
|
|
||||||
$sidebarFeatures.appendChild(section);
|
slot.appendChild(section);
|
||||||
|
|
||||||
var input = document.getElementById("autolaunch-toggle-input");
|
var input = document.getElementById("autolaunch-toggle-input");
|
||||||
var label = document.getElementById("autolaunch-toggle-label");
|
var label = document.getElementById("autolaunch-toggle-label");
|
||||||
|
|||||||
@@ -2,6 +2,35 @@
|
|||||||
|
|
||||||
// ── Rebuild modal ─────────────────────────────────────────────────
|
// ── Rebuild modal ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Status line + header pill for the rebuild dialog (same presentation
|
||||||
|
// contract as the update dialog's _setUpdateStatus).
|
||||||
|
function _setRebuildStatus(text) {
|
||||||
|
if ($rebuildStatus) $rebuildStatus.textContent = text;
|
||||||
|
if ($rebuildPill) {
|
||||||
|
var cls = "upd-pill";
|
||||||
|
var html;
|
||||||
|
if (text.charAt(0) === "✓") {
|
||||||
|
if (text.indexOf("restart required") !== -1) {
|
||||||
|
cls += " st-needs-attention"; html = '<span class="status-dot needs-attention pulse"></span>Restart required';
|
||||||
|
} else {
|
||||||
|
cls += " st-active"; html = '<span class="status-dot active"></span>Done';
|
||||||
|
}
|
||||||
|
} else if (text.charAt(0) === "✗") {
|
||||||
|
cls += " st-failed"; html = '<span class="status-dot failed"></span>Failed';
|
||||||
|
} else {
|
||||||
|
cls += " st-loading"; html = '<span class="status-dot loading pulse"></span>Applying…';
|
||||||
|
}
|
||||||
|
$rebuildPill.className = cls;
|
||||||
|
$rebuildPill.innerHTML = html;
|
||||||
|
}
|
||||||
|
if ($rebuildStatus) {
|
||||||
|
var msg = "update-status-msg";
|
||||||
|
if (text.charAt(0) === "✓") $rebuildStatus.className = (text.indexOf("restart required") !== -1) ? msg + " st-warn" : msg + " st-ok";
|
||||||
|
else if (text.charAt(0) === "✗") $rebuildStatus.className = msg + " st-err";
|
||||||
|
else $rebuildStatus.className = msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function openRebuildModal() {
|
function openRebuildModal() {
|
||||||
if (!$rebuildModal) return;
|
if (!$rebuildModal) return;
|
||||||
_rebuildLog = "";
|
_rebuildLog = "";
|
||||||
@@ -13,11 +42,12 @@ function openRebuildModal() {
|
|||||||
if ($rebuildLog) { $rebuildLog.textContent = ""; $rebuildLog.style.display = "none"; }
|
if ($rebuildLog) { $rebuildLog.textContent = ""; $rebuildLog.style.display = "none"; }
|
||||||
var action = _rebuildIsEnabling ? "Enabling" : "Disabling";
|
var action = _rebuildIsEnabling ? "Enabling" : "Disabling";
|
||||||
var label = _rebuildFeatureName || "feature";
|
var label = _rebuildFeatureName || "feature";
|
||||||
if ($rebuildStatus) $rebuildStatus.textContent = action + " " + label + "…";
|
_setRebuildStatus(action + " " + label + "…");
|
||||||
if ($rebuildSpinner) $rebuildSpinner.classList.add("spinning");
|
if ($rebuildSpinner) $rebuildSpinner.classList.add("spinning");
|
||||||
if ($rebuildReboot) $rebuildReboot.style.display = "none";
|
if ($rebuildReboot) $rebuildReboot.style.display = "none";
|
||||||
if ($rebuildSave) $rebuildSave.style.display = "none";
|
if ($rebuildSave) $rebuildSave.style.display = "none";
|
||||||
if ($rebuildClose) $rebuildClose.disabled = true;
|
if ($rebuildClose) $rebuildClose.disabled = true;
|
||||||
|
if ($rebuildCloseHdr) $rebuildCloseHdr.disabled = true;
|
||||||
$rebuildModal.classList.add("open");
|
$rebuildModal.classList.add("open");
|
||||||
// Delay first poll slightly to let the rebuild service start and clear stale log
|
// Delay first poll slightly to let the rebuild service start and clear stale log
|
||||||
setTimeout(startRebuildPoll, 1500);
|
setTimeout(startRebuildPoll, 1500);
|
||||||
@@ -76,7 +106,7 @@ async function pollRebuildStatus() {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!_rebuildServerDown) { _rebuildServerDown = true; if ($rebuildStatus) $rebuildStatus.textContent = "Applying changes…"; }
|
if (!_rebuildServerDown) { _rebuildServerDown = true; _setRebuildStatus("Applying changes…"); }
|
||||||
} finally {
|
} finally {
|
||||||
_rebuildPollInFlight = false;
|
_rebuildPollInFlight = false;
|
||||||
}
|
}
|
||||||
@@ -85,15 +115,16 @@ async function pollRebuildStatus() {
|
|||||||
function onRebuildDone(result) {
|
function onRebuildDone(result) {
|
||||||
if ($rebuildSpinner) $rebuildSpinner.classList.remove("spinning");
|
if ($rebuildSpinner) $rebuildSpinner.classList.remove("spinning");
|
||||||
if ($rebuildClose) $rebuildClose.disabled = false;
|
if ($rebuildClose) $rebuildClose.disabled = false;
|
||||||
|
if ($rebuildCloseHdr) $rebuildCloseHdr.disabled = false;
|
||||||
if (result === true) {
|
if (result === true) {
|
||||||
if ($rebuildStatus) $rebuildStatus.textContent = "✓ Done";
|
_setRebuildStatus("✓ Done");
|
||||||
// Auto-reload the page after a short delay so tiles and toggles reflect the new state
|
// Auto-reload the page after a short delay so tiles and toggles reflect the new state
|
||||||
setTimeout(function() { window.location.reload(); }, 1200);
|
setTimeout(function() { window.location.reload(); }, 1200);
|
||||||
} else if (result === "reboot_required") {
|
} else if (result === "reboot_required") {
|
||||||
if ($rebuildStatus) $rebuildStatus.textContent = "✓ Done — restart required";
|
_setRebuildStatus("✓ Done — restart required");
|
||||||
if ($rebuildReboot) $rebuildReboot.style.display = "inline-flex";
|
if ($rebuildReboot) $rebuildReboot.style.display = "inline-flex";
|
||||||
} else {
|
} else {
|
||||||
if ($rebuildStatus) $rebuildStatus.textContent = "✗ Something went wrong";
|
_setRebuildStatus("✗ Something went wrong");
|
||||||
if ($rebuildSave) $rebuildSave.style.display = "inline-flex";
|
if ($rebuildSave) $rebuildSave.style.display = "inline-flex";
|
||||||
if ($rebuildReboot) $rebuildReboot.style.display = "inline-flex";
|
if ($rebuildReboot) $rebuildReboot.style.display = "inline-flex";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,12 @@
|
|||||||
function openSecurityModal() {
|
function openSecurityModal() {
|
||||||
if ($supportModal) $supportModal.classList.add("open");
|
if ($supportModal) $supportModal.classList.add("open");
|
||||||
var title = document.getElementById("support-modal-title");
|
var title = document.getElementById("support-modal-title");
|
||||||
if (title) title.textContent = "\uD83D\uDEE1 Security";
|
if (title) title.textContent = "Security";
|
||||||
|
var chip = document.getElementById("support-modal-chip");
|
||||||
|
if (chip) {
|
||||||
|
var use = chip.querySelector("use");
|
||||||
|
if (use) use.setAttribute("href", "#g-shield-check");
|
||||||
|
}
|
||||||
|
|
||||||
if ($supportBody) {
|
if ($supportBody) {
|
||||||
$supportBody.innerHTML =
|
$supportBody.innerHTML =
|
||||||
|
|||||||
@@ -696,6 +696,17 @@ async function openServiceDetailModal(unit, name, icon) {
|
|||||||
: (data.health || data.status);
|
: (data.health || data.status);
|
||||||
var sc = statusClass(effectiveHealth);
|
var sc = statusClass(effectiveHealth);
|
||||||
var st = statusText(effectiveHealth, effectiveEnabled);
|
var st = statusText(effectiveHealth, effectiveEnabled);
|
||||||
|
if ($credsTitle) {
|
||||||
|
var existingPill = $credsTitle.querySelector(".creds-title-status-pill");
|
||||||
|
if (!existingPill) {
|
||||||
|
var pill = document.createElement("span");
|
||||||
|
pill.className = "creds-title-status-pill";
|
||||||
|
$credsTitle.appendChild(pill);
|
||||||
|
existingPill = pill;
|
||||||
|
}
|
||||||
|
existingPill.className = "creds-title-status-pill st-" + sc;
|
||||||
|
existingPill.textContent = st;
|
||||||
|
}
|
||||||
addSetup('<div class="svc-detail-section">' +
|
addSetup('<div class="svc-detail-section">' +
|
||||||
'<div class="svc-detail-section-title">Status</div>' +
|
'<div class="svc-detail-section-title">Status</div>' +
|
||||||
'<div class="svc-detail-status">' +
|
'<div class="svc-detail-status">' +
|
||||||
@@ -707,19 +718,27 @@ async function openServiceDetailModal(unit, name, icon) {
|
|||||||
|
|
||||||
// Section C: Domain diagnostics (domain services)
|
// Section C: Domain diagnostics (domain services)
|
||||||
if (data.needs_domain) {
|
if (data.needs_domain) {
|
||||||
var steps = data.domain_check_steps || [];
|
// The Hub shows only the domain-active step here; the full DNS and
|
||||||
|
// port diagnostics live in the Systems Operational modal.
|
||||||
|
var steps = (data.domain_check_steps || []).filter(function (s) {
|
||||||
|
return Number(s.step) === 1;
|
||||||
|
});
|
||||||
var stepsHtml = "";
|
var stepsHtml = "";
|
||||||
steps.forEach(function(step) {
|
steps.forEach(function(step) {
|
||||||
var iconLabel = "—";
|
|
||||||
if (step.status === "ok") iconLabel = "✅";
|
|
||||||
else if (step.status === "error") iconLabel = "❌";
|
|
||||||
else if (step.status === "warning") iconLabel = "⚠️";
|
|
||||||
else if (step.status === "skipped") iconLabel = "⏭️";
|
|
||||||
var detail = escHtml(step.detail || "").replace(/\n/g, "<br>");
|
var detail = escHtml(step.detail || "").replace(/\n/g, "<br>");
|
||||||
stepsHtml += '<div class="svc-detail-troubleshoot" style="margin-bottom:10px">' +
|
if (step.status === "ok") {
|
||||||
'<strong>' + iconLabel + ' Step ' + escHtml(String(step.step)) + ': ' + escHtml(step.label || "") + '</strong>' +
|
// Not a checklist anymore — just note that the domain works.
|
||||||
(detail ? '<div style="margin-top:6px">' + detail + '</div>' : '') +
|
stepsHtml += '<div class="svc-detail-status"><span class="status-dot active"></span>Domain is active</div>' +
|
||||||
'</div>';
|
(detail ? '<div class="svc-detail-desc" style="margin-top:6px">' + detail + '</div>' : '');
|
||||||
|
} else {
|
||||||
|
var iconLabel = "❌";
|
||||||
|
if (step.status === "warning") iconLabel = "⚠️";
|
||||||
|
else if (step.status === "skipped") iconLabel = "⏭️";
|
||||||
|
stepsHtml += '<div class="svc-detail-troubleshoot" style="margin-bottom:10px">' +
|
||||||
|
'<strong>' + iconLabel + ' ' + escHtml(step.label || "Domain not configured") + '</strong>' +
|
||||||
|
(detail ? '<div style="margin-top:6px">' + detail + '</div>' : '') +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var domainActionHtml = "";
|
var domainActionHtml = "";
|
||||||
@@ -731,11 +750,22 @@ async function openServiceDetailModal(unit, name, icon) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addSetup('<div class="svc-detail-section">' +
|
addSetup('<div class="svc-detail-section">' +
|
||||||
'<div class="svc-detail-section-title">Domain Diagnostic Checklist</div>' +
|
'<div class="svc-detail-section-title">Domain Status</div>' +
|
||||||
stepsHtml +
|
stepsHtml +
|
||||||
domainActionHtml +
|
domainActionHtml +
|
||||||
'</div>');
|
'</div>');
|
||||||
|
|
||||||
|
// Node-only role: BTCPay Server and Lightning Wallet Connections are
|
||||||
|
// the domain services — surface the router task here. (Desktop + Server
|
||||||
|
// set this up during onboarding; Systems Operational shows it too.)
|
||||||
|
if (typeof _currentRole !== "undefined" && _currentRole === "node" &&
|
||||||
|
(unit === "btcpayserver.service" || unit === "albyhub.service")) {
|
||||||
|
addSetup('<div class="svc-detail-section">' +
|
||||||
|
'<div class="svc-detail-section-title">Ports to Forward in Your Router</div>' +
|
||||||
|
'<div class="onboarding-port-warn" id="svc-node-router-box"></div>' +
|
||||||
|
'</div>');
|
||||||
|
}
|
||||||
|
|
||||||
if (data.router_ports && data.router_ports.length > 0) {
|
if (data.router_ports && data.router_ports.length > 0) {
|
||||||
var trimmedInternalIp = data.internal_ip ? String(data.internal_ip).trim() : "";
|
var trimmedInternalIp = data.internal_ip ? String(data.internal_ip).trim() : "";
|
||||||
addSetup('<div class="svc-detail-section">' +
|
addSetup('<div class="svc-detail-section">' +
|
||||||
@@ -993,6 +1023,9 @@ async function openServiceDetailModal(unit, name, icon) {
|
|||||||
$credsBody.innerHTML = html;
|
$credsBody.innerHTML = html;
|
||||||
if (isNwc) _nwcWireTabs();
|
if (isNwc) _nwcWireTabs();
|
||||||
_attachCopyHandlers($credsBody);
|
_attachCopyHandlers($credsBody);
|
||||||
|
if (document.getElementById("svc-node-router-box")) {
|
||||||
|
renderRouterPortsBox("svc-node-router-box");
|
||||||
|
}
|
||||||
if (_isNwcServiceUnit(unit) && (effectiveEnabled || data.enabled)) {
|
if (_isNwcServiceUnit(unit) && (effectiveEnabled || data.enabled)) {
|
||||||
await _nwcInitWalletFlow(unit, name, icon);
|
await _nwcInitWalletFlow(unit, name, icon);
|
||||||
var nwcRtlBtn = document.getElementById("nwc-open-rtl-btn");
|
var nwcRtlBtn = document.getElementById("nwc-open-rtl-btn");
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ const $btnSave = document.getElementById("btn-save-report");
|
|||||||
const $btnRetryUpdate = document.getElementById("btn-retry-update-status");
|
const $btnRetryUpdate = document.getElementById("btn-retry-update-status");
|
||||||
const $btnRetryRun = document.getElementById("btn-retry-update");
|
const $btnRetryRun = document.getElementById("btn-retry-update");
|
||||||
const $btnCloseModal = document.getElementById("btn-close-modal");
|
const $btnCloseModal = document.getElementById("btn-close-modal");
|
||||||
|
const $btnCheckAgain = document.getElementById("btn-check-again");
|
||||||
|
const $updateCloseBtn = document.getElementById("update-close-btn");
|
||||||
|
const $updPill = document.getElementById("upd-pill");
|
||||||
|
const $updLastChecked = document.getElementById("upd-last-checked");
|
||||||
|
|
||||||
const $rebootOverlay = document.getElementById("reboot-overlay");
|
const $rebootOverlay = document.getElementById("reboot-overlay");
|
||||||
const $rebootMainCard = document.getElementById("reboot-main-card");
|
const $rebootMainCard = document.getElementById("reboot-main-card");
|
||||||
@@ -80,6 +84,8 @@ const $rebuildLog = document.getElementById("rebuild-log");
|
|||||||
const $rebuildReboot = document.getElementById("rebuild-reboot-btn");
|
const $rebuildReboot = document.getElementById("rebuild-reboot-btn");
|
||||||
const $rebuildSave = document.getElementById("rebuild-save-report");
|
const $rebuildSave = document.getElementById("rebuild-save-report");
|
||||||
const $rebuildClose = document.getElementById("rebuild-close-btn");
|
const $rebuildClose = document.getElementById("rebuild-close-btn");
|
||||||
|
const $rebuildCloseHdr = document.getElementById("rebuild-close-hdr");
|
||||||
|
const $rebuildPill = document.getElementById("rebuild-pill");
|
||||||
|
|
||||||
// Feature Manager — domain setup modal
|
// Feature Manager — domain setup modal
|
||||||
const $domainSetupModal = document.getElementById("domain-setup-modal");
|
const $domainSetupModal = document.getElementById("domain-setup-modal");
|
||||||
|
|||||||
@@ -5,6 +5,14 @@
|
|||||||
async function openSupportModal() {
|
async function openSupportModal() {
|
||||||
if (!$supportModal) return;
|
if (!$supportModal) return;
|
||||||
$supportModal.classList.add("open");
|
$supportModal.classList.add("open");
|
||||||
|
// The dialog is shared with Security — always restore its identity
|
||||||
|
var title = document.getElementById("support-modal-title");
|
||||||
|
if (title) title.textContent = "Tech Support";
|
||||||
|
var chip = document.getElementById("support-modal-chip");
|
||||||
|
if (chip) {
|
||||||
|
var use = chip.querySelector("use");
|
||||||
|
if (use) use.setAttribute("href", "#g-lifebuoy");
|
||||||
|
}
|
||||||
$supportBody.innerHTML = '<p class="creds-loading">Checking support status…</p>';
|
$supportBody.innerHTML = '<p class="creds-loading">Checking support status…</p>';
|
||||||
try {
|
try {
|
||||||
var status = await apiFetch("/api/support/status");
|
var status = await apiFetch("/api/support/status");
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ function buildTiles(services, categoryLabels) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var cat = svc.category || "other";
|
var cat = svc.category || "other";
|
||||||
|
if (CATEGORY_ALIASES[cat]) cat = CATEGORY_ALIASES[cat];
|
||||||
if (!grouped[cat]) grouped[cat] = [];
|
if (!grouped[cat]) grouped[cat] = [];
|
||||||
grouped[cat].push(svc);
|
grouped[cat].push(svc);
|
||||||
}
|
}
|
||||||
@@ -52,6 +53,7 @@ function buildTiles(services, categoryLabels) {
|
|||||||
if ($tilesArea.children.length === 0) {
|
if ($tilesArea.children.length === 0) {
|
||||||
$tilesArea.innerHTML = '<div class="empty-state"><p>No services configured.</p></div>';
|
$tilesArea.innerHTML = '<div class="empty-state"><p>No services configured.</p></div>';
|
||||||
}
|
}
|
||||||
|
if (typeof window.dashboardServicesUpdated === "function") window.dashboardServicesUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSidebarSupport(supportServices) {
|
function renderSidebarSupport(supportServices) {
|
||||||
@@ -62,20 +64,22 @@ function renderSidebarSupport(supportServices) {
|
|||||||
sidebarUpdateBtn.className = "sidebar-support-btn";
|
sidebarUpdateBtn.className = "sidebar-support-btn";
|
||||||
sidebarUpdateBtn.id = "sidebar-btn-update";
|
sidebarUpdateBtn.id = "sidebar-btn-update";
|
||||||
sidebarUpdateBtn.innerHTML =
|
sidebarUpdateBtn.innerHTML =
|
||||||
'<img class="sidebar-support-icon" src="/static/icons/update.svg" alt="Update" style="width:1.5rem;height:1.5rem;">' +
|
'<span class="sidebar-support-icon"><svg><use href="#g-update"/></svg></span>' +
|
||||||
'<span class="sidebar-support-text">' +
|
'<span class="sidebar-support-text">' +
|
||||||
'<span class="sidebar-support-title">Update System</span>' +
|
'<span class="sidebar-support-title">Update System</span>' +
|
||||||
'<span class="sidebar-support-hint" id="sidebar-update-hint">Check for updates</span>' +
|
'<span class="sidebar-support-hint" id="sidebar-update-hint">Check for updates</span>' +
|
||||||
'</span>';
|
'</span>';
|
||||||
sidebarUpdateBtn.addEventListener("click", function() { openUpdateModal(); });
|
sidebarUpdateBtn.addEventListener("click", function() { openUpdateModal(); });
|
||||||
$sidebarSupport.appendChild(sidebarUpdateBtn);
|
$sidebarSupport.appendChild(sidebarUpdateBtn);
|
||||||
|
// checkUpdates may already have run before the sidebar was built
|
||||||
|
applyUpdateSidebarState(window._lastUpdateCheck);
|
||||||
|
|
||||||
for (var i = 0; i < supportServices.length; i++) {
|
for (var i = 0; i < supportServices.length; i++) {
|
||||||
var svc = supportServices[i];
|
var svc = supportServices[i];
|
||||||
var btn = document.createElement("button");
|
var btn = document.createElement("button");
|
||||||
btn.className = "sidebar-support-btn";
|
btn.className = "sidebar-support-btn";
|
||||||
btn.innerHTML =
|
btn.innerHTML =
|
||||||
'<span class="sidebar-support-icon">🛟</span>' +
|
'<span class="sidebar-support-icon"><svg><use href="#g-lifebuoy"/></svg></span>' +
|
||||||
'<span class="sidebar-support-text">' +
|
'<span class="sidebar-support-text">' +
|
||||||
'<span class="sidebar-support-title">' + escHtml(svc.name || "Tech Support") + '</span>' +
|
'<span class="sidebar-support-title">' + escHtml(svc.name || "Tech Support") + '</span>' +
|
||||||
'<span class="sidebar-support-hint">Click for help</span>' +
|
'<span class="sidebar-support-hint">Click for help</span>' +
|
||||||
@@ -88,7 +92,7 @@ function renderSidebarSupport(supportServices) {
|
|||||||
var backupBtn = document.createElement("button");
|
var backupBtn = document.createElement("button");
|
||||||
backupBtn.className = "sidebar-support-btn";
|
backupBtn.className = "sidebar-support-btn";
|
||||||
backupBtn.innerHTML =
|
backupBtn.innerHTML =
|
||||||
'<span class="sidebar-support-icon">💾</span>' +
|
'<span class="sidebar-support-icon"><svg><use href="#g-box"/></svg></span>' +
|
||||||
'<span class="sidebar-support-text">' +
|
'<span class="sidebar-support-text">' +
|
||||||
'<span class="sidebar-support-title">Manual Backup</span>' +
|
'<span class="sidebar-support-title">Manual Backup</span>' +
|
||||||
'<span class="sidebar-support-hint">Back up to external drive</span>' +
|
'<span class="sidebar-support-hint">Back up to external drive</span>' +
|
||||||
@@ -100,7 +104,7 @@ function renderSidebarSupport(supportServices) {
|
|||||||
var securityBtn = document.createElement("button");
|
var securityBtn = document.createElement("button");
|
||||||
securityBtn.className = "sidebar-support-btn";
|
securityBtn.className = "sidebar-support-btn";
|
||||||
securityBtn.innerHTML =
|
securityBtn.innerHTML =
|
||||||
'<span class="sidebar-support-icon">\uD83D\uDEE1</span>' +
|
'<span class="sidebar-support-icon"><svg><use href="#g-shield-check"/></svg></span>' +
|
||||||
'<span class="sidebar-support-text">' +
|
'<span class="sidebar-support-text">' +
|
||||||
'<span class="sidebar-support-title">Security</span>' +
|
'<span class="sidebar-support-title">Security</span>' +
|
||||||
'<span class="sidebar-support-hint">Reset & verify system</span>' +
|
'<span class="sidebar-support-hint">Reset & verify system</span>' +
|
||||||
@@ -113,7 +117,7 @@ function renderSidebarSupport(supportServices) {
|
|||||||
var upgradeBtn = document.createElement("button");
|
var upgradeBtn = document.createElement("button");
|
||||||
upgradeBtn.className = "sidebar-support-btn";
|
upgradeBtn.className = "sidebar-support-btn";
|
||||||
upgradeBtn.innerHTML =
|
upgradeBtn.innerHTML =
|
||||||
'<span class="sidebar-support-icon">🚀</span>' +
|
'<span class="sidebar-support-icon"><svg><use href="#g-antenna"/></svg></span>' +
|
||||||
'<span class="sidebar-support-text">' +
|
'<span class="sidebar-support-text">' +
|
||||||
'<span class="sidebar-support-title">Upgrade to Full Server</span>' +
|
'<span class="sidebar-support-title">Upgrade to Full Server</span>' +
|
||||||
'<span class="sidebar-support-hint">Unlock all services</span>' +
|
'<span class="sidebar-support-hint">Unlock all services</span>' +
|
||||||
@@ -237,6 +241,7 @@ function updateTiles(services) {
|
|||||||
if (text) text.textContent = st;
|
if (text) text.textContent = st;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (typeof window.dashboardServicesUpdated === "function") window.dashboardServicesUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Service polling ───────────────────────────────────────────────
|
// ── Service polling ───────────────────────────────────────────────
|
||||||
@@ -248,6 +253,10 @@ async function refreshServices() {
|
|||||||
var services = await apiFetch("/api/services");
|
var services = await apiFetch("/api/services");
|
||||||
if (_firstLoad) { buildTiles(services, _categoryLabels); _firstLoad = false; }
|
if (_firstLoad) { buildTiles(services, _categoryLabels); _firstLoad = false; }
|
||||||
else { updateTiles(services); }
|
else { updateTiles(services); }
|
||||||
|
// Service data has rendered — the dashboard is ready; lift the boot
|
||||||
|
// splash (no-op once lifted). Note: dashboardServicesUpdated may also
|
||||||
|
// fire from checkUpdates before this resolves, so the lift lives here.
|
||||||
|
if (typeof window.__liftAppSplash === "function") window.__liftAppSplash();
|
||||||
} catch (err) { console.warn("Failed to fetch services:", err); }
|
} catch (err) { console.warn("Failed to fetch services:", err); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,37 +276,48 @@ async function loadNetwork() {
|
|||||||
|
|
||||||
// ── Update check ──────────────────────────────────────────────────
|
// ── Update check ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Paint the sidebar Update button from the last known update state.
|
||||||
|
// Called after each check AND when the button is (re)built, so the hint
|
||||||
|
// is correct regardless of which finishes first at startup.
|
||||||
|
function applyUpdateSidebarState(data) {
|
||||||
|
if (!data) return;
|
||||||
|
var sidebarUpdateBtn = document.getElementById("sidebar-btn-update");
|
||||||
|
var sidebarUpdateHint = document.getElementById("sidebar-update-hint");
|
||||||
|
if (!sidebarUpdateBtn) return;
|
||||||
|
var hasUpdates = !!data.available;
|
||||||
|
var updateStatus = data.status || "idle";
|
||||||
|
if (updateStatus === "failed") {
|
||||||
|
// Last update errored and did not apply — surface it as a persistent
|
||||||
|
// red banner that re-opens the failed run with a "Retry Update" action.
|
||||||
|
sidebarUpdateBtn.style.borderColor = "#f66151";
|
||||||
|
sidebarUpdateBtn.style.backgroundColor = "rgba(246, 97, 81, 0.10)";
|
||||||
|
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Update failed — click to retry";
|
||||||
|
} else if (updateStatus === "reboot_required") {
|
||||||
|
sidebarUpdateBtn.style.borderColor = "#e9b64a";
|
||||||
|
sidebarUpdateBtn.style.backgroundColor = "rgba(233, 182, 74, 0.10)";
|
||||||
|
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Restart required";
|
||||||
|
} else if (updateStatus === "running") {
|
||||||
|
sidebarUpdateBtn.style.borderColor = "#78aeed";
|
||||||
|
sidebarUpdateBtn.style.backgroundColor = "rgba(120, 174, 237, 0.10)";
|
||||||
|
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Update in progress…";
|
||||||
|
} else if (hasUpdates) {
|
||||||
|
sidebarUpdateBtn.style.borderColor = "#3ecf8e";
|
||||||
|
sidebarUpdateBtn.style.backgroundColor = "rgba(62, 207, 142, 0.10)";
|
||||||
|
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Updates available!";
|
||||||
|
} else {
|
||||||
|
sidebarUpdateBtn.style.borderColor = "";
|
||||||
|
sidebarUpdateBtn.style.backgroundColor = "";
|
||||||
|
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "System is up to date";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function checkUpdates() {
|
async function checkUpdates() {
|
||||||
try {
|
try {
|
||||||
var data = await apiFetch("/api/updates/check");
|
var data = await apiFetch("/api/updates/check");
|
||||||
var hasUpdates = !!data.available;
|
window._lastUpdateCheck = data;
|
||||||
var updateStatus = data.status || "idle";
|
if (typeof markUpdateChecked === "function") markUpdateChecked();
|
||||||
var sidebarUpdateBtn = document.getElementById("sidebar-btn-update");
|
applyUpdateSidebarState(data);
|
||||||
var sidebarUpdateHint = document.getElementById("sidebar-update-hint");
|
// The welcome dashboard's updates card reads this state
|
||||||
if (sidebarUpdateBtn) {
|
if (typeof window.dashboardServicesUpdated === "function") window.dashboardServicesUpdated();
|
||||||
if (updateStatus === "failed") {
|
|
||||||
// Last update errored and did not apply — surface it as a persistent
|
|
||||||
// red banner that re-opens the failed run with a "Retry Update" action.
|
|
||||||
sidebarUpdateBtn.style.borderColor = "#e01b24";
|
|
||||||
sidebarUpdateBtn.style.backgroundColor = "rgba(224, 27, 36, 0.10)";
|
|
||||||
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Update failed — click to retry";
|
|
||||||
} else if (updateStatus === "reboot_required") {
|
|
||||||
sidebarUpdateBtn.style.borderColor = "#e5a50a";
|
|
||||||
sidebarUpdateBtn.style.backgroundColor = "rgba(229, 165, 10, 0.10)";
|
|
||||||
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Restart required";
|
|
||||||
} else if (updateStatus === "running") {
|
|
||||||
sidebarUpdateBtn.style.borderColor = "#3584e4";
|
|
||||||
sidebarUpdateBtn.style.backgroundColor = "rgba(53, 132, 228, 0.10)";
|
|
||||||
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Update in progress…";
|
|
||||||
} else if (hasUpdates) {
|
|
||||||
sidebarUpdateBtn.style.borderColor = "#2ec27e";
|
|
||||||
sidebarUpdateBtn.style.backgroundColor = "rgba(46, 194, 126, 0.08)";
|
|
||||||
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "Updates available!";
|
|
||||||
} else {
|
|
||||||
sidebarUpdateBtn.style.borderColor = "";
|
|
||||||
sidebarUpdateBtn.style.backgroundColor = "";
|
|
||||||
if (sidebarUpdateHint) sidebarUpdateHint.textContent = "System is up to date";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,88 @@
|
|||||||
|
|
||||||
// ── Update modal ──────────────────────────────────────────────────
|
// ── Update modal ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// ── Update dialog presentation (pill, status line, last-checked) ──
|
||||||
|
|
||||||
|
var _updateLastCheckTs = 0;
|
||||||
|
|
||||||
|
function markUpdateChecked() {
|
||||||
|
_updateLastCheckTs = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _updateLastCheckedText() {
|
||||||
|
if (!$updLastChecked) return;
|
||||||
|
if (!_updateLastCheckTs) { $updLastChecked.textContent = "—"; return; }
|
||||||
|
var s = Math.floor((Date.now() - _updateLastCheckTs) / 1000);
|
||||||
|
if (s < 30) { $updLastChecked.textContent = "just now"; return; }
|
||||||
|
var m = Math.floor(s / 60);
|
||||||
|
if (m < 60) { $updLastChecked.textContent = m + (m === 1 ? " minute ago" : " minutes ago"); return; }
|
||||||
|
var h = Math.floor(m / 60);
|
||||||
|
$updLastChecked.textContent = h + (h === 1 ? " hour ago" : " hours ago");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUpdatePill(state) {
|
||||||
|
if (!$updPill) return;
|
||||||
|
var cls = "upd-pill";
|
||||||
|
var html;
|
||||||
|
if (state === "checking") {
|
||||||
|
cls += " st-loading"; html = '<span class="status-dot loading pulse"></span>Checking…';
|
||||||
|
} else if (state === "uptodate") {
|
||||||
|
cls += " st-active"; html = '<span class="status-dot active"></span>Up to date';
|
||||||
|
} else if (state === "complete") {
|
||||||
|
cls += " st-active"; html = '<span class="status-dot active"></span>Update complete';
|
||||||
|
} else if (state === "reboot") {
|
||||||
|
cls += " st-needs-attention"; html = '<span class="status-dot needs-attention pulse"></span>Restart required';
|
||||||
|
} else if (state === "failed") {
|
||||||
|
cls += " st-failed"; html = '<span class="status-dot failed"></span>Update failed';
|
||||||
|
} else if (state === "unavailable") {
|
||||||
|
cls += " st-needs-attention"; html = '<span class="status-dot needs-attention pulse"></span>Status unknown';
|
||||||
|
} else {
|
||||||
|
cls += " st-loading"; html = '<span class="status-dot loading pulse"></span>Updating…';
|
||||||
|
}
|
||||||
|
$updPill.className = cls;
|
||||||
|
$updPill.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single place that reflects update state in the dialog: status message
|
||||||
|
// text + color, header pill, and the Close / Check again availability.
|
||||||
|
function _setUpdateStatus(text) {
|
||||||
|
if ($modalStatus) $modalStatus.textContent = text;
|
||||||
|
var state;
|
||||||
|
if (text.charAt(0) === "✗") state = "failed";
|
||||||
|
else if (text.indexOf("restart required") !== -1) state = "reboot";
|
||||||
|
else if (text.charAt(0) === "✓") state = (text.indexOf("already up to date") !== -1) ? "uptodate" : "complete";
|
||||||
|
else if (text.indexOf("unavailable") !== -1) state = "unavailable";
|
||||||
|
else if (text.indexOf("Checking") === 0) state = "checking";
|
||||||
|
else state = "updating";
|
||||||
|
if ($modalStatus) {
|
||||||
|
var msg = "update-status-msg";
|
||||||
|
if (state === "failed") $modalStatus.className = msg + " st-err";
|
||||||
|
else if (state === "reboot" || state === "unavailable") $modalStatus.className = msg + " st-warn";
|
||||||
|
else if (state === "uptodate" || state === "complete") $modalStatus.className = msg + " st-ok";
|
||||||
|
else $modalStatus.className = msg;
|
||||||
|
// In the up-to-date state the green console line already says it —
|
||||||
|
// the mockup shows it exactly once.
|
||||||
|
$modalStatus.style.display = (state === "uptodate") ? "none" : "";
|
||||||
|
}
|
||||||
|
setUpdatePill(state);
|
||||||
|
var interactive = (state !== "updating");
|
||||||
|
if ($updateCloseBtn) $updateCloseBtn.disabled = !interactive;
|
||||||
|
if ($btnCheckAgain) {
|
||||||
|
$btnCheckAgain.disabled = (state === "checking" || state === "updating");
|
||||||
|
$btnCheckAgain.style.display = interactive ? "inline-flex" : "none";
|
||||||
|
}
|
||||||
|
_updateLastCheckedText();
|
||||||
|
}
|
||||||
|
|
||||||
async function openUpdateModal() {
|
async function openUpdateModal() {
|
||||||
if (!$modal) return;
|
if (!$modal) return;
|
||||||
|
|
||||||
|
// Open immediately in a checking state; the status/check requests below
|
||||||
|
// fill in the real result (mockup: auto-check on open).
|
||||||
|
$modal.classList.add("open");
|
||||||
|
if ($modalLog) $modalLog.innerHTML = '<span class="dim">Checking for updates…</span>';
|
||||||
|
_setUpdateStatus("Checking for updates…");
|
||||||
|
|
||||||
// Reattach before checking for new updates. This makes a browser reload,
|
// Reattach before checking for new updates. This makes a browser reload,
|
||||||
// RDP reconnect, or suspended tab recover the authoritative systemd-backed
|
// RDP reconnect, or suspended tab recover the authoritative systemd-backed
|
||||||
// state instead of starting over or claiming the system is merely up to date.
|
// state instead of starting over or claiming the system is merely up to date.
|
||||||
@@ -31,6 +110,7 @@ async function openUpdateModal() {
|
|||||||
STATUS_POLL_FETCH_TIMEOUT
|
STATUS_POLL_FETCH_TIMEOUT
|
||||||
)
|
)
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
|
markUpdateChecked();
|
||||||
if (!data.available) {
|
if (!data.available) {
|
||||||
stopUpdatePoll();
|
stopUpdatePoll();
|
||||||
_updateLog = "";
|
_updateLog = "";
|
||||||
@@ -38,8 +118,8 @@ async function openUpdateModal() {
|
|||||||
_updateVisibleLogChars = 0;
|
_updateVisibleLogChars = 0;
|
||||||
_updateFinished = true;
|
_updateFinished = true;
|
||||||
_updateStatusUnavailable = false;
|
_updateStatusUnavailable = false;
|
||||||
if ($modalLog) $modalLog.textContent = "";
|
if ($modalLog) $modalLog.innerHTML = '<span class="ok">✓ System is already up to date</span>';
|
||||||
if ($modalStatus) $modalStatus.textContent = "✓ System is already up to date";
|
_setUpdateStatus("✓ System is already up to date");
|
||||||
if ($modalSpinner) $modalSpinner.classList.remove("spinning");
|
if ($modalSpinner) $modalSpinner.classList.remove("spinning");
|
||||||
if ($btnReboot) $btnReboot.style.display = "none";
|
if ($btnReboot) $btnReboot.style.display = "none";
|
||||||
if ($btnSave) $btnSave.style.display = "none";
|
if ($btnSave) $btnSave.style.display = "none";
|
||||||
@@ -68,7 +148,7 @@ function prepareUpdateModal() {
|
|||||||
_updateStatusUnavailable = false;
|
_updateStatusUnavailable = false;
|
||||||
_updatePollFailures = 0;
|
_updatePollFailures = 0;
|
||||||
if ($modalLog) $modalLog.textContent = "";
|
if ($modalLog) $modalLog.textContent = "";
|
||||||
if ($modalStatus) $modalStatus.textContent = "Starting update…";
|
_setUpdateStatus("Starting update…");
|
||||||
if ($modalSpinner) $modalSpinner.classList.add("spinning");
|
if ($modalSpinner) $modalSpinner.classList.add("spinning");
|
||||||
if ($btnReboot) $btnReboot.style.display = "none";
|
if ($btnReboot) $btnReboot.style.display = "none";
|
||||||
if ($btnSave) $btnSave.style.display = "none";
|
if ($btnSave) $btnSave.style.display = "none";
|
||||||
@@ -89,7 +169,7 @@ function showExistingUpdate(data) {
|
|||||||
_updateLogOffset = Number(data.offset) || 0;
|
_updateLogOffset = Number(data.offset) || 0;
|
||||||
|
|
||||||
if (data.running) {
|
if (data.running) {
|
||||||
if ($modalStatus) $modalStatus.textContent = "Updating…";
|
_setUpdateStatus("Updating…");
|
||||||
startUpdatePoll();
|
startUpdatePoll();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -154,7 +234,8 @@ function startUpdate() {
|
|||||||
)
|
)
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (data.status === "no_updates") {
|
if (data.status === "no_updates") {
|
||||||
if ($modalStatus) $modalStatus.textContent = "✓ System is already up to date";
|
if ($modalLog) $modalLog.innerHTML = '<span class="ok">✓ System is already up to date</span>';
|
||||||
|
_setUpdateStatus("✓ System is already up to date");
|
||||||
if ($modalSpinner) $modalSpinner.classList.remove("spinning");
|
if ($modalSpinner) $modalSpinner.classList.remove("spinning");
|
||||||
if ($btnReboot) $btnReboot.style.display = "none";
|
if ($btnReboot) $btnReboot.style.display = "none";
|
||||||
if ($btnSave) $btnSave.style.display = "none";
|
if ($btnSave) $btnSave.style.display = "none";
|
||||||
@@ -165,7 +246,7 @@ function startUpdate() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data.status === "already_running") appendLog("[Update already in progress, attaching…]\n\n");
|
if (data.status === "already_running") appendLog("[Update already in progress, attaching…]\n\n");
|
||||||
if ($modalStatus) $modalStatus.textContent = "Updating…";
|
_setUpdateStatus("Updating…");
|
||||||
startUpdatePoll();
|
startUpdatePoll();
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
@@ -235,7 +316,7 @@ async function pollUpdateStatus() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
appendLog("[Update status reconnected]\n");
|
appendLog("[Update status reconnected]\n");
|
||||||
if ($modalStatus) $modalStatus.textContent = "Updating…";
|
_setUpdateStatus("Updating…");
|
||||||
}
|
}
|
||||||
if (data.log) appendLog(data.log);
|
if (data.log) appendLog(data.log);
|
||||||
_updateLogOffset = data.offset;
|
_updateLogOffset = data.offset;
|
||||||
@@ -258,7 +339,7 @@ async function pollUpdateStatus() {
|
|||||||
if (!_serverWasDown) {
|
if (!_serverWasDown) {
|
||||||
_serverWasDown = true;
|
_serverWasDown = true;
|
||||||
appendLog("\n[Update status connection interrupted — retrying…]\n");
|
appendLog("\n[Update status connection interrupted — retrying…]\n");
|
||||||
if ($modalStatus) $modalStatus.textContent = "Reconnecting to update…";
|
_setUpdateStatus("Reconnecting to update…");
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
_updatePollInFlight = false;
|
_updatePollInFlight = false;
|
||||||
@@ -270,7 +351,7 @@ function showUpdateStatusUnavailable() {
|
|||||||
_updateStatusUnavailable = true;
|
_updateStatusUnavailable = true;
|
||||||
stopUpdatePoll();
|
stopUpdatePoll();
|
||||||
if ($modalSpinner) $modalSpinner.classList.remove("spinning");
|
if ($modalSpinner) $modalSpinner.classList.remove("spinning");
|
||||||
if ($modalStatus) $modalStatus.textContent = "Update status unavailable — update may still be running";
|
_setUpdateStatus("Update status unavailable — update may still be running");
|
||||||
appendLog("\n[The Hub could not confirm update status. The background update was not stopped. Select Retry Status after reconnecting.]\n");
|
appendLog("\n[The Hub could not confirm update status. The background update was not stopped. Select Retry Status after reconnecting.]\n");
|
||||||
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "inline-flex";
|
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "inline-flex";
|
||||||
if ($btnCloseModal) $btnCloseModal.disabled = false;
|
if ($btnCloseModal) $btnCloseModal.disabled = false;
|
||||||
@@ -283,7 +364,7 @@ function retryUpdateStatus() {
|
|||||||
_updatePollFailures = 0;
|
_updatePollFailures = 0;
|
||||||
_serverWasDown = true;
|
_serverWasDown = true;
|
||||||
if ($modalSpinner) $modalSpinner.classList.add("spinning");
|
if ($modalSpinner) $modalSpinner.classList.add("spinning");
|
||||||
if ($modalStatus) $modalStatus.textContent = "Reconnecting to update…";
|
_setUpdateStatus("Reconnecting to update…");
|
||||||
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none";
|
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none";
|
||||||
if ($btnCloseModal) $btnCloseModal.disabled = true;
|
if ($btnCloseModal) $btnCloseModal.disabled = true;
|
||||||
startUpdatePoll();
|
startUpdatePoll();
|
||||||
@@ -314,13 +395,13 @@ function onUpdateDone(result) {
|
|||||||
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none";
|
if ($btnRetryUpdate) $btnRetryUpdate.style.display = "none";
|
||||||
if ($btnCloseModal) $btnCloseModal.disabled = false;
|
if ($btnCloseModal) $btnCloseModal.disabled = false;
|
||||||
if (result === true) {
|
if (result === true) {
|
||||||
if ($modalStatus) $modalStatus.textContent = "✓ Update complete";
|
_setUpdateStatus("✓ Update complete");
|
||||||
if ($btnReboot) $btnReboot.style.display = "inline-flex";
|
if ($btnReboot) $btnReboot.style.display = "inline-flex";
|
||||||
} else if (result === "reboot_required") {
|
} else if (result === "reboot_required") {
|
||||||
if ($modalStatus) $modalStatus.textContent = "✓ Update complete — restart required";
|
_setUpdateStatus("✓ Update complete — restart required");
|
||||||
if ($btnReboot) $btnReboot.style.display = "inline-flex";
|
if ($btnReboot) $btnReboot.style.display = "inline-flex";
|
||||||
} else {
|
} else {
|
||||||
if ($modalStatus) $modalStatus.textContent = "✗ Update failed — your system was not changed. Run the update again or save the error report for support.";
|
_setUpdateStatus("✗ Update failed — your system was not changed. Run the update again or save the error report for support.");
|
||||||
if ($btnRetryRun) $btnRetryRun.style.display = "inline-flex";
|
if ($btnRetryRun) $btnRetryRun.style.display = "inline-flex";
|
||||||
if ($btnSave) $btnSave.style.display = "inline-flex";
|
if ($btnSave) $btnSave.style.display = "inline-flex";
|
||||||
if ($btnReboot) $btnReboot.style.display = "none";
|
if ($btnReboot) $btnReboot.style.display = "none";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Sovran_SystemsOS Hub</title>
|
<title>Sovran_SystemsOS — The Hub</title>
|
||||||
<link rel="stylesheet" href="/static/css/base.css?v={{ asset_version }}" />
|
<link rel="stylesheet" href="/static/css/base.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/buttons.css?v={{ asset_version }}" />
|
<link rel="stylesheet" href="/static/css/buttons.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/header.css?v={{ asset_version }}" />
|
<link rel="stylesheet" href="/static/css/header.css?v={{ asset_version }}" />
|
||||||
@@ -18,66 +18,324 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- Header bar -->
|
<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
|
||||||
<header class="header-bar">
|
|
||||||
<img src="/static/sovran-hub-icon.svg" alt="Sovran Hub" class="header-logo" />
|
|
||||||
|
|
||||||
<div class="title-group">
|
<!-- service glyphs -->
|
||||||
<span class="title">Sovran_SystemsOS Hub</span>
|
<!-- ═══ Real service logos — extracted verbatim from app/icons/*.svg ═══
|
||||||
|
(internal ids namespaced per icon to avoid collisions) -->
|
||||||
|
|
||||||
<!-- OS Version Badge — styled identically to the version badge in service modals -->
|
<!-- symbolic helpers (nav + modal internals) -->
|
||||||
<span class="os-version-badge" id="os-version-badge" title="Sovran_SystemsOS Version">v{{ sovran_version }}</span>
|
<!-- Bitcoin nav icon: the OFFICIAL logo silhouette (paths taken verbatim
|
||||||
|
from app/icons/bitcoin-core.svg) rendered monochrome in currentColor
|
||||||
|
so it matches the symbolic sidebar set -->
|
||||||
|
<symbol id="g-btc-sym" viewBox="-34 -34 580 580">
|
||||||
|
<path fill="currentColor" d="M317.871 7.656c-137.12-34.192-276.024 49.28-310.2 186.44-34.208 137.136 49.256 276.048 186.36 310.24 137.16 34.199 276.063-49.265 310.256-186.408 34.192-137.152-49.264-276.08-186.416-310.272m50.936 211.872c-3.688 24.936-17.512 37.008-35.864 41.24 25.2 13.12 38.024 33.239 25.809 68.12-15.16 43.319-51.176 46.976-99.072 37.912l-11.624 46.584-28.088-7 11.472-45.96a1076 1076 0 0 1-22.384-5.809l-11.512 46.177-28.056-7 11.624-46.673c-6.561-1.68-13.225-3.464-20.024-5.168l-36.552-9.111 13.943-32.152s20.696 5.504 20.416 5.096c7.952 1.969 11.48-3.216 12.872-6.672l18.368-73.64.048-.2 13.104-52.568c.344-5.968-1.712-13.496-13.088-16.336.439-.296-20.4-5.072-20.4-5.072l7.472-30 38.736 9.673-.032.144c5.824 1.448 11.824 2.824 17.937 4.216L245.423 89.2l28.072 7-11.28 45.224c7.536 1.721 15.12 3.456 22.504 5.297l11.2-44.929 28.088 7-11.504 46.145c35.464 12.215 61.401 30.527 56.304 64.591"/>
|
||||||
|
<path fill="currentColor" d="m254.647 174.6-13.983 56.08c15.855 3.951 64.735 20.071 72.656-11.656 8.248-33.096-42.817-40.472-58.673-44.424"/>
|
||||||
|
<path fill="currentColor" d="m233.608 258.984-15.425 61.832c19.04 4.729 77.769 23.584 86.448-11.296 9.072-36.376-51.984-45.784-71.023-50.536"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="g-chat" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M20 5.5v7a2.5 2.5 0 0 1-2.5 2.5H10l-4 3.2c-.6.5-1 .3-1-.5V5.5A2.5 2.5 0 0 1 7.5 3h10A2.5 2.5 0 0 1 20 5.5z"/>
|
||||||
|
<circle cx="8.7" cy="9.2" r="0.5" fill="currentColor"/><circle cx="12" cy="9.2" r="0.5" fill="currentColor"/><circle cx="15.3" cy="9.2" r="0.5" fill="currentColor"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="g-antenna" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round">
|
||||||
|
<path d="M5.4 9.9a9.3 9.3 0 0 1 13.2 0"/>
|
||||||
|
<path d="M8.1 12.9a5.6 5.6 0 0 1 7.8 0"/>
|
||||||
|
<circle cx="12" cy="16.6" r="1.5" fill="currentColor" stroke="none"/>
|
||||||
|
<path d="M12 16.6V21"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="g-lock" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="5.5" y="10.5" width="13" height="9.5" rx="2.6"/>
|
||||||
|
<path d="M8.5 10.5V8a3.5 3.5 0 0 1 7 0v2.5"/>
|
||||||
|
<circle cx="12" cy="15" r="1.3" fill="currentColor" stroke="none"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<!-- nav + ui symbolic icons -->
|
||||||
|
<symbol id="g-home" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M4 11.2L12 4.5l8 6.7"/>
|
||||||
|
<path d="M6 10.5v8a1.2 1.2 0 0 0 1.2 1.2h9.6a1.2 1.2 0 0 0 1.2-1.2v-8"/>
|
||||||
|
<path d="M10 19.7v-5.4h4v5.4"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="g-grid" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round">
|
||||||
|
<rect x="3.5" y="3.5" width="7.2" height="7.2" rx="2"/><rect x="13.3" y="3.5" width="7.2" height="7.2" rx="2"/>
|
||||||
|
<rect x="3.5" y="13.3" width="7.2" height="7.2" rx="2"/><rect x="13.3" y="13.3" width="7.2" height="7.2" rx="2"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="g-server" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round">
|
||||||
|
<rect x="3.5" y="3.5" width="17" height="7.4" rx="2"/><rect x="3.5" y="13.1" width="17" height="7.4" rx="2"/>
|
||||||
|
<circle cx="7.2" cy="7.2" r="0.6" fill="currentColor"/><circle cx="7.2" cy="16.8" r="0.6" fill="currentColor"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="g-dots" viewBox="0 0 24 24">
|
||||||
|
<g fill="currentColor">
|
||||||
|
<circle cx="5" cy="5" r="1.7"/><circle cx="12" cy="5" r="1.7"/><circle cx="19" cy="5" r="1.7"/>
|
||||||
|
<circle cx="5" cy="12" r="1.7"/><circle cx="12" cy="12" r="1.7"/><circle cx="19" cy="12" r="1.7"/>
|
||||||
|
<circle cx="5" cy="19" r="1.7"/><circle cx="12" cy="19" r="1.7"/><circle cx="19" cy="19" r="1.7"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="g-refresh" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||||
|
<path d="M20 12a8 8 0 1 1-2.9-6.2"/>
|
||||||
|
<path d="M20.6 2.6l-.5 4.9-4.6-1.7z" fill="currentColor" stroke="none"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<!-- Update System: down arrow into a tray (get / install updates) -->
|
||||||
|
<symbol id="g-update" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 4v10.5"/>
|
||||||
|
<path d="M7.8 10.3L12 14.5l4.2-4.2"/>
|
||||||
|
<path d="M4.5 16.5v2a2.5 2.5 0 0 0 2.5 2.5h10a2.5 2.5 0 0 0 2.5-2.5v-2"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="g-box" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="3.5" y="4" width="17" height="4.4" rx="1.6"/>
|
||||||
|
<path d="M5.2 8.4v9a2.4 2.4 0 0 0 2.4 2.4h8.8a2.4 2.4 0 0 0 2.4-2.4v-9"/>
|
||||||
|
<path d="M10 12.5h4"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="g-shield-check" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 2.5 4.5 5.5v6c0 4.6 3.1 8 7.5 9.9 4.4-1.9 7.5-5.3 7.5-9.9v-6z"/>
|
||||||
|
<path d="M8.6 12l2.4 2.4 4.4-4.6"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<!-- Systems Operational: activity pulse (system health) — distinct from
|
||||||
|
the Security shield above -->
|
||||||
|
<symbol id="g-pulse" viewBox="0 0 24 24">
|
||||||
|
<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M3 12h4l3-7 4 14 3-7h4"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="g-lifebuoy" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
||||||
|
<circle cx="12" cy="12" r="8.6"/><circle cx="12" cy="12" r="3.6"/>
|
||||||
|
<path d="M6 6l3.2 3.2M18 6l-3.2 3.2M6 18l3.2-3.2M18 18l-3.2-3.2"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="g-power" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||||
|
<path d="M7.2 6.1a7 7 0 1 0 9.6 0"/>
|
||||||
|
<path d="M12 2.8v8.4"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="g-logout" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M14 4h4.5A1.5 1.5 0 0 1 20 5.5v13a1.5 1.5 0 0 1-1.5 1.5H14"/>
|
||||||
|
<path d="M4 12h10M10.5 8.5 14 12l-3.5 3.5"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="g-search" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||||
|
<circle cx="11" cy="11" r="6.5"/>
|
||||||
|
<path d="M15.8 15.8 21 21"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="g-copy" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="9" y="9" width="11" height="11" rx="2.2"/>
|
||||||
|
<path d="M5.5 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v.5"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="g-check" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.5l5 5 10-11"/></symbol>
|
||||||
|
<symbol id="g-chev" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" d="M9.5 5.5 16 12l-6.5 6.5"/></symbol>
|
||||||
|
<symbol id="g-x" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" d="M6 6l12 12M18 6 6 18"/></symbol>
|
||||||
|
<symbol id="g-wifi" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round">
|
||||||
|
<path d="M4.5 10.2a11 11 0 0 1 15 0"/>
|
||||||
|
<path d="M7.3 13.3a7 7 0 0 1 9.4 0"/>
|
||||||
|
<circle cx="12" cy="17" r="1.4" fill="currentColor" stroke="none"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="g-key" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="7.5" cy="15.5" r="4"/>
|
||||||
|
<path d="M10.4 12.6 20 3M16.2 6.8l2.4 2.4M18.8 4.2l2.2 2.2"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="g-alert" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 3.6 22.2 20.4H1.8z"/>
|
||||||
|
<path d="M12 9.8v4.6"/><circle cx="12" cy="17.2" r="0.6" fill="currentColor" stroke="none"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- ═══ BOOT SPLASH ═══ covers the shell while the first data loads;
|
||||||
|
dashboard.js lifts it once the welcome cards are rendered -->
|
||||||
|
<div id="app-splash" role="status" aria-live="polite">
|
||||||
|
<div class="splash-card">
|
||||||
|
<div class="splash-ring">
|
||||||
|
<img src="/static/sovran-hub-icon.svg" alt="" width="64" height="64" />
|
||||||
|
<span class="splash-ring-arc" aria-hidden="true"></span>
|
||||||
|
</div>
|
||||||
|
<div class="splash-title">Starting The Hub</div>
|
||||||
|
<div class="splash-sub" id="splash-sub">Gathering your services…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var splash = document.getElementById("app-splash");
|
||||||
|
var sub = document.getElementById("splash-sub");
|
||||||
|
var lifted = false;
|
||||||
|
function lift() {
|
||||||
|
if (lifted || !splash) return;
|
||||||
|
lifted = true;
|
||||||
|
splash.classList.add("done");
|
||||||
|
setTimeout(function () {
|
||||||
|
if (splash && splash.parentNode) splash.parentNode.removeChild(splash);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
window.__liftAppSplash = lift;
|
||||||
|
// Reassure the user if the backend is slow (e.g. right after a reboot)
|
||||||
|
setTimeout(function () {
|
||||||
|
if (!lifted && sub) sub.textContent = "Still starting\u2026 this can take a moment after a reboot.";
|
||||||
|
}, 8000);
|
||||||
|
// Never trap the user behind the splash
|
||||||
|
setTimeout(lift, 25000);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="app">
|
||||||
|
|
||||||
|
<!-- ═══ SIDEBAR ═══ -->
|
||||||
|
<aside class="sidebar" id="sidebar">
|
||||||
|
<div class="brand">
|
||||||
|
<img src="/static/sovran-hub-icon.svg" class="brand-logo" alt="The Hub" />
|
||||||
|
<div class="brand-text">
|
||||||
|
<div class="brand-title">The <em>Hub</em></div>
|
||||||
|
<div class="brand-sub">Sovran_SystemsOS v{{ sovran_version }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav id="sidebar-nav" aria-label="Service categories"></nav>
|
||||||
|
|
||||||
|
<div class="nav-label">System</div>
|
||||||
|
<div id="sidebar-support"></div>
|
||||||
|
|
||||||
|
<!-- Feature Manager + Preferences (features.js) -->
|
||||||
|
<div id="sidebar-features"></div>
|
||||||
|
|
||||||
|
<div class="sidebar-spacer"></div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- ═══ MAIN ═══ -->
|
||||||
|
<div class="main">
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="page-title" id="page-title">Dashboard</div>
|
||||||
|
|
||||||
|
<div class="search-box">
|
||||||
|
<svg><use href="#g-search"/></svg>
|
||||||
|
<input id="search-input" type="text" placeholder="Search services…" autocomplete="off" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="top-actions">
|
||||||
|
<button class="btn btn-header-reboot" id="btn-header-reboot" title="Restart the entire computer">Reboot</button>
|
||||||
|
<button class="btn btn-logout" id="btn-logout" title="Sign out">Sign Out</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="content" id="content">
|
||||||
|
<!-- ═══ WELCOME DASHBOARD (default view) ═══ -->
|
||||||
|
<section class="welcome" id="welcome-view">
|
||||||
|
<div class="welcome-bg" aria-hidden="true">
|
||||||
|
<div class="orb orb-a"></div>
|
||||||
|
<div class="orb orb-b"></div>
|
||||||
|
<div class="orb orb-c"></div>
|
||||||
|
</div>
|
||||||
|
<div class="welcome-inner">
|
||||||
|
<div class="welcome-greeting" id="welcome-greeting">Hello</div>
|
||||||
|
<h1 class="welcome-title">Welcome to Your <em>Sovereign</em> Digital & Financial Life</h1>
|
||||||
|
<div class="welcome-meta">Sovran_SystemsOS v{{ sovran_version }}<span class="welcome-meta-sep">·</span><span id="welcome-role"></span></div>
|
||||||
|
<div class="welcome-cards" id="welcome-cards">
|
||||||
|
<div class="welcome-dyn" id="wc-systems"></div>
|
||||||
|
<div class="widget w-network" id="w-network">
|
||||||
|
<div class="widget-chip chip-green"><svg><use href="#g-wifi"/></svg></div>
|
||||||
|
<div class="w-body">
|
||||||
|
<h3>Network</h3>
|
||||||
|
<div class="net-row"><span class="net-k">LAN</span><span class="ip-value" id="ip-internal">…</span></div>
|
||||||
|
<div class="net-row"><span class="net-k">WAN</span><span class="ip-value" id="ip-external">…</span></div>
|
||||||
|
<div class="sub">sovransystemsos.local</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="welcome-dyn" id="wc-more"></div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-ghost welcome-browse" id="welcome-browse-btn">Browse all services →</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ SERVICES GRID ═══ -->
|
||||||
|
<div id="tiles-area" style="display:none">
|
||||||
|
<div class="dashboard-loading" role="status" aria-live="polite">
|
||||||
|
<span class="dashboard-loading-spinner" aria-hidden="true"></span>
|
||||||
|
<span>Loading service status…</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-buttons">
|
|
||||||
<span class="role-badge" id="role-badge">Loading…</span>
|
|
||||||
<button class="btn btn-header-reboot" id="btn-header-reboot" title="Restart the entire computer">Reboot</button>
|
|
||||||
<button class="btn btn-logout" id="btn-logout" title="Sign out">Sign Out</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- IP bar -->
|
|
||||||
<div class="ip-bar">
|
|
||||||
<span>
|
|
||||||
<span class="ip-label">Internal IP:</span>
|
|
||||||
<span class="ip-value" id="ip-internal">…</span>
|
|
||||||
</span>
|
|
||||||
<span class="ip-separator">|</span>
|
|
||||||
<span>
|
|
||||||
<span class="ip-label">External IP:</span>
|
|
||||||
<span class="ip-value" id="ip-external">…</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Service tiles -->
|
<!-- ═══ SYSTEMS OPERATIONAL MODAL (dashboard.js) ═══ -->
|
||||||
<main class="main-content">
|
<div class="modal-overlay" id="systems-modal" role="dialog" aria-modal="true" aria-labelledby="systems-modal-title">
|
||||||
<aside class="sidebar" id="sidebar">
|
<div class="creds-dialog">
|
||||||
<div id="sidebar-support"></div>
|
<div class="creds-header">
|
||||||
<div id="sidebar-features"></div>
|
<span class="creds-title" id="systems-modal-title">
|
||||||
</aside>
|
<span class="widget-chip chip-green" style="width:54px;height:54px;border-radius:16px"><svg style="width:27px;height:27px"><use href="#g-pulse"/></svg></span>
|
||||||
<div id="tiles-area">
|
<span>Systems Operational</span>
|
||||||
<div class="dashboard-loading" role="status" aria-live="polite">
|
</span>
|
||||||
<span class="dashboard-loading-spinner" aria-hidden="true"></span>
|
<button class="creds-close-btn" id="systems-close-btn" title="Close">✕</button>
|
||||||
<span>Loading service status…</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="creds-body" id="systems-body">
|
||||||
|
<p class="creds-loading">Loading…</p>
|
||||||
|
</div>
|
||||||
|
<!-- Auto-launch preference (features.js) — moved here from the sidebar -->
|
||||||
|
<div class="sysprefs" id="autolaunch-slot"></div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
|
|
||||||
<!-- Update modal -->
|
<!-- Update modal -->
|
||||||
<div class="modal-overlay" id="update-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title-text">
|
<div class="modal-overlay" id="update-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title-text">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-header">
|
<div class="upd-header">
|
||||||
<span class="modal-title" id="modal-title-text">Sovran_SystemsOS Update</span>
|
<span class="widget-chip chip-sovran upd-chip"><svg class="upd-chip-svg"><use href="#g-update"/></svg></span>
|
||||||
<div class="modal-spinner" id="modal-spinner"></div>
|
<span class="upd-title-wrap">
|
||||||
<span class="modal-status" id="modal-status">Updating…</span>
|
<span class="upd-title" id="modal-title-text">Sovran_SystemsOS Update</span>
|
||||||
|
<span class="upd-sub">
|
||||||
|
<span class="ver-chip">Sovran_SystemsOS v{{ sovran_version }}</span>
|
||||||
|
<span class="upd-pill" id="upd-pill"></span>
|
||||||
|
<span class="modal-spinner" id="modal-spinner"></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<button class="creds-close-btn" id="update-close-btn" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body-scroll">
|
||||||
|
<div class="sysmodal-card">
|
||||||
|
<div class="sysmodal-card-title"><svg><use href="#g-server"/></svg>System Details</div>
|
||||||
|
<div class="sysstep"><div class="sysnum">1</div><div class="sysstep-x"><div class="sysstep-t">Current version</div><div class="sysval"><span class="sysval-text">{{ sovran_version }}</span></div></div></div>
|
||||||
|
<div class="sysstep"><div class="sysnum">2</div><div class="sysstep-x"><div class="sysstep-t">Release channel</div><div class="sysval"><span class="sysval-text">stable</span></div></div></div>
|
||||||
|
<div class="sysstep"><div class="sysnum">3</div><div class="sysstep-x"><div class="sysstep-t">Last checked</div><div class="sysval"><span class="sysval-text" id="upd-last-checked">—</span></div></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="update-status-msg" id="modal-status">Checking for updates…</div>
|
||||||
|
<div class="modal-log" id="modal-log" aria-live="polite"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-log" id="modal-log" aria-live="polite"></div>
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-save" id="btn-save-report" style="display:none">Save Error Report</button>
|
<button class="btn btn-save" id="btn-save-report" style="display:none">Save Error Report</button>
|
||||||
<button class="btn btn-save" id="btn-retry-update-status" style="display:none">Retry Status</button>
|
<button class="btn btn-save" id="btn-retry-update-status" style="display:none">Retry Status</button>
|
||||||
<button class="btn btn-reboot" id="btn-retry-update" style="display:none">Retry Update</button>
|
<button class="btn btn-reboot" id="btn-retry-update" style="display:none">Retry Update</button>
|
||||||
<button class="btn btn-reboot" id="btn-reboot" style="display:none">Restart Entire System</button>
|
<button class="btn btn-reboot" id="btn-reboot" style="display:none">Restart Entire System</button>
|
||||||
|
<span class="modal-footer-spacer"></span>
|
||||||
<button class="btn btn-close-modal" id="btn-close-modal" disabled>Close</button>
|
<button class="btn btn-close-modal" id="btn-close-modal" disabled>Close</button>
|
||||||
|
<button class="btn btn-primary" id="btn-check-again"><svg><use href="#g-refresh"/></svg>Check again</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,7 +357,10 @@
|
|||||||
<div class="modal-overlay" id="support-modal" role="dialog" aria-modal="true" aria-labelledby="support-modal-title">
|
<div class="modal-overlay" id="support-modal" role="dialog" aria-modal="true" aria-labelledby="support-modal-title">
|
||||||
<div class="creds-dialog">
|
<div class="creds-dialog">
|
||||||
<div class="creds-header">
|
<div class="creds-header">
|
||||||
<span class="creds-title" id="support-modal-title">Tech Support</span>
|
<span class="creds-title">
|
||||||
|
<span class="widget-chip chip-neutral" id="support-modal-chip" style="width:54px;height:54px;border-radius:16px"><svg style="width:27px;height:27px"><use href="#g-lifebuoy"/></svg></span>
|
||||||
|
<span id="support-modal-title">Tech Support</span>
|
||||||
|
</span>
|
||||||
<button class="creds-close-btn" id="support-close-btn" title="Close">✕</button>
|
<button class="creds-close-btn" id="support-close-btn" title="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="creds-body" id="support-body">
|
<div class="creds-body" id="support-body">
|
||||||
@@ -171,15 +432,26 @@
|
|||||||
<!-- Rebuild Modal -->
|
<!-- Rebuild Modal -->
|
||||||
<div class="modal-overlay" id="rebuild-modal" role="dialog" aria-modal="true" aria-labelledby="rebuild-modal-title">
|
<div class="modal-overlay" id="rebuild-modal" role="dialog" aria-modal="true" aria-labelledby="rebuild-modal-title">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-header">
|
<div class="upd-header">
|
||||||
<span class="modal-title" id="rebuild-modal-title">Sovran_SystemsOS Rebuild</span>
|
<span class="widget-chip chip-sovran upd-chip"><svg class="upd-chip-svg"><use href="#g-server"/></svg></span>
|
||||||
<div class="modal-spinner" id="rebuild-spinner"></div>
|
<span class="upd-title-wrap">
|
||||||
<span class="modal-status" id="rebuild-status">Rebuilding…</span>
|
<span class="upd-title" id="rebuild-modal-title">Sovran_SystemsOS Rebuild</span>
|
||||||
|
<span class="upd-sub">
|
||||||
|
<span class="ver-chip">Sovran_SystemsOS v{{ sovran_version }}</span>
|
||||||
|
<span class="upd-pill" id="rebuild-pill"></span>
|
||||||
|
<span class="modal-spinner" id="rebuild-spinner"></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<button class="creds-close-btn" id="rebuild-close-hdr" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body-scroll">
|
||||||
|
<div class="update-status-msg" id="rebuild-status">Rebuilding…</div>
|
||||||
|
<div class="modal-log" id="rebuild-log" aria-live="polite" style="display:none"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-log" id="rebuild-log" aria-live="polite"></div>
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-save" id="rebuild-save-report" style="display:none">Save Error Report</button>
|
<button class="btn btn-save" id="rebuild-save-report" style="display:none">Save Error Report</button>
|
||||||
<button class="btn btn-reboot" id="rebuild-reboot-btn" style="display:none">Restart Entire System</button>
|
<button class="btn btn-reboot" id="rebuild-reboot-btn" style="display:none">Restart Entire System</button>
|
||||||
|
<span class="modal-footer-spacer"></span>
|
||||||
<button class="btn btn-close-modal" id="rebuild-close-btn" disabled>Close</button>
|
<button class="btn btn-close-modal" id="rebuild-close-btn" disabled>Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -256,7 +528,7 @@
|
|||||||
<div class="reboot-overlay" id="reboot-overlay">
|
<div class="reboot-overlay" id="reboot-overlay">
|
||||||
<!-- Normal restarting card -->
|
<!-- Normal restarting card -->
|
||||||
<div class="reboot-card" id="reboot-main-card">
|
<div class="reboot-card" id="reboot-main-card">
|
||||||
<div class="reboot-icon" aria-hidden="true">↻</div>
|
<div class="reboot-icon" aria-hidden="true"></div>
|
||||||
<h2 class="reboot-title">Restarting Entire System</h2>
|
<h2 class="reboot-title">Restarting Entire System</h2>
|
||||||
<p class="reboot-message">
|
<p class="reboot-message">
|
||||||
The entire computer is restarting, including the desktop and all hosted services.<br />
|
The entire computer is restarting, including the desktop and all hosted services.<br />
|
||||||
@@ -271,7 +543,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Error card (shown if restart request fails definitively) -->
|
<!-- Error card (shown if restart request fails definitively) -->
|
||||||
<div class="reboot-card" id="reboot-error-card" style="display:none">
|
<div class="reboot-card" id="reboot-error-card" style="display:none">
|
||||||
<div class="reboot-icon" aria-hidden="true">⚠</div>
|
<div class="reboot-icon" aria-hidden="true"></div>
|
||||||
<h2 class="reboot-title">Restart could not be started</h2>
|
<h2 class="reboot-title">Restart could not be started</h2>
|
||||||
<p class="reboot-message">
|
<p class="reboot-message">
|
||||||
The computer did not begin restarting. No services were intentionally stopped. Please try again.
|
The computer did not begin restarting. No services were intentionally stopped. Please try again.
|
||||||
@@ -316,6 +588,7 @@
|
|||||||
<script src="/static/js/rebuild.js?v={{ asset_version }}"></script>
|
<script src="/static/js/rebuild.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/features.js?v={{ asset_version }}"></script>
|
<script src="/static/js/features.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/security.js?v={{ asset_version }}"></script>
|
<script src="/static/js/security.js?v={{ asset_version }}"></script>
|
||||||
|
<script src="/static/js/dashboard.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/events.js?v={{ asset_version }}"></script>
|
<script src="/static/js/events.js?v={{ asset_version }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Sovran Hub — Login</title>
|
<title>The Hub — Login</title>
|
||||||
<link rel="stylesheet" href="/static/css/base.css?v={{ asset_version }}" />
|
<link rel="stylesheet" href="/static/css/base.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/buttons.css?v={{ asset_version }}" />
|
<link rel="stylesheet" href="/static/css/buttons.css?v={{ asset_version }}" />
|
||||||
</head>
|
</head>
|
||||||
@@ -11,8 +11,9 @@
|
|||||||
<div class="login-wrapper">
|
<div class="login-wrapper">
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
<div class="login-header">
|
<div class="login-header">
|
||||||
<img src="/static/sovran-hub-icon.svg" alt="Sovran Hub" class="login-logo" />
|
<img src="/static/sovran-hub-icon.svg" alt="The Hub" class="login-logo" />
|
||||||
<div class="login-title">Sovran Hub</div>
|
<div class="login-title">The <em>Hub</em></div>
|
||||||
|
<div class="login-sub">Sovran_SystemsOS</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="login-form" id="login-form" onsubmit="return false;">
|
<form class="login-form" id="login-form" onsubmit="return false;">
|
||||||
|
|||||||
@@ -50,10 +50,10 @@
|
|||||||
<p class="onboarding-body-text" style="text-align:center; margin-bottom:4px;">
|
<p class="onboarding-body-text" style="text-align:center; margin-bottom:4px;">
|
||||||
Your new login password is:
|
Your new login password is:
|
||||||
</p>
|
</p>
|
||||||
<div id="migration-password-value" style="font-family:monospace; font-size:1.35rem; font-weight:700; color:var(--text-primary); background:rgba(109, 191, 139, 0.10); border:1.5px solid rgba(109, 191, 139, 0.35); border-radius:8px; padding:14px 24px; letter-spacing:0.04em; text-align:center; word-break:break-all; margin-bottom:8px;">
|
<div id="migration-password-value" style="font-family:monospace; font-size:1.35rem; font-weight:700; color:var(--text-primary); background:rgba(62, 207, 142, 0.10); border:1.5px solid rgba(62, 207, 142, 0.35); border-radius:8px; padding:14px 24px; letter-spacing:0.04em; text-align:center; word-break:break-all; margin-bottom:8px;">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div style="padding:10px 14px; background-color:rgba(229, 165, 10, 0.1); border:1px solid rgba(229, 165, 10, 0.35); border-radius:8px; font-size:0.92rem; color:var(--yellow); line-height:1.55;">
|
<div style="padding:10px 14px; background-color:rgba(233, 182, 74, 0.1); border:1px solid rgba(233, 182, 74, 0.35); border-radius:8px; font-size:0.92rem; color:var(--yellow); line-height:1.55;">
|
||||||
⚠ Write this password down! You will need it to log in next time. This is also your Sovran Hub login password.
|
⚠ Write this password down! You will need it to log in next time. This is also your Sovran Hub login password.
|
||||||
</div>
|
</div>
|
||||||
<div id="migration-password-status" class="onboarding-save-status" style="margin-top:8px;"></div>
|
<div id="migration-password-status" class="onboarding-save-status" style="margin-top:8px;"></div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"bitcoind.service": "27.1.0",
|
"bitcoind.service": "27.1.0",
|
||||||
"electrs.service": "0.10.6",
|
"electrs.service": "0.10.6",
|
||||||
"lnd.service": "0.18.0",
|
"lnd.service": "0.18.0",
|
||||||
"rtl.service": "0.15.10",
|
"rtl.service": "0.15.12",
|
||||||
"btcpayserver.service": "2.4.2",
|
"btcpayserver.service": "2.4.2",
|
||||||
"albyhub.service": "1.24.0",
|
"albyhub.service": "1.24.0",
|
||||||
"mempool.service": "3.2.1",
|
"mempool.service": "3.2.1",
|
||||||
|
|||||||
+1
-1
@@ -147,7 +147,7 @@
|
|||||||
hunspell hunspellDicts.en_US
|
hunspell hunspellDicts.en_US
|
||||||
synadm brave-origin dua
|
synadm brave-origin dua
|
||||||
gparted pv unzip parted screen zenity
|
gparted pv unzip parted screen zenity
|
||||||
libargon2 gnome-terminal libreoffice-fresh
|
libargon2 gnome-terminal libreoffice-stable
|
||||||
dig firefox wp-cli axel
|
dig firefox wp-cli axel
|
||||||
lk-jwt-service livekit-libwebrtc livekit
|
lk-jwt-service livekit-libwebrtc livekit
|
||||||
matrix-synapse age onlyoffice-desktopeditors
|
matrix-synapse age onlyoffice-desktopeditors
|
||||||
|
|||||||
Generated
+21
-21
@@ -5,11 +5,11 @@
|
|||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1787862680,
|
"lastModified": 1788871704,
|
||||||
"narHash": "sha256-mv+W62cI1Bjtkz9k8N8M7+7VSauv0/WmBrDYjcy5sE0=",
|
"narHash": "sha256-Vtc2SqCB7NOO028+K5JHbpWQ3XVpLyoC75MZQ12l13o=",
|
||||||
"owner": "emmanuelrosa",
|
"owner": "emmanuelrosa",
|
||||||
"repo": "btc-clients-nix",
|
"repo": "btc-clients-nix",
|
||||||
"rev": "f00585b12e751ac738392e2cccfbe305d077e2d4",
|
"rev": "e14502cba22806341f8c54c6f0c349094830c58b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -41,11 +41,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1785590095,
|
"lastModified": 1788179970,
|
||||||
"narHash": "sha256-CNO2szJbdLjVN/Hi1BML9MSALz1GM2fIdwnzs404QO8=",
|
"narHash": "sha256-r5LmxzIhsu5+oDybatN/HJ8roYOKjb2Apa5xI6v46VU=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "e568f3b19d54b08f48bfae9b12b3e124d1a28002",
|
"rev": "1db62ab7d2ccf1916bbf7deb61fc9d16f1c4ab49",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -56,11 +56,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-stable": {
|
"nixpkgs-stable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1788297115,
|
"lastModified": 1788921488,
|
||||||
"narHash": "sha256-Z+vUNbfd2FIKkWOTkcT7RYlh3oFCnig/d2eXD1SWf2E=",
|
"narHash": "sha256-8+xWRxEkD6l217cIUdRxfeUGS9lQX0hVtUuNVsBaDzk=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "a3116115851d68b8952a2a4221cc25a84e56b532",
|
"rev": "6aefcda9401be8acc2b74244fb3b37520ea1f0a8",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -72,11 +72,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-stable_2": {
|
"nixpkgs-stable_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1788297115,
|
"lastModified": 1788807765,
|
||||||
"narHash": "sha256-Z+vUNbfd2FIKkWOTkcT7RYlh3oFCnig/d2eXD1SWf2E=",
|
"narHash": "sha256-J9oC0bKnkXUrMegqRTXVkyDFJ0gn2U/Qpoo9HgGMQmA=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "a3116115851d68b8952a2a4221cc25a84e56b532",
|
"rev": "93108a538f079596c9a16c72cf03e9322782b6dd",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -88,11 +88,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1788179007,
|
"lastModified": 1788881743,
|
||||||
"narHash": "sha256-hn1oU2rue2SYK8dAr8+WNZWtbsz1S2W5mnHlSEuh3bo=",
|
"narHash": "sha256-2V9GZGvPfrNzxFozhI9dcqV+c3QdA8YZrvAAzqEB+dI=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "34ab99075ac4f7e40cf037eef32cb1c360bb85e9",
|
"rev": "d6524aaca2ff07876657ae2b323f24be4874944b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -120,11 +120,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_4": {
|
"nixpkgs_4": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1788179007,
|
"lastModified": 1788881743,
|
||||||
"narHash": "sha256-hn1oU2rue2SYK8dAr8+WNZWtbsz1S2W5mnHlSEuh3bo=",
|
"narHash": "sha256-2V9GZGvPfrNzxFozhI9dcqV+c3QdA8YZrvAAzqEB+dI=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "34ab99075ac4f7e40cf037eef32cb1c360bb85e9",
|
"rev": "d6524aaca2ff07876657ae2b323f24be4874944b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -169,11 +169,11 @@
|
|||||||
"nixpkgs-stable": "nixpkgs-stable_2"
|
"nixpkgs-stable": "nixpkgs-stable_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1788378694,
|
"lastModified": 1788921077,
|
||||||
"narHash": "sha256-s2yvGC1IHrQq2jP4KSbV2TA9Gh1T/YkcS+9neS/WcVI=",
|
"narHash": "sha256-k2i55M0lK3xzjsJKS/kOhQHSf8479r6RYEllnICxggg=",
|
||||||
"owner": "naturallaw777",
|
"owner": "naturallaw777",
|
||||||
"repo": "Sovran_Bitcoin",
|
"repo": "Sovran_Bitcoin",
|
||||||
"rev": "b4678fcc712da9dd01c167f62275192183490085",
|
"rev": "7c4d5b509c0533ea48192146f987182668bf42dc",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ let
|
|||||||
]; }
|
]; }
|
||||||
{ name = "LND"; unit = "lnd.service"; type = "system"; icon = "lnd"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = []; }
|
{ name = "LND"; unit = "lnd.service"; type = "system"; icon = "lnd"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = []; }
|
||||||
{ name = "Ride The Lightning"; unit = "rtl.service"; type = "system"; icon = "rtl"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
{ name = "Ride The Lightning"; unit = "rtl.service"; type = "system"; icon = "rtl"; enabled = cfg.services.bitcoin; category = "bitcoin-apps"; credentials = [
|
||||||
{ label = "Tor Address — Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/rtl/hostname"; prefix = "http://"; }
|
{ label = "Tor Address — Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/rtl/hostname"; prefix = "http://"; suffix = "/rtl/"; }
|
||||||
{ label = "Local Network — Access on your home network only"; file = "/var/lib/secrets/internal-ip"; prefix = "http://"; suffix = ":3051"; }
|
{ label = "Local Network — Access on your home network only"; file = "/var/lib/secrets/internal-ip"; prefix = "http://"; suffix = ":3051/rtl/"; }
|
||||||
{ label = "Password"; file = "/etc/nix-bitcoin-secrets/rtl-password"; }
|
{ label = "Password"; file = "/etc/nix-bitcoin-secrets/rtl-password"; }
|
||||||
{ label = "How to Access"; value = "• Tor Address: Open in Tor Browser from any device, anywhere in the world\n• Local Network: Open in any browser, but only when connected to your home network"; }
|
{ label = "How to Access"; value = "• Tor Address: Open in Tor Browser from any device, anywhere in the world\n• Local Network: Open in any browser, but only when connected to your home network"; }
|
||||||
]; }
|
]; }
|
||||||
|
|||||||
Reference in New Issue
Block a user