diff --git a/Docs/Frontend.md b/Docs/Frontend.md index 3e78e67..3583747 100644 --- a/Docs/Frontend.md +++ b/Docs/Frontend.md @@ -2,128 +2,303 @@ ## Stack -- Vue 3 -- Pinia -- Vite -- Native Fetch API -- Native WebSocket API -- TailwindCSS -- Custom OKLCH Design System +Vue 3 · Pinia · Vite · TailwindCSS · Native WebSocket · OKLCH Design System --- # Architecture Overview -The frontend follows a strict layered architecture: +Strict layered architecture. Data flows one way. Each layer owns its responsibility and nothing else. -| Layer | Responsibility | -|---|---| -| Views & Components | UI rendering only | -| Stores (Pinia) | Global state + business logic | -| Services | Persistent network systems | -| Utils | Shared helpers/theme logic | +``` +┌─────────────────────────────────┐ +│ Views & Components │ UI rendering + ephemeral state +├─────────────────────────────────┤ +│ Stores (Pinia) │ Business logic + state ownership +├─────────────────────────────────┤ +│ API Client · Services │ Transport + error normalization +├─────────────────────────────────┤ +│ Utils │ Shared helpers +└─────────────────────────────────┘ +``` -Components never directly manage backend state. +**No layer reaches above itself.** Views never call APIs. Stores never import Vue. Services never touch state. -All backend communication flows through Pinia stores. +--- + +# Core Principles + +- Deterministic state flow +- Centralized business logic +- Optimistic UI with rollback safety +- Transport abstraction +- Framework-agnostic services +- Zero duplicated network logic +- Stable reactive identity +- Normalized backend data --- # Project Structure -```text -src/ -├── api/ # API composables/config loaders -├── assets/ # Global styles & design system -├── components/ -│ ├── dashboard/ # Dashboard widgets -│ ├── layout/ # Shell layout components -│ └── ui/ # Reusable design-system primitives -│ -├── services/ # Persistent services (WebSocket) -├── stores/ # Pinia state domains -├── utils/ # Shared utilities/helpers -├── views/ # Route-level screens -│ -├── App.vue # Root orchestrator -└── main.js # Application bootstrap ``` +src/ +├── api/ +│ ├── client.js # Centralized HTTP client +│ └── useConfig.js # Config loader composable +│ +├── assets/ +│ └── style.css # Design system tokens +│ +├── components/ +│ ├── dashboard/ # Dashboard widgets +│ ├── layout/ # App shell components +│ └── ui/ # Reusable UI primitives +│ +├── services/ +│ └── ws.js # WebSocket service +│ +├── stores/ +│ ├── app.js # App + auth state +│ ├── events.js # Event pipeline +│ └── fleet.js # Infrastructure state +│ +├── utils/ +│ ├── chartConfig.js +│ ├── theme.js +│ └── useDropdown.js +│ +├── views/ +│ ├── Dashboard.vue +│ ├── FleetView.vue +│ ├── Login.vue +│ ├── NodeDetailView.vue +│ ├── Settings.vue +│ ├── Setup.vue +│ └── Store.vue +│ +├── App.vue # Root orchestrator +└── main.js # Bootstrap +``` + +--- + +# Layer Responsibilities + +## Views + +Own ephemeral UI state only: + +- Modal open/close +- Form input values +- Local loading flags +- Layout composition + +**Never:** call APIs, normalize data, own business state, implement rollback. + +--- + +## Components + +Presentation-focused primitives. + +**Should:** receive data via props/stores, emit actions upward, stay stateless when possible. + +**Should not:** import the API client, call `fetch()`, own business logic, normalize backend payloads. + +--- + +## Stores + +Single source of truth. All backend communication flows through stores. + +Stores own: + +- Async requests +- Optimistic mutations + rollback +- Backend normalization +- WebSocket mutations +- Derived + indexed state +- Selection state + +--- + +## API Client + +`src/api/client.js` — centralized HTTP transport. + +```js +api.get(url) +api.post(url, body) +api.patch(url, body) +api.delete(url) +api.request(url, options) // non-JSON bodies, custom headers +``` + +The client owns transport, serialization, and error normalization. Nothing else. + +Non-2xx throws `ApiError` with `.status` and `.message`. Stores catch and rollback. + +**The client does NOT own:** business logic, rollback behavior, Vue state. --- # State Architecture -## `app.js` +## `app.js` — Application + Auth -Global application/UI state. +| Responsibility | Key State | +|---|---| +| Auth lifecycle | `isAuthenticated`, `login()` | +| Setup flow | `requiresSetup`, `completeSetup()` | +| Active routing | `currentView` | +| Sidebar | `sidebarOpen` | +| Archive mode | `viewingArchive` | +| Armed state | `isArmed`, `toggleArmed()` | +| Version | `version` | +| Timeframes | `activeTimeframe`, `velocityTimeframe` | -### Responsibilities +### Auth Lifecycle Rule -- active view routing -- archive mode -- sidebar state -- armed/disarmed state -- version/config state -- authentication/logout +> **`login()` does NOT set `isAuthenticated`.** App.vue controls shell reveal. ---- +This prevents two critical bugs: -## `fleet.js` +1. **Login unmount race** — if `isAuthenticated` flips during `login()`, the Login component unmounts before emitting its success event, so `loadAppData()` never runs. +2. **Empty store mount** — if the shell reveals before stores populate, Dashboard mounts into empty data. -Infrastructure state. +**Correct sequence:** -### Responsibilities - -- nodes -- installed sensors -- node details -- uptime metrics -- sensor selection -- node deployment state - -### Important - -Sensors are identified using: - -```text -node_id + sensor_id +``` +login() succeeds → returns { success: true } + ↓ +Login emits 'login-success' + ↓ +App.vue calls loadAppData() + ↓ +stores populate + WebSocket connects + ↓ +isAuthenticated = true ← shell revealed LAST ``` -This prevents cross-node collisions. +--- + +## `fleet.js` — Infrastructure + +| Responsibility | Key State | +|---|---| +| Nodes | `nodes`, `fetchFleet()`, `createNode()` | +| Sensors | `getSensor()`, indexed access | +| Uptime | `uptimeData`, `fetchUptime()` | +| Deployment | `deployingSensors`, `deploySensor()` | +| Syncing | `syncingNodes`, `syncNode()` | +| WS updates | `handleWsUpdate()` | + +### Sensor Identity + +> **Always use `node_id + sensor_id` as a composite key.** + +Never reference `sensor_id` globally — it's only unique within a node. Cross-node collisions will occur otherwise. + +### Normalization + +Backend payloads are normalized once at the store boundary: + +```js +raw.last_heartbeat → lastHeartbeat +``` + +Components never normalize. They consume stable, frontend-safe structures. + +### Reactive Merge + +Preserve array identity. Never reassign: + +```js +// ✅ Safe — preserves identity +array.splice(index, 1, newItem) +Object.assign(existing, updates) + +// ❌ Breaks watchers and computed dependencies +array = filteredArray +``` + +### Indexed Access + +O(1) lookup via computed maps: + +```js +getNode(nodeId) +getSensor(nodeId, sensorId) +``` + +Avoids repeated nested scans across components. --- -## `events.js` +## `events.js` — Telemetry Pipeline -Telemetry + alert pipeline. - -### Responsibilities - -- realtime events -- unread counters -- filtering -- archive handling -- websocket event ingestion +| Responsibility | Key State | +|---|---| +| Realtime events | `events`, `handleWsEvent()` | +| Filtering | `filteredEvents` (computed) | +| Unread tracking | `unreadCount`, `markEventRead()` | +| Archive | `archiveEvent()`, `archiveAll()` | +| Bulk operations | `purgeEvents()` | --- # Data Flow -HoneyWire uses strict unidirectional flow: +Strict unidirectional: -```text -UI Interaction +``` +User Action → View → Store Action → API Client → Backend ↓ -Pinia Action - ↓ -API Request / State Mutation - ↓ -Reactive Update - ↓ -Component Re-render +Store Mutation → Reactive Update → Component Re-render ``` -Views never mutate state directly. +Views never mutate persistent state directly. + +--- + +# Bootstrap Sequences + +## Cold Boot (page load) + +``` +onMounted() + → checkRequiresSetup() + → checkSystemState() + → loadAppData() + → fetchConfig() + → checkSetupStatus() + → fetchFleet() + fetchUptime() + fetchEvents() + → connect WebSocket + → isAuthenticated = true +``` + +## Post-Login + +``` +login() → { success: true } + → Login emits 'login-success' + → onLoginSuccess() + → loadAppData() + → isAuthenticated = true +``` + +## Post-Setup + +``` +completeSetup() → { success: true } + → Setup emits 'setup-complete' + → onSetupComplete() + → loadAppData() + → isAuthenticated = true +``` + +**Invariant:** `isAuthenticated` is set **after** all data loads. Components mount into populated stores. --- @@ -131,40 +306,34 @@ Views never mutate state directly. ## Service Layer -Location: +`src/services/ws.js` — framework-agnostic. -```text -src/services/ws.js -``` - -The WS service: - -- owns socket lifecycle -- reconnects automatically -- emits parsed payloads -- contains zero Vue logic - ---- +- Owns socket lifecycle +- Auto-reconnects +- Emits parsed payloads +- Zero Vue/Pinia imports ## App Orchestrator -`App.vue` wires: +`App.vue` wires events to store actions: -```text -WebSocket Events - ↓ -Store Actions - ↓ -Reactive UI Updates +``` +NEW_EVENT → eventsStore.handleWsEvent() +NEW_SENSOR → fleetStore.handleWsUpdate('NEW_SENSOR') +DELETE_SENSOR → fleetStore.handleWsUpdate('DELETE_SENSOR') +NODE_SYNCED → fleetStore.handleWsUpdate('NODE_SYNCED') +... ``` -Example: +## Smart Reconnect + +On reconnect, refetch authoritative state: -```text -NEW_EVENT - ↓ -eventsStore.handleWsEvent() ``` +WS reconnect → fetchFleet() + fetchUptime() + fetchEvents() +``` + +Guarantees recovery of any missed realtime events. --- @@ -172,245 +341,91 @@ eventsStore.handleWsEvent() | View | Purpose | |---|---| -| Dashboard.vue | Telemetry & analytics | -| FleetView.vue | Node management | -| NodeDetailView.vue | Sensor deployment/configuration | -| Store.vue | Sensor catalog | -| Settings.vue | Hub configuration | -| Setup.vue | Initial setup flow | -| Login.vue | Authentication | +| Dashboard | Telemetry & analytics | +| FleetView | Node management | +| NodeDetailView | Sensor deployment & configuration | +| Store | Sensor catalog | +| Settings | Hub configuration | +| Setup | Initial setup flow | +| Login | Authentication | --- # Component System -## Dashboard Components +## Dashboard Widgets -Located in: - -```text -components/dashboard/ -``` - -Contains telemetry widgets: +`components/dashboard/` — data-driven, stateless when possible: - EventTable - SeverityChart - ThreatVelocity -- UptimeHeatmap - TrafficFilters +- UptimeHeatmap -These components are data-driven and stateless whenever possible. +## UI Primitives ---- +`components/ui/` — reusable design-system primitives: -## UI Component Library - -Located in: - -```text -components/ui/ -``` - -Reusable primitives grouped by domain. - -### Categories - -| Folder | Purpose | +| Category | Contents | |---|---| -| branding | logos/theme | -| feedback | alerts/modals/status | -| forms | inputs/buttons/selectors | -| layout | cards/widgets/page shells | -| navigation | menus/sidebar/nav | +| branding | Logos, theme elements | +| feedback | Alerts, modals, status | +| forms | Inputs, buttons, selectors | +| layout | Cards, widgets, page shells | +| navigation | Menus, sidebar, nav | --- # Design System -HoneyWire uses a centralized token-based design system. +`src/assets/style.css` — centralized token system on OKLCH color space. -Location: +## Principles -```text -src/assets/style.css -``` +- Semantic tokens only — no hardcoded colors in components +- Accessibility-first contrast +- Dark/light parity +- Theme switching swaps root variables only -Built entirely on CSS custom properties using OKLCH color space. +## Token Categories ---- +| Category | Examples | Usage | +|---|---|---| +| Structural | `--bg`, `--bg-surface`, `--border-default` | Layout hierarchy | +| Interactive | `--primary-main`, `--secondary-main`, `--danger-main` | Actions & states | +| Severity | `--sev-critical` through `--sev-info` | Charts, alerts, tables, status | +| Typography | `--text-h1`, `--text-base`, `--text-sm` | All text sizing | +| Spacing | `--space-card-p`, `--space-flow` | Layout rhythm | +| Elevation | `--radius-sm`, `--shadow-md` | Cards, modals, widgets | +| Z-index | `--z-dropdown` → `--z-toast` | Stacking hierarchy | -# Theme Architecture +**No arbitrary values.** Use tokens. -## Core Principles - -- semantic tokens only -- no hardcoded component colors -- light/dark parity -- accessibility-first contrast -- consistent spacing scale - ---- - -# Color System - -## Structural Colors - -Used for layout hierarchy. - -Examples: - -```css ---bg ---bg-base ---bg-surface ---border-default -``` - ---- - -## Interactive Colors - -Used for actions/buttons. - -Examples: - -```css ---primary-main ---secondary-main ---danger-main -``` - ---- - -## Severity Tokens - -Telemetry severity colors: - -```css ---sev-critical ---sev-high ---sev-medium ---sev-low ---sev-info -``` - -Used consistently across: - -- charts -- alerts -- status dots -- event tables - ---- - -# Typography - -Two-font system: +## Typography | Token | Font | |---|---| | `--font-sans` | Inter | | `--font-mono` | JetBrains Mono | -Typography is fully tokenized: +## Tailwind Integration + +Tokens exposed via `@theme`: ```css ---text-h1 ---text-base ---text-sm +bg-bg-surface → --bg-surface +text-text-h → --text-h +border-border-default → --border-default ``` ---- - -# Spacing System - -Consistent layout rhythm: +## Dark Mode ```css ---space-card-p ---space-flow ---space-label-gap +.dark { /* overrides root variables */ } ``` -No arbitrary spacing should be introduced in components. - ---- - -# Radius & Elevation - -## Radius Tokens - -```css ---radius-sm ---radius-md ---radius-lg -``` - -## Shadow Tokens - -```css ---shadow-sm ---shadow-md ---shadow-lg -``` - -All cards/modals/widgets use shared elevation values. - ---- - -# Z-Index System - -Strict stacking hierarchy: - -| Token | Usage | -|---|---| -| `--z-dropdown` | menus | -| `--z-overlay` | backdrops | -| `--z-modal` | dialogs | -| `--z-toast` | notifications | - -Avoid arbitrary z-index usage. - ---- - -# Tailwind Integration - -The design system is exposed through Tailwind using: - -```css -@theme -``` - -This maps CSS tokens into utility classes. - -Example: - -```css -bg-bg-surface -text-text-h -border-border-default -``` - -This ensures: - -- consistent theming -- centralized color control -- instant dark-mode support - ---- - -# Dark Mode - -Dark mode is token-driven using: - -```css -.dark { ... } -``` - -Components never define separate dark styles manually unless necessary. - -Theme switching only swaps root variables. +Components should not implement separate dark styles unless absolutely necessary. --- @@ -418,88 +433,90 @@ Theme switching only swaps root variables. ## Components -Components SHOULD: +| ✅ Do | ❌ Don't | +|---|---| +| Stay presentation-focused | Import the API client | +| Receive data via props/stores | Call `fetch()` | +| Emit actions upward | Own business logic | +| Remain reusable | Normalize backend payloads | -- remain presentation-focused -- receive data via props/stores -- emit actions upward +## Views -Components SHOULD NOT: - -- perform complex business logic -- own global state -- duplicate API calls - ---- +| ✅ Do | ❌ Don't | +|---|---| +| Own ephemeral UI state | Perform backend mutations | +| Delegate to stores | Duplicate rollback logic | +| Orchestrate layout | Normalize API responses | ## Stores -Stores are the single source of truth. - -Stores: - -- own async requests -- normalize backend data -- handle websocket mutations -- expose reactive state - ---- +| ✅ Do | ❌ Don't | +|---|---| +| Own all backend mutations | Use raw `fetch()` | +| Perform optimistic updates | Set `isAuthenticated` during `login()` | +| Rollback on failure | Reassign reactive arrays | +| Normalize at boundary | Let components normalize | ## Services -Services must remain framework-agnostic. - -They should not: - -- import Vue -- import Pinia -- mutate UI directly +| ✅ Do | ❌ Don't | +|---|---| +| Stay framework-agnostic | Import Vue or Pinia | +| Own socket lifecycle | Mutate UI state directly | +| Emit parsed payloads | Own business logic | --- -# Debugging Rules +# Debugging Guide + +## Blank Dashboard After Login + +The #1 cause: **authenticated shell revealed before stores populated.** + +Check: + +1. Did `loadAppData()` actually run? +2. Is `isAuthenticated` set **after** data loads? +3. Did Login emit before unmounting? +4. Did `login()` avoid setting `isAuthenticated` internally? ## UI Not Updating -Check: - -1. component action call -2. store mutation -3. reactive dependency -4. watcher trigger - ---- +1. Reactive dependency — is the component actually tracking the right ref? +2. Store mutation — did the mutation preserve array identity? +3. Watcher trigger — is the watcher source a valid ref/getter? +4. Computed dependency — are all inputs reactive? ## Realtime Issues -Check: +1. WS connection status — is it connected? +2. Reconnect flow — did it refetch? +3. App.vue routing — is the event mapped to the right handler? +4. Active filters — is the event filtered out by `node_id + sensor_id`? -1. WS connection status -2. event routing in App.vue -3. store WS handler -4. active filters +## Optimistic Update Bugs ---- +1. Rollback path — does it restore the exact previous state? +2. Mutation symmetry — does undo match the original mutation? +3. Array identity — did you use `splice`/`Object.assign`, not reassignment? ## Sensor Selection Bugs -Always verify: +> Always verify composite key: `node_id + sensor_id` -```text -node_id + sensor_id -``` - -Selection/filtering logic must use composite keys. +Never filter by `sensor_id` alone — cross-node collisions will produce wrong results. --- -# Frontend Philosophy +# Architecture Philosophy HoneyWire prioritizes: -- deterministic state flow -- centralized business logic -- reusable primitives -- strict visual consistency -- zero duplicated network logic -- scalable component composition \ No newline at end of file +- **Deterministic rendering** — same state, same output +- **Stable reactive identity** — no array reassignment, no broken watchers +- **Centralized state ownership** — stores are the truth +- **Optimistic responsiveness** — instant UI, background sync +- **Rollback safety** — every mutation has an undo +- **Strict layering** — no upward dependency +- **Transport abstraction** — swap API client without touching stores +- **Predictable data flow** — one direction, one owner \ No newline at end of file diff --git a/Hub/internal/api/nodes.go b/Hub/internal/api/nodes.go index 48f023d..7bddb1a 100644 --- a/Hub/internal/api/nodes.go +++ b/Hub/internal/api/nodes.go @@ -39,11 +39,14 @@ func (h *Handler) CreateNode(w http.ResponseWriter, r *http.Request) { return } + h.broadcastWS("NEW_NODE", map[string]string{"node_id": nodeID}) + SendJSON(w, http.StatusCreated, map[string]interface{}{ "node_id": nodeID, "apiKey": apiKey, "alias": req.Alias, }) + } // UpdateNode handles UI requests to edit a Node's metadata diff --git a/Hub/ui/src/App.vue b/Hub/ui/src/App.vue index 26deb47..8b36cb3 100644 --- a/Hub/ui/src/App.vue +++ b/Hub/ui/src/App.vue @@ -1,8 +1,7 @@ - -