content(guide): internal platform architecture guides (DRAFT, Refs #1)
Internal (audience:internal) guides capturing the HiveIQ architecture that caused repeated mistakes: data-architecture (executor/Kafka/record-store), service-ownership, deployment topology, auth/JWT, git workflow, module map. For admins + Claude's reference.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
---
|
||||
module: platform.auth
|
||||
title: Auth & JWT Flow
|
||||
tab: Overview
|
||||
order: 40
|
||||
audience: internal
|
||||
---
|
||||
|
||||
> **DRAFT · internal** (2026-07-01). Verify claim names / secret handling against
|
||||
> `hiveops-security-common` before relying on specifics.
|
||||
|
||||
## Who issues what
|
||||
- **hiveops-auth** authenticates customers (TOTP MFA) and issues the **JWT**. Login is
|
||||
`POST /auth/api/login` with body field **`email`** (not `username`), response field **`token`**.
|
||||
- **hiveops-mgmt** handles internal/admin; shares the **same `JWT_SECRET`** as auth.
|
||||
- The `JWT_SECRET` must be **identical** across services that validate tokens.
|
||||
|
||||
## How services validate (shared lib)
|
||||
`hiveops-security-common` `JwtTokenValidator`: key = `Keys.hmacShaKeyFor(jwtSecret.getBytes())`
|
||||
(the secret is used as **raw bytes — do NOT base64-decode it**). Claims read:
|
||||
`sub` (numeric user id), **`email`**, **`role`** (single string), `institutionKey(s)`, `type`.
|
||||
Each service's `JwtAuthenticationFilter` sets a single authority `ROLE_<role>`.
|
||||
|
||||
## Roles
|
||||
Admin roles are **`MSP_ADMIN`, `BCOS_ADMIN`** (and `QDS_ADMIN`). Most `@PreAuthorize` uses
|
||||
`hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`. Audience gating (e.g. the guide's internal tabs) keys off
|
||||
these admin roles.
|
||||
|
||||
## The HiveIQ Browser injects auth
|
||||
The desktop **HiveIQ Browser** (Electron) injects `Authorization: Bearer <jwt>` **and**
|
||||
`X-HiveIQ-Browser: true` on every module request (`onBeforeSendHeaders`). So the SPAs often carry
|
||||
**no token interceptor themselves** — the browser adds it. `X-HiveIQ-Browser: true` is a reliable
|
||||
server-side signal for "this is the desktop app" (used for the Browser-required redirect).
|
||||
|
||||
## Minting a token for tests (no real account)
|
||||
`pyjwt` with the env `JWT_SECRET`, HS256, claims `{sub:'1', email, role, iat, exp}`. Secret must be
|
||||
≥32 bytes for HS256. Route calls through nginx (`api.bcos.*/<svc>/...`).
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
module: platform.data-architecture
|
||||
title: Data Architecture — Executors, Kafka, Record Stores
|
||||
tab: Overview
|
||||
order: 10
|
||||
audience: internal
|
||||
---
|
||||
|
||||
> **DRAFT · internal.** Written 2026-07-01 to stop the most expensive recurring mistake:
|
||||
> assuming "where data lives." Verify against code before relying on any specific path.
|
||||
|
||||
## The core pattern
|
||||
|
||||
HiveIQ services are **single-responsibility and decoupled via Kafka producer/consumer.** The rule
|
||||
that matters:
|
||||
|
||||
1. An **executor service does one job and stays clean** — it does *not* keep long-term history.
|
||||
2. On **completion or failure**, the executor **produces a Kafka event** with the result.
|
||||
3. The **owning / record-keeping service consumes** the event and **stores the record**.
|
||||
4. ⇒ **Query history from the record store; send commands to the executor.**
|
||||
|
||||
## Worked example — fleet tasks (the 2026-06-30 bug)
|
||||
|
||||
- **Fleet = executor.** It runs download/install/etc. It keeps **no queryable history** — the
|
||||
standalone `/fleet/tasks` and `/fleet/tasks/paginated` endpoints are **empty by design** once a
|
||||
job finishes. An empty executor endpoint is **NOT** a bug or a stale instance.
|
||||
- On done/fail, fleet **publishes to Kafka**.
|
||||
- **Devices consumes and stores** the device-scoped task record, served at
|
||||
`GET /api/atms/{id}/fleet-tasks` (`DeviceFleetTask`). **This is the source of truth for a
|
||||
device's task history.** (Task History and the SW Deploy tab must read *this*, not the fleet
|
||||
executor.)
|
||||
- The **install action is the exception** — a *command*, sent directly to the fleet executor
|
||||
(`fleetAPI.createTask`).
|
||||
|
||||
## Kafka vs direct API — judgment, not dogma
|
||||
|
||||
- **Direct API** when you need **live/real-time state** (in-flight progress) or a **synchronous
|
||||
command** (create a task), or immediate consistency.
|
||||
- **Kafka** for **durable history, cross-service mirrors, eventual propagation.**
|
||||
- One feature can legitimately read history from the record store *and* hit a direct API for live
|
||||
progress. Match the mechanism to the need.
|
||||
|
||||
## Do NOT
|
||||
- Do not say a service's data "lives in" whichever service happens to *serve* it. Ownership
|
||||
(execution) and record-keeping (storage) can be different services.
|
||||
- Do not treat an empty executor endpoint as missing data.
|
||||
- When unsure where data lives: check the code / ask, don't assert.
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
module: platform.deployment
|
||||
title: Deployment Topology & bcos.dev
|
||||
tab: Overview
|
||||
order: 30
|
||||
audience: internal
|
||||
---
|
||||
|
||||
> **DRAFT · internal** (2026-07-01). Server paths verified today; re-check before prod actions.
|
||||
|
||||
## Environments
|
||||
- **bcos.dev** — all-in-one dev/staging on VM **`173.231.195.251`**. Registry `registry.bcos.dev`
|
||||
(user `hiveiq`, pw in memory). Domains `*.bcos.dev`.
|
||||
- **Production** — OpenMetal; services VM **`173.231.195.250`**, Postgres on a **separate** VM
|
||||
`10.10.10.188` (ProxyJump). Registry `registry.bcos.cloud`. Domains `*.bcos.cloud`.
|
||||
|
||||
## Where config lives vs. where deploys run (this bit me 2026-06-30)
|
||||
- **Source of truth for bcos.dev config = the `develop` branch of `hiveiq-openmetal-dev`**
|
||||
(`/source/hiveiq-ops/hiveiq-openmetal/hiveiq-openmetal-dev/hiveops/instances/dev/`).
|
||||
- The `dev` folder still inside `hiveiq-openmetal-prod` is **legacy — do not use it.**
|
||||
- **Deploys RUN on `.251` from** `~/hiveiq/hiveops-openmetal/hiveops/instances/dev/` (a deployed
|
||||
clone named `hiveops-openmetal` — note `hiveops` on the box vs `hiveiq` in the source repo).
|
||||
That is the correct stack the running nginx + containers use.
|
||||
- Live VM edits must be **committed back to `hiveiq-openmetal-dev`** or they drift.
|
||||
|
||||
## Deploy a service to bcos.dev
|
||||
```bash
|
||||
cd /source/hiveops-src/hiveops-<svc>
|
||||
./build.sh --registry registry.bcos.dev --push # tag :dev
|
||||
ssh -i ~/.ssh/id_ed25519_hiveiq bcosadmin@173.231.195.251
|
||||
cd ~/hiveiq/hiveops-openmetal/hiveops/instances/dev
|
||||
docker compose pull hiveiq-<svc> && docker compose up -d hiveiq-<svc>
|
||||
```
|
||||
|
||||
## nginx routing gotchas
|
||||
- API routes live in `.../instances/dev/nginx/conf.d/api.conf` (bind-mounted into `hiveiq-nginx`).
|
||||
- Backends are set per-location: `set $backend "hiveiq-<svc>:<port>"` then `proxy_pass`. **Include
|
||||
the port** — a port-less upstream (`hiveiq-guide-backend` with no `:8099`) proxies to :80 → 502.
|
||||
(Exact bug, 2026-07-01.)
|
||||
- After recreating a container, nginx may hold a **stale upstream** → reload: `docker exec
|
||||
hiveiq-nginx nginx -t && docker exec hiveiq-nginx nginx -s reload`.
|
||||
- Per-domain confs must use blue/green **map vars**, not hardcoded hostnames, or they 502.
|
||||
|
||||
## Deploy = smoke test
|
||||
"Deployed" is only true after a curl of the changed endpoint against the live URL returns 200 +
|
||||
a parseable body. A green/healthy container is not a smoke test.
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
module: platform.git-workflow
|
||||
title: Git & Deploy Workflow
|
||||
tab: Overview
|
||||
order: 50
|
||||
audience: internal
|
||||
---
|
||||
|
||||
> **DRAFT · internal** (2026-07-01). Written after violating this repeatedly in one session.
|
||||
|
||||
## The one rule (service repos: `hiveops-*`)
|
||||
- **Commit DIRECTLY to `develop`. No feature branch for normal work. No PR into develop.**
|
||||
`git checkout develop` in the sub-project → pull → edit → commit with `#NN` → push develop.
|
||||
- **NEVER work on `main`.** (Working on stale main once caused a full duplication of work that
|
||||
already existed on develop.)
|
||||
- **Exception — ONLY when Johannes says "hotfix on main":** `hotfix/*` branch off main → PR to
|
||||
`main` → Johannes approves in the Gitea UI (SOC2) → he explicitly says "deploy". That is the
|
||||
only time branches, PRs, or main are involved.
|
||||
|
||||
## Enforced by a hook
|
||||
`.claude/hooks/block-commit-on-main.py` blocks `git commit` while HEAD is `main` — because a
|
||||
written rule alone did not reliably stop me. It **exempts deployment/config/docs repos** that live
|
||||
on main by design (openmetal / hiveiq-ops / hiveops-devops / hiveops-docs / hiveops-release /
|
||||
hiveiq-claude) and the monorepo root.
|
||||
|
||||
## Deployment/config repos
|
||||
`hiveiq-openmetal-dev` (bcos.dev config) now uses **`develop`** too (as of 2026-07-01). Other
|
||||
config repos historically live on `main` — confirm per repo.
|
||||
|
||||
## Gates that always stay manual
|
||||
- **PR approval** (develop→main) in the Gitea UI — SOC2 separation of duties; never self-approve.
|
||||
- **Prod deploy go** — separate from merge. Merge ≠ deploy. Deploy to the **standby** blue/green
|
||||
slot, verify, then switch nginx **and** internal callers together.
|
||||
- **Prod DB writes / SSH to prod hosts** — deliberate, prompted.
|
||||
|
||||
## Trackers
|
||||
`Refs #N` in commits/PRs — **never `Fixes #N`/`Closes #N`** (auto-close bypasses the Verify Fix →
|
||||
tester gate). Issue-first: every change traces to a Gitea issue.
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
module: platform.module-map
|
||||
title: Module Map (what each app does + its data source)
|
||||
tab: Overview
|
||||
order: 60
|
||||
audience: internal
|
||||
---
|
||||
|
||||
> **DRAFT · internal** (2026-07-01). Ports/routes from CLAUDE.md; expand + verify per module.
|
||||
> When a "tab is empty / data missing" report comes in, start from this map: know the intended
|
||||
> behaviour and the data source, then diagnose the **product**, don't investigate whether data
|
||||
> "happens to exist."
|
||||
|
||||
| App (URL) | Purpose | Backend / data source | Gotchas |
|
||||
|---|---|---|---|
|
||||
| **fleet** (fleet.bcos.*) | Deploy hotfix/software/agent to ATMs; task execution | fleet backend :8097 — **executor only**, publishes to Kafka | history is in the **devices** record store, not fleet; `/fleet/tasks` empty by design |
|
||||
| **devices** (devices.bcos.*) | ATM records, properties, config changes, per-device task history | devices backend (frontend calls legacy `/incident/...` route) | `/api/atms/{id}/fleet-tasks` is the device task record store; publishes `hiveops.device.events` |
|
||||
| **incident** | Incident create/resolve/auto-close; journal-event ingest | incident backend :8081 | 32-issue auto-close/resolve cluster = few root causes |
|
||||
| **transactions** | Transaction browser, customer journey | reads journal/incident data | — |
|
||||
| **recon** | Cash reconciliation | recon backend :8091; `device_summary` mirror | LACA NCR-8800 parser specifics |
|
||||
| **reports** | Report builder, scheduling, CSV | reports backend :8088; `device_summary` mirror | — |
|
||||
| **analytics** | Fleet dashboard analytics | analytics backend :8089 (stateless aggregation) | — |
|
||||
| **mgmt / msp** | Institutions, users, allocations, `app_modules` | mgmt backend :8080 (`hiveiq_mgmt`) | menu items can be `app_modules`-driven (DB), not hardcoded |
|
||||
| **journal** | Journal file storage + parsing | journal :8087; per-institution workers | `DB_URL` (full JDBC), not split host/port |
|
||||
| **guide** (this app) | In-app help; markdown → API → tabbed slide-out | guide backend :8099 — **markdown files**, no DB | route needs `hiveiq-guide-backend:8099` (port!) |
|
||||
| **agent-proxy** | Agent-facing gateway (auth, fingerprints, proxy) | :8093/8015 | owns agent paths natively |
|
||||
| **ai / adoons** | NL assistant (branded "Adoons" externally) | Spring AI; incident→ai Kafka | never say "AI" externally |
|
||||
| **vault / claims / aria / recon** | CIT visits / loss recovery / threat intel | own backends | see per-project memory |
|
||||
|
||||
## SW update flow (a worked "empty tab" case)
|
||||
SW updates reach an ATM via (1) fleet SOFTWARE/SOFTWARE_INSTALL task, (2) QDS, or (3) agent
|
||||
self-update. The device SW-deploy history reads the **devices record store**. "No info in the SW
|
||||
Update tab" after a real fleet-task update ⇒ the tab is reading the wrong source or the record
|
||||
isn't captured — a **product bug to fix**, not "no data".
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
module: platform.service-ownership
|
||||
title: Service Ownership & Where Data Lives
|
||||
tab: Overview
|
||||
order: 20
|
||||
audience: internal
|
||||
---
|
||||
|
||||
> **DRAFT · internal** (2026-07-01). Verify service/endpoint details against the code.
|
||||
|
||||
## One rule
|
||||
**Every endpoint and table belongs to exactly ONE service.** Never add an endpoint or write to
|
||||
another service's domain. If a view needs data from another domain, it consumes a **Kafka mirror**
|
||||
or calls that service's API — it does not reach into the other DB.
|
||||
|
||||
## Domains (owner → what it owns)
|
||||
- **incident** — incidents (create/resolve/auto-close), journal-events ingest path.
|
||||
- **devices** — everything device-related: ATM records, properties, config changes, and the
|
||||
**device-scoped fleet-task record store** (`/api/atms/{id}/fleet-tasks`). Backend extracted from
|
||||
incident (~2026-06-18); the frontend still calls `api.bcos.*/incident/...` (legacy routing that
|
||||
nginx points at the devices backend).
|
||||
- **fleet** — fleet task **execution** only (download/install/hotfix/software/capture). Publishes
|
||||
results to Kafka; keeps no long-term history (see [[platform.data-architecture]]).
|
||||
- **mgmt / msp** — institutions, internal users, allocations, `app_modules`.
|
||||
- **journal** — journal file storage + parsing (per-institution workers).
|
||||
- **auth** — customer auth + MFA; issues the shared JWT.
|
||||
- Others: **reports, analytics, recon, messaging, ai(adoons), agent-proxy, vault, claims, aria**.
|
||||
|
||||
## The `device_summary` mirror (why "wrong data in app X but not Y")
|
||||
Device state is **mirrored into a local `device_summary` table in incident / fleet / reports /
|
||||
recon** via the Kafka topic `hiveops.device.events`. Devices must **publish on every change** or
|
||||
the mirrors go stale (there is no reconciler). So *"the wrong institution shows in fleet but not
|
||||
devices"* = a **stale mirror**, not a bug in fleet. Fix at the source (devices publishes).
|
||||
|
||||
## Reflex to build
|
||||
When data looks wrong or missing in one app but not another: suspect a **stale/again-missing
|
||||
mirror or the wrong record store**, not the app you're looking at. Trace to the **owner**.
|
||||
Reference in New Issue
Block a user