The Terminal page held all session state (xterm instances and their
WebSockets) in component-local React state. Because it renders as a
`<Route element={<Terminal />}>`, navigating away unmounted it and ran
the xterm cleanup (`term.dispose()` + `ws.close()`), tearing down every
SSH session. Returning to the page reconnected from scratch, losing
scrollback and any running work.
Lift terminal sessions into a `TerminalSessionProvider` mounted above the
router (in `main.tsx`, inside `AuthProvider`). The provider owns each
pane's xterm instance, fit addon, WebSocket, and a persistent wrapper DOM
node. Wrappers live in a hidden container at the app root; the Terminal
page re-parents them into its grid on mount and moves them back to the
hidden root on unmount instead of disposing — so the xterm + WebSocket
keep running in the background across route changes.
Disconnect semantics: closing a tab/pane (or shrinking the 1/2/4 grid)
destroys those sessions; logout tears down all sessions. A full browser
reload still drops connections (the WebSocket dies with the page) — this
persists across in-app navigation only.
Shared terminal constants/types/prefs are split into a non-component
module (`src/lib/terminalPrefs.ts`) so the context file stays a clean
component module.
Also document the terminal window grid-view tiering in ROADMAP.md
(self-hosted = 4-window cap, current; paid = as many as fit on screen,
planned for the AWS deployment), and realign HANDOFF/README/design-docs
to reflect that auth Phase 3 (multi-user) shipped and Phase 4 (SSO) is
deferred to a paid AWS add-on.
Verified with a clean `tsc -b && vite build` (frontend) and
`tsc --noEmit -p .` (backend).
Co-authored-by: Samuel James <ssamjame@amazon.com>
Co-authored-by: Kiro <noreply@kiro.dev>
284 lines
15 KiB
Markdown
284 lines
15 KiB
Markdown
# ArchNest — Design Decisions & Page Reference
|
|
|
|
> Living reference for visual/UX conventions and what's actually built on each page.
|
|
> Apply the Global Rules consistently to any new page. The Page Notes section
|
|
> describes the **real, deployed** implementation — not a mockup. If you change a
|
|
> page's layout or data source, update its section here in the same PR.
|
|
>
|
|
> This file replaces the old `archnest-blueprint.md` (original 6-page mockup
|
|
> pitch, since deleted — superseded by the real 11-page app below) and
|
|
> `glance.md` (original Glance spec with fictional config files like
|
|
> `systems.config`/`infra.config` that were never built — also deleted). Anything
|
|
> from those files still true (color palette, typography, card style, dropdown
|
|
> menu shape) has been folded in below.
|
|
|
|
---
|
|
|
|
## Global Rules (apply to every page)
|
|
|
|
### Sidebar
|
|
- Expanded width: 200px. Collapsed width: 64px (icons only).
|
|
- User can manually collapse/expand via a toggle button (not just responsive breakpoints).
|
|
- Main content `margin-left` must match the sidebar width exactly.
|
|
- Bottom of sidebar: live "System Status" widget — green dot + "All Systems
|
|
Operational" or "X Issue(s) Detected", driven by polling integration status
|
|
(not the fictional ping-sweep config described in the old spec).
|
|
|
|
### Page Title (Top Bar)
|
|
- Color: gold `#C8A434` — not white. Use inline `style={{ color: '#C8A434' }}`
|
|
if a Tailwind class doesn't apply.
|
|
- Font: 18px, bold, uppercase, tracking-wide.
|
|
- No border on the top bar — it blends into the page background.
|
|
- Pages with a `pageSubtitles` entry (currently only BookNest) render a larger
|
|
28px title + subtitle line instead, and the top bar grows from 56px → 72px —
|
|
see `TopBar.tsx`'s `pageSubtitles` map and `App.tsx`'s `topBarHeight` lookup,
|
|
which must be kept in sync or the layout clips/gaps.
|
|
|
|
### Colors
|
|
| Role | Value |
|
|
|------|-------|
|
|
| Background (page) | `#0D0E10` |
|
|
| Background (cards) | `#141518` |
|
|
| Background (sidebar) | `#0A0B0D` |
|
|
| Border (cards) | `#1E2025` |
|
|
| Border/accent (hover/active) | `#C8A434` (gold) |
|
|
| Success | `#2ECC71` |
|
|
| Warning | `#E67E22` |
|
|
| Danger | `#E74C3C` |
|
|
| Text (primary) | `#E8E6E0` |
|
|
| Text (secondary) | `#7A7D85` |
|
|
|
|
Tailwind v4 `@theme` custom colors (`text-gold`, `bg-card`, etc.) don't always
|
|
apply reliably — fall back to inline `style={{ color: '#C8A434' }}` when a
|
|
color isn't rendering, and verify visually after changes.
|
|
|
|
### Typography
|
|
- Card titles: 10-11px, uppercase, tracking-[1.5px], secondary color, font-medium.
|
|
- Large numbers: 24-28px, bold, primary color.
|
|
- Subtitles/labels: 10-11px, secondary color.
|
|
- Body text in lists: 13px, primary color.
|
|
- Timestamps: 11px, secondary color.
|
|
|
|
### Card Style
|
|
- Border radius: 12px. Background `#141518`. Border 1px solid `#1E2025`.
|
|
- Padding: 16-24px depending on density. No shadows (flat design).
|
|
- Hover: border transitions to gold, 0.2s ease.
|
|
|
|
### Content Alignment
|
|
- All rows in a page share the same horizontal padding (`px-6` at the parent
|
|
container level) so the hero banner, status cards, and content rows line up
|
|
left/right.
|
|
|
|
### Animations
|
|
- Card hover: border → gold, 0.2s ease.
|
|
- Progress ring / sparkline: animate from 0 on load, ~1s.
|
|
- Progress bars: fill animation ~0.8s.
|
|
|
|
### Icons
|
|
- Lucide React, 14-18px depending on context. Gold for active/accent, secondary
|
|
color for inactive. Gold glow on active sidebar items:
|
|
`shadow-[0_0_6px_rgba(200,164,52,0.5)]`.
|
|
- **Gotcha**: the installed lucide-react version's TypeScript types list some
|
|
brand/wordmark icons (`Github`, `Gitlab`, `Linkedin`, `Youtube`) that aren't
|
|
actually exported at runtime — `tsc --noEmit` stays clean while the page
|
|
renders blank with a runtime `SyntaxError` only visible in the Vite dev log.
|
|
Verify icon names against the package's actual exports before importing
|
|
anything brand-flavored.
|
|
|
|
### No Footer
|
|
- No page has a fixed footer/status bar. Don't add one unless explicitly requested.
|
|
|
|
### Target Display
|
|
- Primary design target: 16-inch / 1920px-wide screen. Don't constrain content
|
|
width unnecessarily — design should feel spacious.
|
|
|
|
### User Avatar & Dropdown (TopBar, every page)
|
|
- 36px circle, gold border + glow, shows uploaded avatar image or initials.
|
|
- Adjacent: display name + "Administrator" role label, chevron (rotates on open).
|
|
- Dropdown menu: header (name + email) → **Profile** (`/settings?tab=profile`)
|
|
→ **Appearance** (`/settings?tab=appearance`) → **Security**
|
|
(`/settings?tab=security`) → **Help & Support** (`/help`) → divider →
|
|
**Sign Out** (red, danger styling). See `TopBar.tsx`.
|
|
|
|
---
|
|
|
|
## Page Notes (what's actually built, page by page)
|
|
|
|
All pages below consume real backend data via `src/lib/api.ts` — **zero mock
|
|
data anywhere in the app.** Where a section says "real backend data," it means
|
|
an actual SQL-backed or live-polled endpoint, not a config file or static array.
|
|
|
|
### Glance (`/`)
|
|
- Hero banner (image with radial-gradient fade mask) + KPI cards overlapping
|
|
the bottom edge via negative margin.
|
|
- 4 status cards: **System Status** (% of integrations reachable),
|
|
**Infrastructure** (resource count + health breakdown from
|
|
`api.listResources()`), **Integrations** (connected/total from
|
|
`api.listIntegrations()`), **Bookmarks** (total + favorites from
|
|
`api.listBookmarks()`).
|
|
- Middle row (3 columns): Resource Overview (top 6 resources + status),
|
|
Recent Activity (5 latest from `api.listEvents(5)`), problem/critical
|
|
resources widget.
|
|
- Bottom row: Connected Integrations grid + 4 Shortcuts buttons (to Settings,
|
|
BookNest, Infrastructure).
|
|
- No footer. Hides the hero gracefully if the banner image fails to load.
|
|
- There is **no** `systems.config`/`infra.config`/ping-sweep/fail2ban
|
|
machinery — that was speculative spec from before the backend existed.
|
|
Health figures are derived directly from each integration adapter's
|
|
`testConnection()` result, polled by the existing `/api/integrations` list.
|
|
|
|
### Infrastructure (`/infrastructure`)
|
|
- Hero banner at the `App.tsx` layout level (`showHero` includes this route),
|
|
extending behind the sticky, transparent-on-hero-routes TopBar.
|
|
- Sub-tabs: **Overview** (only enabled tab) and **Network** (disabled,
|
|
"Coming soon" — intentional, leave alone unless asked).
|
|
- 4 status cards: Total Resources, Healthy, Warnings, Critical — from
|
|
`api.listResources()`.
|
|
- Middle row: Resource Distribution (donut by integration type) + Node Status
|
|
(4-col tile grid, one tile per resource — replaced an earlier world-map
|
|
concept since a map doesn't make sense for a small/single-site setup).
|
|
- Bottom row: Integration Health list + Recent Activity (`api.listEvents(4)`).
|
|
- Card backgrounds use a `cardDim` + `cardVignette` (radial-gradient,
|
|
`closest-side` keyword — a fixed `%` leaves a visible hard edge on straight
|
|
sides) combo on the middle row only, per explicit visual preference; bottom
|
|
row stays plain/regular.
|
|
|
|
### BookNest (`/booknest`)
|
|
- Hero banner reused at layout level with page-specific tuning
|
|
(`heroPaddingTop`/`heroObjectPosition` lookup maps in `App.tsx`).
|
|
- Large 28px title + "Your Digital Library" subtitle (the one page using
|
|
`pageSubtitles`), header grows to 72px.
|
|
- Page stats row (Links / Categories / Favorites) + "Delete All" bulk action.
|
|
- Main column: Quick Access (top 5 categories) + bookmark groups grid (4 cols,
|
|
full CRUD with hover edit/delete/star).
|
|
- Right sidebar (spans both grid rows via `gridRow: '1 / span 2'`): Favorites,
|
|
Recently Added (5 newest), Link Health donut, Category Breakdown donut.
|
|
- Add/edit bookmark modal supports auto-detected favicons via
|
|
`guessServiceIconUrl()` or manual icon entry.
|
|
- All data from `api.listBookmarks()` / `api.listBookmarkCategories()` with
|
|
real create/update/delete calls — no local-only state.
|
|
|
|
### Terminal (`/terminal`)
|
|
- Left sidebar: SSH hosts (integrations of type `ssh`), click to connect.
|
|
- Tab bar + 1/2/4-pane split layout, each pane an independent xterm instance.
|
|
- Preferences panel (theme: ArchNest Dark/Matrix/Solarized/Midnight Blue, font
|
|
size 11-16px, font family) — stored in `localStorage`
|
|
(`archnest-terminal-prefs`), not synced server-side.
|
|
- WebSocket to `/api/terminal`: `connect`/`input`/`resize`/`list_tmux`/`disconnect`
|
|
messages; supports attaching to an existing tmux session or starting a new one.
|
|
- Certificate auth (OPKSSH) shells out to the system `ssh` binary under a pty
|
|
rather than using the JS SSH client.
|
|
- Session logging to `ARCHNEST_SESSION_LOG_DIR` is backend-supported but has
|
|
no dedicated viewer UI yet.
|
|
|
|
### Tunnels (`/tunnels`)
|
|
- List + create form for local/remote/dynamic (SOCKS5) SSH tunnels.
|
|
- Create form fields conditionally show/hide based on mode (no endpoint
|
|
host/port fields for dynamic mode).
|
|
- Status color map: stopped `#7A7D85`, connecting `#C8A434`, retrying
|
|
`#E0A030`, connected `#2ECC71`, error `#E74C3C`. Polls every 3s.
|
|
- Backed by `api.listTunnels()/createTunnel()/connectTunnel()/disconnectTunnel()/deleteTunnel()`.
|
|
|
|
### Files (`/files`)
|
|
- SSH host selector + breadcrumb-navigable SFTP directory browser.
|
|
- File editor modal: plain textarea, files up to 50MB (anything larger is
|
|
download-only). Heuristic binary detection (null bytes/control chars) decides
|
|
base64 vs. text encoding.
|
|
- Inline directory creation, rename, delete, upload, download.
|
|
- Host-to-host transfer modal: pick source file + destination host/path,
|
|
copy-or-move toggle, live progress bar fed by `api.getTransfer(id)` polling.
|
|
|
|
### Containers (`/containers`)
|
|
- Docker host selector (integrations of type `docker`) + container list.
|
|
- Per-container state badge (running/paused/exited/dead) with context-aware
|
|
action buttons (Start/Stop/Restart/Pause/Unpause/Remove) — buttons disable
|
|
themselves for invalid transitions (e.g. can't pause a stopped container).
|
|
- Live CPU/memory stats polled only for running containers.
|
|
- Logs modal (configurable tail count) and an exec modal (interactive shell via
|
|
WebSocket to `/api/docker/exec`).
|
|
|
|
### Remote Desktop (`/remote-desktop`)
|
|
- Left sidebar: hosts from integrations of type `remote_desktop`.
|
|
- Main area is a Guacamole canvas tunneled over WebSocket to `/api/guacamole`,
|
|
proxying to the `guacd` sidecar container (RDP/VNC/Telnet).
|
|
- Requires `ARCHNEST_GUAC_CRYPT_KEY` — sessions fail without it (the backend
|
|
warns on startup if it's missing).
|
|
|
|
### Host Metrics (`/host-metrics`)
|
|
- Left sidebar: SSH hosts. Selecting one starts a 5-second polling loop against
|
|
`api.getHostMetrics(integrationId)`, cleared on deselect.
|
|
- Cards: CPU/Memory/Disk gauges (color thresholds: green <75%, yellow 75-90%,
|
|
red ≥90%) + Uptime, then Network Interfaces, Processes (top 10),
|
|
Listening Ports, Firewall Rules (UFW/iptables), Login Activity.
|
|
- Backed by 10 sequential SSH-exec collectors under `backend/src/ssh/metrics/`
|
|
(cpu, memory, disk, uptime, network, system, processes, ports, firewall,
|
|
login-stats) — sequential on purpose, to avoid exceeding OpenSSH's
|
|
`MaxSessions` limit per host.
|
|
|
|
### Settings (`/settings`)
|
|
- Fixed 200px left nav, 7 sections: Profile, Appearance, Security,
|
|
Integrations, Notifications, Data & Backup, About. URL-deep-linkable via
|
|
`?tab=` (`useSearchParams`) — use this pattern for any future section.
|
|
- **Profile**: display name, email, avatar upload (`FileReader.readAsDataURL`
|
|
→ base64 data URL, persisted via `api.updateMe()` — this one *is* a real
|
|
backend round-trip, unlike the rest of the page below).
|
|
- **Appearance**: accent color swatches — **local-state only, doesn't persist
|
|
or apply anywhere** (see Known Stubs in `HANDOFF.md`).
|
|
- **Security**: password-change form + 2FA toggle — currently placeholder UI
|
|
pending the Phase 2 auth work in `HANDOFF.md`.
|
|
- **Integrations**: one card per type (Proxmox/Docker/NetBird/Cloudflare/AWS/
|
|
Uptime Kuma/Weather/Remote Desktop/SSH), each with type-specific fields
|
|
(secrets masked, eye-toggle to reveal, "Test Connection" button). Real CRUD
|
|
via `api.createIntegration()/updateIntegration()/deleteIntegration()/testIntegration()`.
|
|
- **Notifications**: toggles — placeholder, no delivery mechanism (see
|
|
Known Stubs in `HANDOFF.md`).
|
|
- **Data & Backup**: full JSON export/import of integrations + secrets +
|
|
bookmarks + tunnels via `backend/src/routes/data.ts` — this is the one
|
|
endpoint that intentionally round-trips decrypted secrets, by design, for
|
|
backup portability.
|
|
- **About**: static version/license/links info.
|
|
|
|
### Help (`/help`)
|
|
- Fully static — no backend calls. 11 guide cards (one per real page) with a
|
|
short description and tips. Update this page whenever a new page ships.
|
|
|
|
### Login (`/login`) / Enrollment (`/enrollment`)
|
|
- `Login.tsx`: username/password form → `api.login()` → `AuthContext`.
|
|
- `Enrollment.tsx`: two-step first-run flow — (1) create the admin account via
|
|
`api.setup()`, gated to only work while the `users` table is empty; (2)
|
|
optional "Connect Your Services" integration onboarding grid (skippable,
|
|
same integration form as Settings). Routed to based on
|
|
`GET /api/system/setup-status`.
|
|
|
|
---
|
|
|
|
## Backend Architecture (current, not aspirational)
|
|
|
|
- `backend/` — Fastify 5 + TypeScript (ESM, `tsx` dev / `tsc -b` build),
|
|
`better-sqlite3` for storage, deployed as its own Docker container alongside
|
|
the frontend and a `guacd` sidecar.
|
|
- Auth: JWT (`@fastify/jwt`) with server-tracked sessions and a multi-user
|
|
schema (admin/member roles, 10-seat cap). Authentik SSO is deferred to a
|
|
paid AWS add-on — see `ROADMAP.md`.
|
|
- Integration credentials split across `integrations` (non-secret config) and
|
|
`secrets` (AES-256-GCM-encrypted, keyed by `ARCHNEST_SECRET_KEY`) tables —
|
|
secrets never appear in a generic "list integrations" response by
|
|
construction, not by remembering to redact a field.
|
|
- Every integration type has an adapter in `backend/src/integrations/`
|
|
implementing `testConnection()` (required) and `listResources()` (optional);
|
|
`registry.ts` maps `IntegrationType` → adapter. All 9 types
|
|
(proxmox/docker/netbird/cloudflare/aws/uptime_kuma/weather/ssh/remote_desktop)
|
|
are real, working adapters — none are stubs anymore.
|
|
- `backend/src/ssh/` is the shared SSH transport layer powering Terminal,
|
|
Files, Tunnels, Transfers, and Host Metrics: `connect.ts` (jump-host
|
|
chaining, host-key verification, cert auth), `sftp.ts` (ephemeral SFTP),
|
|
`transfer.ts` (host-to-host streamed copy/move with progress + cancel), and
|
|
`metrics/` (the 10 collectors listed above).
|
|
- Vite dev server proxies `/api` → `http://localhost:4000`; prod routes `/api`
|
|
to the backend container via Nginx Proxy Manager.
|
|
|
|
## Future Integration Notes
|
|
- AWS/Cloudflare/NetBird/Proxmox/Uptime Kuma adapters are real but currently
|
|
surface basic resource inventory + health only — deeper cost/pricing/budget
|
|
data (mentioned in the old blueprint) is not implemented and not currently
|
|
planned; revisit only if explicitly requested.
|