Commit Graph

358 Commits

Author SHA1 Message Date
Joseph Doherty 3b7489a7b0 fix(localdb): a rebuilt peer's own writes were silently dropped
The other half of the rebuilt-peer story, and only reachable once 0.1.2 made
back-fill work: with the wiped node repopulated and writing again, its writes
never reached its peer.

last_applied_remote_seq is a watermark in the PEER's seq space, and a rebuilt
peer is a new database whose oplog numbers from 1. Two things then went wrong,
and both had to be fixed — the first alone leaves data stuck:

- The healthy node kept advertising the OLD peer's watermark, and the sending
  side seeds its pump from exactly that number, so the rebuilt node skipped its
  entire oplog. The watermark (and the observed peer clock) is now reset when
  the peer's node id changes. last_acked_seq is deliberately NOT reset: it
  describes our own oplog's pruned horizon and is what tells a rebuilt peer it
  needs a snapshot — clearing it would turn 0.1.2's back-fill back off.

- A peer claiming to have applied more of our stream than we have ever produced
  can only be remembering a previous incarnation of us. That claim is now
  rejected in favour of starting from the beginning; LWW makes the re-send
  harmless. This mirrors the clamp RecordPeerAckAsync already applies to acks.

Found on the OtOpcUa docker-dev rig, where the healthy node held watermark 10
while the rebuilt peer's oplog was seqs 1-3 and its last_acked stayed 0 — the
peer never accepted a single one. Covered by a test that converges with one
peer, replaces it with a fresh database, and requires a write on the new peer
to arrive: RED before this fix, green after. 149/149 pass.

Version 0.1.3.

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-21 02:06:03 -04:00
Joseph Doherty 45d08929e7 docs(index): LocalDb 0.1.2 + OtOpcUa Phase 1 adoption in the umbrella index
The lib row still claimed ScadaBridge was the only consumer (verified
2026-07-20); OtOpcUa's Phase 1 adoption merged 2026-07-21. Records 0.1.2 and
the wiped-peer snapshot fix it carries.

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-21 01:24:09 -04:00
Joseph Doherty cad3bcb6bf fix(localdb): snapshot a wiped peer whose deltas were already acked and pruned
A converged pair prunes its oplog on ack, so "no oplog rows at all" is the
steady state of a healthy pair — not an anomaly. ComputeSnapshotRequiredAsync
measured the peer's gap against the oldest surviving oplog row and treated an
empty oplog as "no gap possible", which made that steady state the one state
from which a wiped peer could never be healed: it reconnected with an empty
database and a zero watermark, was told it needed no snapshot, and stayed
empty indefinitely.

With an empty oplog the oldest seq we could still stream is one past
last_acked_seq — ack-pruning is the only thing that empties the oplog without
also flagging needs_snapshot, and it deletes exactly seq <= last_acked_seq.
Measuring the gap against that heals the wiped peer and leaves every other
case identical: a converged peer's watermark equals last_acked_seq (no
snapshot), and a pair that has never written anything has both at zero.

Found by the OtOpcUa LocalDb Phase 1 live gate (check 4), which recorded it as
a documented limitation. Covered by a test that reproduces the live-gate
scenario through the real session stack: RED before this fix (the wiped side
never converges, 15 s timeout), green after. 148/148 pass.

Version 0.1.2.

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-21 01:17:32 -04:00
Joseph Doherty c42348eff5 docs(localdb): the ScadaBridge adoption is merged, not pending
Phases 1 and 2 landed on ScadaBridge main together as PR #23
(merge 28ca04d7) on 2026-07-20 and were pushed to origin. The
component row still described both as unmerged branches.

Verified before editing: main..feat/localdb-phase1 and
main..feat/localdb-phase2 are both empty, main == origin/main,
and SiteReplicationActor/ReplicationService have no hits left in
main's tree.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-20 08:41:21 -04:00
Joseph Doherty 5836c79d3d docs(localdb): phase 2 complete + 0.1.1 in the component row
The row still said "Adopted by ScadaBridge (Phase 1 only)" and "Phase 2 is NOT
started". Both phases are now complete with a 10-check live gate PASS.

