Commit Graph

8 Commits

Author SHA1 Message Date
Joseph Doherty a222421374 docs: mesh gRPC ingress is AllowAnonymous; the PSK interceptors are the gate (R3-01)
New docs/security.md section "Mesh gRPC ingress is AllowAnonymous by design"
(archreview 03/S14): what the cookie FallbackPolicy covers, why it can block a
correctly-PSK'd call before the interceptor's verdict matters, why the
AllowAnonymous opt-out is safe (all five interceptors fail-closed), the
affected-endpoint table (fused-vs-driver-only distinction), and the two
standing guards (MeshGrpcAnonymousMetadataTests, MeshGrpcMapConventionTests)
plus the auth-stack/driver-only boundary tests.

One cross-reference line each in docs/Configuration.md (§MeshTransport,
§ConfigSource/ConfigServe) and docs/Telemetry.md, so an operator reading
about the listener ports meets the constraint where they'd look for it.
2026-08-20 10:27:40 -04:00
Joseph Doherty 697a773340 fix(adminui): link the dead scoped-CSS bundle; add btn/notice-alert/tree-twist rules
Foundation task of the AdminUI UI sweep. CSS/link/doc only — no behavior change
beyond restoring styles that were built but never served.

- App.razor: link the RCL's scoped-CSS bundle
  (_content/ZB.MOM.WW.OtOpcUa.AdminUI/ZB.MOM.WW.OtOpcUa.AdminUI.bundle.scp.css).
  It is produced on every compile and served by the Host, but nothing referenced
  it, so ContextMenu.razor.css was dead at runtime and context menus rendered
  in-flow with no position:fixed.
- site.css: three additions.
  * .btn / .btn-sm sizing as Bootstrap CSS-VARIABLE overrides (never box
    properties — .btn-group depends on Bootstrap's own .btn rules for its seams
    and end-cap radii). Theme 0.3.1 ships no .btn rule, so Bootstrap's 1rem
    applied against the theme's 0.9rem body. Upstreaming candidate, same as the
    ignitionoee router's app.css.
  * .notice-alert — red variant of the kit's amber .notice, on the --bad/
    --bad-bg/--bad-border tokens. The 13 existing call sites hack this with
    style="border-color:var(--alert)", and no --alert token exists anywhere, so
    the tint silently no-ops today. Call-site migration is a separate task.
  * .tree-twist — shared 18px expander column for the tree components, replacing
    repeated inline style="width:18px". Rows are d-flex, so flex-basis is what
    actually holds the column. Adoption is a separate task.
- ContextMenu.razor.css: correct the header comment — the kit is light-only
  (no dark block in Theme 0.3.1, site.css, or this file). No rule changes.
- docs/Telemetry.md: record that driver-health is keyed per driver instance
  across the cluster, not per Akka member (the wire record carries no per-member
  identity). The fact previously lived only in Hosts.razor page copy.
- docs/Configuration.md: new audit-log subsection recording that EventId gives
  cross-restart dedup, enforced by the filtered unique index
  UX_ConfigAuditLog_EventId, so a retried apply inserts no duplicate row;
  CorrelationId joins a row to its originating workflow. Previously only in the
  historical v2 rebuild plan and ClusterAudit page copy.
2026-08-10 16:31:42 -04:00
Joseph Doherty f1c188114b refactor(fleet): #542 delete the dead fleet-status chain (broadcaster, SignalR bridge, hub, messages)
#542 filed FleetStatusBroadcaster as blind to site nodes since the Phase 6 per-cluster mesh
split: it derives "the fleet" from central's own Akka gossip ring, which after the split is
just central's own pair. Scoping the cross-mesh rewrite found the deeper truth — the whole
chain is dead end to end, so this deletes it rather than reviving it.

  - Zero consumers. FleetStatusBroadcaster published FleetStatusChanged on the DPS
    "fleet-status" topic; FleetStatusSignalRBridge forwarded it to FleetStatusHub
    ("/hubs/fleet-status"); nothing anywhere connected. The AdminUI bans self-connecting
    HubConnections, no browser JS subscribes, and /fleet (Fleet.razor) reads
    NodeDeploymentState from the ConfigDb plus IClusterRoleInfo on a 5 s poll instead.
  - Its second input never existed. DriverHostStatusHeartbeat had no production sender —
    only the broadcaster's own test told it — so CurrentRevision was always null and the
    30 s staleness -> Degraded flip could never fire.
  - Post-Phase-6 DPS is pair-local, so even the central-only view it did produce was an
    accident of topology rather than a design.

The fleet visibility that actually works is unchanged and already cross-mesh-correct:
/fleet over NodeDeploymentState, and the four Phase 5 per-node telemetry dials.

Precedent for deleting inert machinery rather than activating it: #522 (driver stability
tiers), #507 (discovered-node injection), #525 (GetMemoryFootprint).

Deleted: FleetStatusBroadcaster (+ its nested DriverHostStatusHeartbeat), its admin
singleton registration and FleetStatusBroadcasterKey, FleetStatusSignalRBridge and its
key, FleetStatusHub and its route mapping, and the FleetStatusChanged / FleetNodeStatus /
FleetNodeHealth messages. Both Fleet/ directories survive — ControlPlane keeps
ClusterNodeAddressReconciler, Commons keeps GetDiagnostics. Docs swept where they claimed
a fleet-status channel exists (Telemetry.md's Phase 5 non-migrated channels,
Configuration.md, CLAUDE.md); completed-program plan docs under docs/plans and docs/v2 are
left as the historical record they are.

Build 0 errors, 868 warnings — byte-identical to master's rebuild baseline.
ControlPlane.Tests 231/231, AdminUI.Tests 1109/1109.

Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1
2026-08-06 17:21:30 -04:00
Joseph Doherty 8e25ed55f6 docs: fix two stale mesh-program claims found in review (Phase 5 gate passed; Phase 6 ended fleet-wide gossip)
Review of the ClusterClient-deletion docs sweep (406b1ae5) flagged two
pre-existing staleness items in the swept files, unrelated to ClusterClient
but caught while touching the same passages:

- docs/Telemetry.md: the Phase 5 telemetry section's Status line still said
  "code-complete ... live gate has not yet run" — the gate PASSED 2026-07-23
  (merge 35552a96), already recorded in CLAUDE.md. Updated to match.
- docs/v2/config-db-schema.md: the AkkaPort/GrpcPort note asserted in the
  present tense that "central shares a gossip ring with every node," which
  Phase 6 (2026-07-24) ended — central now runs one 2-node Akka mesh per
  application Cluster and reaches a site pair only over gRPC. Reworded the
  premise to past tense so it no longer contradicts the ClusterClient-deleted
  parenthetical sitting right after it.

docs-only; dotnet build ZB.MOM.WW.OtOpcUa.slnx stays green (0 errors).

Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1
2026-08-06 16:30:47 -04:00
Joseph Doherty 406b1ae5ac docs(mesh): sweep for ClusterClient deletion — Dps|Grpc only, CentralContactPoints retired
Task 3.2 of the ClusterClient->gRPC migration. Sweeps the docs that still
described the Akka ClusterClient transport as live, following its Phase 3
deletion (this branch): MeshTransport:Mode is now Dps (pair-local DPS) or
Grpc (cross-mesh unary calls) only, ClusterClient fails validation loudly,
CentralContactPoints is deleted (not superseded), and the split-topology
validator requires exactly Grpc rather than ClusterClient.

- docs/Configuration.md: rewrite the MeshTransport section — table,
  dark-switch/PSK-seeding prose, listener sharing (:4056 driver-role,
  :4055 admin-role), remove the "shipped dark, cutover pending" framing.
- docs/Redundancy.md: replace the Dps/ClusterClient transport table with
  Dps/Grpc, fix the Phase 6 CentralCommunicationActor + validator bullets.
- docs/Telemetry.md: fix stale ClusterClient cross-refs, add a note that
  the driver-role telemetry listener also hosts NodeCommandService.
- docs/v2/config-db-schema.md: one-line "(retired: ...)" annotation on the
  historical AkkaPort/GrpcPort note — left as a historical record otherwise.
- CLAUDE.md: rewrite "## Mesh command transport (MeshTransport)" for the
  post-deletion world (no ClusterClient/receptionist, per-node gRPC dial,
  fire-and-forget semantics unchanged) and fix two Phase 6 bullets.

deferment.md has no mesh-transport mentions; left untouched.