Also records what the row could not have said before: the 0.1.1 parent-
directory boot fix, that ScadaBridge is the library's ONLY consumer (verified —
OtOpcUa, mxgw and HistorianGateway have no LocalDb reference at all), the
deliberate never-registered status of notification_lists/smtp_configurations
and why, and the three operational constraints an operator has to know
(stop/start a pair together, the TombstoneRetention resurrection bound, and
MaxBatchSize batching by row count against a 4 MB gRPC cap).

Replication remains default-OFF and no production deployment has it enabled.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-20 05:38:02 -04:00
Joseph Doherty 10d076a9da test(localdb): sustained concurrent write load through pooled connections
Closes criterion 5 of ScadaBridge/docs/known-issues/2026-07-20-localdb-disk-io-
error-under-load.md §9: Phase 1 shipped only correctness and convergence tests,
so nothing exercised many pooled per-operation connections writing a REGISTERED
table (capture trigger + zb_hlc_next UDF) concurrently — which is exactly the
shape the ScadaBridge site nodes run under load.

8 writers x 250 inserts, one fresh pooled connection per operation, with
concurrent readers churning the pool and the WAL index throughout. Asserts both
that every write lands and that the oplog captured each exactly once, so a
trigger that dropped or double-counted under contention fails it.

Written during the 2026-07-20 disk-I/O incident investigation and left
uncommitted; the incident itself was root-caused to host-side sqlite3 access on
a bind-mounted WAL database, not a library defect.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-20 05:01:57 -04:00
Joseph Doherty 12e6f0d2c5 fix(localdb): create the database file's parent directory (0.1.1)
SQLite creates the database FILE on demand but never its parent DIRECTORY, and
SqliteLocalDb opens the file eagerly in its constructor — so a configured path
under a not-yet-existing directory was a hard startup failure ("SQLite Error
14: unable to open database file") rather than a first run that just worked.
Relative defaults like ./data/app.db hit it routinely; a containerized consumer
escapes only when a volume mount happens to create the directory first.

ScadaBridge found this during LocalDb Phase 2 and worked around it app-side
(SiteLocalDbDirectory.Ensure). Every other consumer still had the gap, so the
guarantee belongs here.

The standing objection to a library that creates directories is that it can
mask a mis-typed path by silently starting an empty database. That does not
apply: SQLite ALREADY does exactly that for a mis-typed file NAME. Refusing to
create the directory protects nothing — it only makes the two halves of one
path behave inconsistently and turns the directory half into an opaque error.

A creation failure (permissions, read-only mount) now throws an
InvalidOperationException naming the directory and the cause, instead of
surfacing as SQLite error 14. That is a change of exception type, but only on a
path that already failed hard.

Both behaviours are pinned and verified to fail with the call removed.
147 tests pass, 0 warnings. Published to the Gitea feed at 0.1.1.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-20 04:51:18 -04:00
Joseph Doherty baea6cc2e0 docs(secrets): interactive-console quickstart + discoverability sweep
- new task-first quickstart (deployment seeding + KEK-lockout walkthroughs)
- runbook: document the sixth ReferenceStatus (PresentUnverified) + quickstart link
- README + shared-contract + umbrella CLAUDE.md secrets row updated for 0.3.0 console
- CLI usage text now mentions the interactive console (was undiscoverable from --help)
2026-07-19 14:55:53 -04:00
Joseph Doherty 0139c90f89 docs(localdb): correct the adoption-design doc location in the LocalDb row
The design doc lives here in scadaproj, not in ScadaBridge; ScadaBridge holds
the Phase 1 plan and the Phase 2 gate.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-19 11:27:53 -04:00
Joseph Doherty 87ffe1b774 Merge branch 'worktree-secrets-interactive-cli' 2026-07-19 11:27:50 -04:00
Joseph Doherty 980b4a37f6 fix(secrets-cli): contain file-source key IO/access errors in doctor + bundle-import flows 2026-07-19 10:38:16 -04:00
Joseph Doherty 6dbb372adf docs(secrets): interactive console runbook + 0.3.0 version bump 2026-07-19 10:37:37 -04:00
Joseph Doherty 1f7d26d4d9 feat(secrets-cli): launch interactive console on bare secret in a TTY 2026-07-19 10:30:30 -04:00
Joseph Doherty 7e67bd61c7 fix(secrets-cli): doctor flow — contained old-key errors, corrupt-row + rewrap-failure coverage 2026-07-19 10:27:54 -04:00
Joseph Doherty 1b65e820fb test(secrets): serialize env-var-mutating test classes into one xunit collection
Environment.SetEnvironmentVariable/GetEnvironmentVariable mutate the
process-wide environ; concurrent setenv/getenv across parallel xunit test
classes is a documented crash/flake source on glibc even with unique
variable names. Group the six classes that touch process env vars into a
DisableParallelization=true collection so they run serially against each
other while everything else keeps running in parallel.
2026-07-19 10:27:09 -04:00
Joseph Doherty 04018aab6e fix(secrets-cli): bundle import — source-key verification, format gate up front, access-denied containment 2026-07-19 10:00:59 -04:00
Joseph Doherty 4549b93d2a fix(secrets-cli): doctor flow — catch scoped exactly to the rewrap call 2026-07-19 10:00:00 -04:00
Joseph Doherty e8b34be255 fix(secrets-cli): audit flow — cipher guard, shared actor helper, summary + branch coverage 2026-07-19 09:55:16 -04:00
Joseph Doherty d1ec0fe5da feat(secrets-cli): lockout-recovery flow — KEK diagnosis, guided rewrap, lost-KEK reset 2026-07-19 09:53:10 -04:00
Joseph Doherty 000d283c99 feat(secrets-cli): bundle export/import flows 2026-07-19 09:52:31 -04:00
Joseph Doherty 08c4ce5769 docs(localdb): ScadaBridge Phase 1 adopted - LocalDb component row truth pass
Was 'no app adoption yet'. Now records the honest state: ScadaBridge site nodes
adopted Phase 1 on an UNMERGED branch, replication wired but default-OFF and
live-proven on the docker rig's site-a pair only, Phase 2 not started, no other
app adopted, no production deployment replicating.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-19 09:51:55 -04:00
Joseph Doherty e5a4d0276a fix(secrets-cli): CRUD flows — escaped validation markup, shared prompt helpers, delete existence check 2026-07-19 09:47:10 -04:00
Joseph Doherty 6ad710bdbd test(secrets-cli): structural no-plaintext assert (base64 '+' JSON-escaping flake) + override doc note 2026-07-19 09:46:26 -04:00
Joseph Doherty 779f8a8d78 feat(secrets-cli): deployment seeding flow — audit ${secret:} refs and fill the gaps 2026-07-19 09:43:17 -04:00
Joseph Doherty fe3ea684f5 feat(secrets-cli): CRUD flows for the interactive console 2026-07-19 09:37:46 -04:00
Joseph Doherty 138e2c1006 fix(secrets-cli): bundle import — verbatim ApplyReplicated writes, format gate, entry-aware errors 2026-07-19 09:35:39 -04:00
Joseph Doherty 4576f73c4c fix(secrets-cli): KEK doctor — fresh-row verdicts, degraded rewrap guard test, vanished-row visibility 2026-07-19 09:33:53 -04:00
Joseph Doherty e60f2d5dad fix(secrets-cli): reference auditor — InvalidName containment, comment-key parity, name normalization 2026-07-19 09:27:13 -04:00
Joseph Doherty 21f6a0df92 fix(secrets-cli): shell — contain UnauthorizedAccess, flow-title guard, cancel-path test 2026-07-19 09:26:04 -04:00
Joseph Doherty e49496c856 feat(secrets-cli): interactive shell skeleton — target picker, degraded-KEK upgrade, flow dispatch 2026-07-19 09:16:06 -04:00
Joseph Doherty 6255409f16 feat(secrets-cli): ciphertext-only bundle export/import with LWW + cross-KEK rewrap 2026-07-19 09:14:43 -04:00
Joseph Doherty f0f2b23d03 feat(secrets-cli): KEK doctor — per-row diagnosis + guided rewrap remedy 2026-07-19 09:11:35 -04:00
Joseph Doherty 38d33cf4b7 feat(secrets-cli): reference auditor — classify every ${secret:} the target app needs 2026-07-19 09:11:21 -04:00
Joseph Doherty 9bc1e5852e fix(secrets-cli): degrade on malformed master-key source + topology note 2026-07-19 09:08:24 -04:00
Joseph Doherty 83af2b2eaf feat(secrets-cli): per-target session factory with degraded-KEK mode + literal KEK provider 2026-07-19 09:01:46 -04:00
Joseph Doherty b8c6c7d432 test(secrets-cli): clean up temp dirs in TargetConfigReaderTests 2026-07-19 08:54:54 -04:00
Joseph Doherty b9a57fe06e feat(secrets-cli): target config reader — operate on the app's own composed Secrets config 2026-07-19 08:50:52 -04:00
Joseph Doherty 1b013a56a7 feat(secrets-cli): recent-targets store for the interactive console 2026-07-19 08:50:03 -04:00
Joseph Doherty fe9d8865fe feat(secrets-cli): wire Spectre.Console + SqlServer store refs for interactive console 2026-07-19 08:48:22 -04:00
Joseph Doherty 7072dc25a2 docs(secrets): interactive console implementation plan (14 tasks) 2026-07-19 08:46:15 -04:00
Joseph Doherty a53671e0af docs(secrets): interactive CLI console design (approved) 2026-07-19 08:41:41 -04:00
Joseph Doherty fb5c41bd73 docs(localdb): ScadaBridge LocalDb adoption design (approved)
Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-19 01:52:48 -04:00
Joseph Doherty cc7d0dbf18 docs(secrets): 0.2.3 rollout complete - all 4 apps bumped, modal gates PASS
Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-19 01:28:15 -04:00
Joseph Doherty 7e7fbe9bf0 docs(secrets): scadaproj#2 fixed - Secrets.Ui 0.2.3 published, SB verified
Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-19 01:11:40 -04:00
Joseph Doherty 62ab8f68d1 fix(secrets): ship the delete modal's own styles under collision-proof class names (0.2.3)
Fixes scadaproj#2. ConfirmDeleteModal used the bare `modal` class and the
Secrets.Ui RCL shipped no CSS at all, silently depending on the host having
no opinion about that selector. Bootstrap 5 does: `.modal { display: none }`
made the delete modal permanently invisible on every Bootstrap host
(ScadaBridge, OtOpcUa, HistorianGateway — confirmed live on the first two)
while every @onclick handler kept working. Only MxGateway, the lone
Bootstrap-free host, could ever have rendered it.

Fix, entirely inside the RCL so a package bump repairs every host with no
host-side changes:
- class vocabulary renamed to zb-secrets-modal / -backdrop / -card / -title
  so no host framework selector can match the elements;
- the component emits its own <style> block alongside the markup. Scoped
  .razor.css was deliberately NOT used: three of the four family hosts never
  link a scoped-CSS bundle, so isolation CSS would silently fail to load —
  the exact defect class being fixed. Theme tokens with fallbacks keep the
  card legible even on a host without the kit stylesheet.
- data-testids unchanged, so existing Playwright gates and bUnit tests keep
  their selectors.

New bUnit pins: no Bootstrap-reserved class name appears in the rendered
markup, and the component ships a style block that both positions the
overlay and gives it a display mode. Suite 182 pass / 0 fail / 0.2.3.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-19 01:08:08 -04:00
Joseph Doherty 0b23da429e docs(secrets): ScadaBridge#22 fixed + new Secrets.Ui modal-CSS defect (scadaproj#2)
Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-19 00:59:28 -04:00
Joseph Doherty 03ff5cd8bc docs(secrets): lmxopcua#483 fixed - /admin/secrets interactive via host wrapper page
Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-19 00:23:01 -04:00
Joseph Doherty fbb788d463 docs(secrets): clustered replication ENABLED + proven on the OtOpcUa docker-dev rig; #482 closed
Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-18 16:01:15 -04:00
Joseph Doherty 8d9bcedaf3 docs(secrets): OtOpcUa on 0.2.2 - Akka gate re-run vs feed packages 6/6
Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-18 15:04:17 -04:00
Joseph Doherty b077ca1b7a docs(secrets): 0.2.2 published to the Gitea feed (5 packages, restore-verified)
Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-18 14:55:19 -04:00