docs-only change; `dotnet build ZB.MOM.WW.OtOpcUa.slnx` stays green (0
errors) after the edit.

Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1
2026-08-06 16:23:23 -04:00
Joseph Doherty 30d0697c28 feat(hosts): surface per-host connectivity on /hosts; drop the unwritable table (#521)
`IHostConnectivityProbe` was a dead surface: eleven drivers implement it, `GetHostStatuses()`
had ZERO production call sites, and `OnHostStatusChanged` had no subscriber outside the Galaxy
driver's own aggregator. Per-host connectivity was computed by every driver and read by nobody.

The issue offered "build the publisher or delete it". The publisher as its entity doc described
it — driver nodes upserting `DriverHostStatus` rows — is not buildable: per-cluster mesh Phase 4
gates `AddOtOpcUaConfigDb` on the `admin` role, so a driver-only node has no ConfigDb connection
to write rows with. So the capability is kept and the transport changed.

`DriverHealthChanged.HostStatuses` now carries the probe result to `/hosts` as a Hosts column.
That channel already reached the page, already survives the mesh split via the Phase 5 gRPC
telemetry stream, and already replays a last-value snapshot on re-subscribe — so per-host state
re-primes after a reconnect without a durable store. Both halves of the interface finally do
what they are for: the event triggers a prompt publish, the pull is the source of truth.

The point of the column is the case the driver-level state chip structurally cannot express: a
multi-device driver stays aggregate-Healthy while ONE of its devices is unreachable.

Two traps, both pinned by tests that were falsified against the prod code:

- The host digest MUST be in the publish fingerprint. On a single-host-down transition every
  other fingerprint component is unchanged, so the dedup would swallow exactly the publish
  carrying the news — the trap that already bit the rediscovery signal. Removing it turns the
  guard test red, verified.
- null (no probe) must stay distinct from empty (probe with no hosts). proto3 cannot tell an
  absent repeated field from an empty one, hence the explicit `has_host_statuses` flag;
  collapsing them would render every probe-less driver as one whose devices are all fine.

Dropped: the DriverHostStatus entity, enum, DbSet, model config and table (migration
DropDriverHostStatusTable — empty on every deployment, so the scaffolder's data-loss warning is
moot, and Down() recreates it exactly).

Found en route, NOT fixed here: `DriverInstanceResilienceStatus` is the identical defect — no
writer, no reader, only a DbSet declaration, while the live data rides the
`driver-resilience-status` telemetry channel. Its doc-comment now states that rather than
describing the sampler and AdminUI join that were never built. Filed as #524 rather than widening
this schema change beyond what was asked.

Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
2026-07-30 04:21:08 -04:00
Joseph Doherty 8b7ea3213d fix(rig,docs): backfill ClusterNode.GrpcPort and document the upgrade step (#493)
`GrpcPort` is a nullable column added by Phase 5. Every insert in the docker-dev
seed is INSERT-if-not-exists — the right shape for a re-runnable seed, but it means
a new nullable column never reaches rows an earlier version of the file created. On
a persisted volume all six rows kept NULL, central found no dial targets, and
`TelemetryDial:Mode=Grpc` connected to nothing. (`AkkaPort` escaped this only
because it is NOT NULL with a default of 4053.)

Seed now backfills `GrpcPort IS NULL AND CreatedBy = 'docker-dev-seed'` to 4056.
NULL only — it must not overwrite a port an operator set deliberately on a
long-lived dev volume, and NULL is the unambiguous "predates the column" marker.

Deliberately NOT doing the suggested EF data migration for real deployments. The
correct value is each node's own `Telemetry:GrpcListenPort`, which is
per-deployment configuration the database cannot derive; a migration could only
invent one, and a WRONG dial target is worse than the NULL the dialer already skips
explicitly with a throttled warning. The rig can be backfilled because there the
port is fixed by docker-compose.yml and therefore actually knowable.

`docs/Telemetry.md` gains an upgrade section stating the prerequisite, the symptoms
(the throttled skip log, LISTEN with no ESTABLISHED, the pill never going live) and
the UPDATE to run before flipping any node to Grpc — plus why there is no migration.
2026-07-26 10:29:51 -04:00
Joseph Doherty e99ea40e2f docs(mesh-phase5): telemetry stream — Telemetry.md, config keys, supersede §6.3, program status
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-23 16:33:29 -04:00