Compare commits

..

47 Commits

Author SHA1 Message Date
44280dfe98 sync: auto-sync from AD2 at 2026-06-18 08:05:50
Author: Mike Swanson
Machine: AD2
Timestamp: 2026-06-18 08:05:50
2026-06-18 13:02:35 -07:00
05f7f1cde2 sync: auto-sync from AD2 at 2026-06-18 08:02:06
Author: Mike Swanson
Machine: AD2
Timestamp: 2026-06-18 08:02:06
2026-06-18 13:02:35 -07:00
78d694848a memory: DSCA33/DSCA45 spec gap (missing main specs, not a bug)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:34 -07:00
a35858058e sync: auto-sync from AD2 at 2026-06-18 07:52:33
Author: Mike Swanson
Machine: AD2
Timestamp: 2026-06-18 07:52:33
2026-06-18 13:02:34 -07:00
94330e0ff9 dataforth(datasheet): Fix 2 — per-model slot maps resolve ambiguous DSCA layouts
Some DSCA subtypes' raw_data STATUS groups carry more (or fewer) value-bearing
entries than the template's spec-bearing rows (the test program measures slots the
printed sheet omits, e.g. DSCA49's 5mA load pair), so the in-order zip misaligned
values and those models were skipped by the count-guard.

New tool derive-dsca-slotmaps.js derives a per-model slotMap (absolute statusEntries
index per spec-bearing row) by greedily matching a staged original's printed values
to the DB raw_data STATUS entries (same fround formatting), then picking the
candidate map that validates against the most units. Models are grouped by identical
row-name signature and one map is derived per group from all sibling units — this
disambiguates duplicate values (e.g. a unit where 5mA != 50mA linearity forces the
correct slot; DSCA49-04 alone has only 2 staged units that can't, but its siblings'
25 units do). Stored as `slotMap` in dsca-templates.json.

Renderer: consults slotMap only when the sequential zip fails (value count !=
spec-row count), so the 88 already-clean models keep their path (no regression) and
ambiguous ones pull the right value via the map.

STAGE 3 re-validation: FINAL-TEST CLEAN 88 -> 92; 134 more certs now render
(null 450 -> 316); matches 2278 -> 2412. Same 6 retest-vintage dirty models, no
new mismatches. DSCA49 family + DSCA40-03 group now clean and validated.

Still blocked (separate gap, NOT layout ambiguity): DSCA45-* and most DSCA33-*
render null because they have NO spec-reader entries (render-datasheet bails before
rendering). Their slotMaps are derived and ready; they need spec coverage. One DSCA33
group (DSCA33-02/03/03A/04/05/1948) did not reach the slotMap validation threshold
(best 19/35 units) and stays skipped pending more/cleaner staged samples.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:34 -07:00
7fb0d95af5 sync: auto-sync from AD2 at 2026-06-18 07:33:56
Author: Mike Swanson
Machine: AD2
Timestamp: 2026-06-18 07:33:56
2026-06-18 13:02:34 -07:00
bde5ad8706 dataforth(datasheet): Fix 2 — data-driven DSCA load note (fixes DSCA39 footer artifact)
Root cause of the DSCA39 footer mismatch: the "Standard output load for test is
250 ohms." line is a footer note, not a parameter, but the STAGE 1 extractor
captured it as a (column-truncated) row "Standard output load for te". And the
renderer's OUTSIGTYPE==='CURRENT' emission was wrong on both ends — it printed the
note (after the underline, invisible to the validator gate) for many -C current
models whose staged originals never had it, and never placed it correctly for the
models that do.

Fix is data-driven, matching the rest of the template approach:
- derive-dsca-templates.js: detect the "Standard output load..." line, capture it
  as a per-model `loadNote` property, and exclude it from rows. Regenerated
  dsca-templates.json — surgically clean: only the 5 DSCA39 models changed (lost
  the truncated row, gained loadNote); all 121 others byte-identical.
- datasheet-exact.js: emit `dscaTpl.loadNote` (blank line + note) before the footer
  underline, only for models that have it; removed the OUTSIGTYPE-based emission.

STAGE 3 re-validation: FINAL-TEST CLEAN 85 -> 88, mismatches 9 -> 6, matches
2206 -> 2278. DSCA39-01/02/07 now fully clean; DSCA39-01 byte-content-verified.
No regression — the -C current models stayed clean and no longer carry the
spurious after-underline note.

The 6 remaining dirty models (DSCA38-05/-1793/-19C/-19E, DSCA39-05, DSCA39-1950)
are ALL retest data-vintage: the staged .TXT is an older test run than the DB
latest-wins record (Supply Current / Linearity differ by more than rounding).
Not render bugs — cannot be reconciled against an older sheet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:34 -07:00
31cdfa4b02 sync: auto-sync from AD2 at 2026-06-18 07:23:32
Author: Mike Swanson
Machine: AD2
Timestamp: 2026-06-18 07:23:32
2026-06-18 13:02:34 -07:00
80065b13e4 dataforth(datasheet): Fix 2 — emulate QB single-precision rounding (26 -> 9 dirty)
formatMeasuredExact now applies Math.fround to the parsed value before toFixed.
The DOS QuickBASIC computed/stored these as single-precision floats, so the
last-digit rounding at the .5 boundary follows single, not double, precision.
Without it, double-precision toFixed flipped boundaries (9.9995 -> "9.999" vs
golden "10.000"; 46.85 -> "46.9" vs "46.8"; .45 -> "0.5" vs "0.4"; 3.3325 ->
"3.333" vs "3.332"). Verified each against the staged golden.

STAGE 3 re-validation: FINAL-TEST CLEAN models 68 -> 85 (+17), mismatches
26 -> 9, cert matches 2123 -> 2206. Zero regression — every remaining dirty
model was already dirty pre-fix; no previously-clean model flipped.

The 9 remaining are NOT rounding:
- 4 models (DSCA38-05/-1793/-19C/-19E): Supply Current retest data-vintage — the
  staged .TXT is an older test run than the DB latest-wins record; not a render
  bug, can't reconcile against an older sheet.
- 5 models (DSCA39-01/02/05/07/1950): STAGE 1 template artifact — the footer note
  "Standard output load for test is 250 ohms." was mis-captured as a truncated
  parameter row. A renderer-side fix was attempted but emitted the note for all
  current-output models (regressed 24 clean -C models), so it was reverted; needs
  a targeted STAGE 1 extractor fix instead.

Renamed dsca-clean68-models.json -> dsca-clean-models.json (now 85 models).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:34 -07:00
5cd4e5c6fb sync: auto-sync from AD2 at 2026-06-18 07:07:25
Author: Mike Swanson
Machine: AD2
Timestamp: 2026-06-18 07:07:25
2026-06-18 13:02:33 -07:00
be1186aab0 dataforth(datasheet): Fix 2 — publish the 68 STAGE-3-clean DSCA models to Hoffman
Restarted testdatadb service (new template live), canary-pushed 1 cert (updated,
0 errors), then re-pushed all PASS certs for the 68 Final-Test-content-clean
models via uploadBySerialNumbers from a fresh node process.

Result over 30,423 PASS certs: updated=26022 unchanged=2738 created=0 errors=0
skipped=1663. The 26,022 updates replace the old defective DSCA renders on the
live site with the rebuilt, byte-content-validated ones. The 1,663 skips are the
count-guard correctly refusing any individual cert whose value count != its
template row count (ambiguous) — never publishing misaligned data.

Artifacts: push-clean68.js (driver), dsca-clean68-models.json (the published
set). NOT yet published: the 26 last-digit-diff models, the ~32 ambiguous/null
layout families, and the 231 untemplated models.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:33 -07:00
344fbc31be dataforth(datasheet): Fix 2 STAGE 3 — DSCA render validator + first full report
validate-dsca-stage3.js: read-only harness that, for every staged DSCA original
we have ground truth for (2806 across 126 models), looks up the DB record,
renders it through the live path, and content-compares. GATE = the FINAL TEST
RESULTS section (rule lines canonicalized, whitespace collapsed — so the deferred
column-spacing cosmetic doesn't register); accuracy-section diffs reported
separately as informational.

First run verdict (report attached):
- 68 models FINAL-TEST CONTENT-CLEAN (0 mismatch over compared certs).
- 2123/2316 certs match the Final-Test content exactly (91.7%).
- 26 models show measured-value last-digit diffs only — structure (names, specs,
  row alignment, statuses) is correct. Two root causes, neither structural:
    * rounding-mode: JS double toFixed vs QB single-precision half-up
      (e.g. raw 9.9995 code3 -> "9.999" here, "10.000" in golden). Fixable but
      float-precision-sensitive; risks regressing currently-clean values.
    * data vintage: staged .TXT is a different test run than the DB latest-wins
      record (Fix 3) — e.g. Supply Current 19.6 vs 20.3, 0.7 apart. Not a render
      bug; can't be reconciled against an older staged sheet.
- ~32 models render null (count-guard): DSCA33-*, DSCA45-*, DSCA49-* families
  whose raw_data carries load points the template omits -> need per-subtype slot
  mapping (the canonical-slot approach) before they can render.

Still NOT published: service not restarted, nothing re-pushed to Hoffman.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:33 -07:00
6e42e39b2f dataforth(datasheet): Fix 2 STAGE 2 — wire DSCA per-model templates into render
Deployed file is C:\Shares\testdatadb\templates\datasheet-exact.js; this
reconciles the repo copy + adds dsca-templates.json (STAGE 1 output).

What changed in generateExactDatasheet (DSCA family only; 5B/8B/7B/DSCT/SCMVAS
paths byte-unchanged):
- Load dsca-templates.json once at module top (126 per-model layouts).
- DSCA Final-Test now renders names + specs from the staged template rows, not
  the single hardcoded DATA_LINES['DSCA'] + buildTSpecs DSCA branch.
- Value-bearing raw_data STATUS groups map positionally onto the spec-bearing
  template rows; empty-spec rows (240VAC Withstand / Hi-Pot) render blank+PASS.
  Removed the duplicate hardcoded 240VAC/Hi-Pot footer for DSCA (now rows).
- ACCURACY header uses the template accOut ("Output (V)"/"Output (mA)") with '-'
  rule separators instead of "Vout (V)" + '='.
- Header/columns match the staged originals (Measured Value*, 25/15/19/6 rule).

Two real bugs fixed (both are the handoff's "lines drop" / wrong-value defect):
- formatMeasuredExact reads the value from index 4 so negative signs survive
  ("PASS-4.24..." -> "-4", not "4"); also decimal-code N -> toFixed(N) exactly
  (DSCA differs from 5B/8B where code 2 means 1 decimal).
- parseRawData no longer consumes the first DSCA STATUS group as a bare
  step-response line when that line is absent (dropped 3 rows on e.g. DSCA39-01).

Safety: when value count != spec-row count the positional zip is ambiguous
(subtype measures load points the template omits, e.g. DSCA49 5mA pair), so the
cert is SKIPPED (null) and left for STAGE 3 per-subtype mapping rather than
emitting misaligned data.

Validation: DSCA38-05 (SN 180224-1) Final-Test block byte-identical to its
staged original. 92/126 templated models render cleanly; 7 ambiguous + 27
no-spec skip. Remaining ACCURACY-block spacing diffs are the deferred cosmetic
gap. NOT YET LIVE: testdatadb service not restarted, nothing re-pushed to
Hoffman (STAGE 3 gate).

Coverage gap to resolve before publish: only 126/357 DSCA models in the DB have
a staged template (56,074 certs, 70.1%); 231 models / 23,866 certs have none and
now render null — needs a STAGE 1 extension (more staged originals).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:33 -07:00
ac616ae3f5 sync: auto-sync from AD2 at 2026-06-18 06:13:27
Author: Mike Swanson
Machine: AD2
Timestamp: 2026-06-18 06:13:27
2026-06-18 13:02:33 -07:00
a1a09cd0fb sync: auto-sync from AD2 at 2026-06-17 16:21:47
Author: Mike Swanson
Machine: AD2
Timestamp: 2026-06-17 16:21:47
2026-06-18 13:02:33 -07:00
42fa576692 dataforth(datasheet): save email draft to John summarizing all findings
Plain-language summary for John (EE, non-developer) covering all 4 problems
(RTD label, DSCA table, same-day run, missing units), framed as ACG-owned fixes.
Cross-links the four supporting technical docs. Draft - not yet sent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:33 -07:00
10552b74f6 dataforth(datasheet): correct Cause 2 of missing units - cryptolocker incident, not import timing
Per Mike: import runs every 15 min, so routine timing isn't the cause. The 379
absent units are confined to 2025-10..2026-01 (stop after Jan 2026) on TS-4L/4R/1R
- fingerprint of a one-time overwrite during the incident/recovery (fresh DOS logs
overwrote accumulated appended server-side logs for ~2 weeks). One-time, not
recurring; backfill from the surviving staged .TXT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:32 -07:00
344c467be4 dataforth(datasheet): root-cause the 608 missing units (report for John)
608 staged datasheets absent from DB. Two causes: (1) 229 units with encoded/
non-standard serials the importer's leading-digit regex silently skips - data is
in the .DAT, recoverable; full blind spot is 840 serials / 9,510 records / 141
models dropped fleet-wide. (2) 379 units whose per-model .DAT was overwritten by a
later work order - recoverable only from the staged .TXT or a log backup. Adds
John-facing report, raw data, and the chase-missing-units.js tool.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:32 -07:00
ae9b49a75f dataforth(datasheet): same-day retest faithfulness — exposure sweep + fix proposal
Whole-source sweep (981,716 records / 406,549 serials): 6,515 same-day multi-run
events; DB holds a NON-latest run for 311 (the strictly-greater-date conflict rule
freezes on an arbitrary same-day run). Corrects the verdict doc to flag same-day
retests as a latest-wins faithfulness violation (not benign). Adds the proposed
>= -with-data-differs conflict-rule fix (diagnose-only) and the sweep tool.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:32 -07:00
2fc1b934eb dataforth(datasheet): parsing-fidelity validation — all staged originals vs DB
Validated all 11,922 staged original .TXT datasheets against test_records.
0 genuine parse faults across 11,239 comparable records; mismatches all explained
(retests, reused serials, VAS format, legacy out-of-scope units). Adds the
validate-parsing.js tool, raw report, and verdict. Two follow-ups (NOT parse bugs):
608 staged units absent from DB (ingestion completeness), and same-day retests keep
the first run (ON CONFLICT strictly-greater-date).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:32 -07:00
fc80a918ba dataforth(datasheet): PROPOSED Defect-A fix — render RTD input as Temp (C), not resistance
Repo copy only (review before deploying to C:\Shares\testdatadb). Folds RTD
(sensorNum 7) into the temperature path so the ACCURACY input column shows
'Temp. (C)' with signed temperature values, matching the original DOS-generated
datasheets and thermocouples (3-6). raw_data stimulus is already in deg C; no
conversion. getSensorNum and the i==13 ohm/ohm unit override are untouched.

Verified read-only against deployed env: 8B35 SN 179553-13 now shows Temp (C);
regression over 184 5B/8B renders -> 15 RTD changed (intended), 0 non-RTD changed.
Does NOT address Defect B (DSCA template). See DATASHEET-RTD-BUG-DIAGNOSIS-2026-06-17.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:32 -07:00
89ffc6fbe0 memory: AD2 sync.sh pushes main not ad2 (fork push gotcha)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:32 -07:00
88ae32e22e sync: auto-sync from AD2 at 2026-06-17 13:56:18
Author: Mike Swanson
Machine: AD2
Timestamp: 2026-06-17 13:56:18
2026-06-18 13:02:31 -07:00
d5499741ee memory: record AD2 Dataforth-fork structure + sync gotchas
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:31 -07:00
cfeec7f9f6 dataforth: preserve fork operational context as clients/dataforth/CLAUDE.dataforth.md
Relocated verbatim from .claude/CLAUDE.md so the ad2 fork stops editing the
shared fleet harness doc. After rebase onto main, .claude/CLAUDE.md = the lean
fleet version; Dataforth ops context lives here. Keeps future ad2 syncs clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:31 -07:00
120bd8059b sync: auto-sync from AD2 at 2026-06-17 13:35:55
Author: Mike Swanson
Machine: AD2
Timestamp: 2026-06-17 13:35:55
2026-06-18 13:02:27 -07:00
sysadmin
b7656258b7 Add AD scripts and stage import instructions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:23 -07:00
sysadmin
d2529e9e80 Session log 2026-04-03: WO import, 7B support, PG migration started
- 33K work orders imported, 2.27M records linked
- 7B exact-match formatter added (31 params, 120VAC, Packing Check List)
- TXT formatting refined to match QB TAB positions exactly
- PostgreSQL 18 installed on AD2, database created
- SQL Server Express uninstalled
- Full Dataforth audit document generated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:23 -07:00
a458b7cbef sync: auto-sync from GURU-5070 at 2026-06-18 12:57:51
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-18 12:57:51
2026-06-18 12:58:07 -07:00
ee0df93489 wiki: compile darrell-delphen (seed) 2026-06-18 12:54:57 -07:00
27170e1a5c sync: auto-sync from GURU-5070 at 2026-06-18 12:49:38
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-18 12:49:38
2026-06-18 12:51:08 -07:00
0745f5d09b dataforth/testdatadb: 8B/5B/SCM render verify results + convergence plan
Stage+verify (template-gated, no slotmaps/precision yet) vs Hoffman, content-only:
15 models content-perfect, 17 precision-distance, 70 NULL (need slotmaps), 8B38/7B
family-specific. Remaining work = AD2's existing DSCA machinery (slotmaps / Math.fround
QB rounding / frequency-AAC accuracy labels). Recommend converging with AD2's DSCA path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 12:50:44 -07:00
82ae349941 dataforth/testdatadb: mine 8B/5B/SCM Final-Test templates from Hoffman (136 models)
Root cause of the ~5,148 unpublished 8B/5B/SCM PASS records (driving the fix):
(1) parseRawData wrongly consumes a PASS/FAIL line as the step-response line for
    non-DSCA families that omit the "0","0",v line (8B45/8B49/5B39/SCM5B33...) ->
    drops the first Final-Test group -> measurement-count mismatch -> null render.
(2) Even parsed, the renderer has ONE hardcoded DATA_LINES['8B'] (RTD-shaped), so
    models like 8B45 (frequency input, == DSCA45 structurally) get wrong param
    names/specs. Same class as DSCA -> needs per-model templates.

Mined per-model templates from the Hoffman originals (published siblings) for all
136 mineable models via tools/mine-hoffman-dsca.py (family-agnostic extractor):
8b5bscm-templates.json = {accOut, accHeader, rows[name,spec], _srcSerial}.
Input-type split: 72 voltage / 18 temp / 12 current (accuracy already handled) +
10 frequency (8B45/5B45 — same unsolved freq-accuracy as DSCA45) + 24 7B/other.
Only 3 niche models (17 units) have no Hoffman original.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 12:50:44 -07:00
760c2264dc dataforth/testdatadb: wire UI presets + publish buttons; add /api/search sort/dir
Backend (deployed live on AD2, service restarted, + repo copy resynced — it was
far behind the deployed server):
- /api/search: add whitelisted sort/dir (NULLS LAST) so sortable headers and the
  "Latest uploads" preset work. web_status filter and POST /api/upload already
  existed on the server; the stale repo copy now matches live.

Frontend (redesign prototype):
- "Latest uploads" preset (web_status=on + sort=api_uploaded_at desc) and
  "Not yet published" (web_status=off) are now active presets.
- Push to Web (inspector) + Re-push (multi-select) wired to POST /api/upload
  behind a confirm() gate; refresh WEB status after. Validated idempotently on a
  published record (unchanged:1, errors:0).
- "Retested units" stays disabled — needs a retest flag in the pipeline (next).

tools/preview-proxy.py: forward POST so the publish buttons work in same-origin preview.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 12:50:44 -07:00
72a2fbe6ce dataforth/testdatadb UI: fix cert fit (transform-scale) + publish-state chips
- fitCert: replace the flaky CSS `zoom` (Firefox support is recent/inconsistent)
  with transform:scale() measured against the widest line (+ right margin and
  font-load retries) so the cert always scales to fit the inspector with no
  horizontal clip. Validated live on a narrow 5B cert (0.74x) and a wide DSCA45
  cert (0.55x) against the real AD2 dataset.
- inspector Web field -> Published (green) / Not published (amber) chips.
- widen default inspector 480 -> 500px.
- tools/preview-proxy.py: serve the prototype AND reverse-proxy /api to the live
  AD2 server so the cert iframe is same-origin during preview — styleCert/fitCert
  read iframe.contentDocument, which silently no-ops when the iframe is loaded
  cross-origin straight from AD2 (why the fit looked broken in earlier previews).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 12:50:44 -07:00
419d6e58b5 dataforth/dsca33-45: recover lost specs from Hoffman API (56/58 models)
The DSCA33/DSCA45 main spec files lost in the cryptolocker wipe are recoverable:
the original software published correct certs to the Hoffman product API before
the wipe and our null-skipping renderer never overwrote them. Mine per-model
Final-Test templates (names + specs + verbatim accuracy headers) straight from
those originals instead of requesting spec files from Dataforth/John.

- dsca33-45-templates.json: 56 models (DSCA33 34/35, DSCA45 22/23); only
  DSCA33-1948 + DSCA45-1746 (24 units) lack an original.
- mine-hoffman-dsca.py: the re-runnable miner.
- DSCA33-45-HOFFMAN-RECOVERY handoff for the AD2 session (incl. the gate:
  validate each render vs its Hoffman original before enabling live rendering).
- memories: Hoffman recovery (supersedes the spec-gap "need John" note) and the
  AD2 SSH MTU-blackhole root cause/fix; errorlog entries (syncro jq, ssh correction).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 12:50:43 -07:00
c76bdc4db1 wiki: compile cascades-tucson (full) 2026-06-18 12:49:25 -07:00
b405523a62 sync: auto-sync from HOWARD-HOME at 2026-06-18 12:31:06
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-18 12:31:06
2026-06-18 12:31:14 -07:00
852396cebe sync: auto-sync from HOWARD-HOME at 2026-06-18 12:24:43
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-18 12:24:43
2026-06-18 12:24:54 -07:00
ba3636430f sync: auto-sync from HOWARD-HOME at 2026-06-18 12:23:28
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-18 12:23:28
2026-06-18 12:23:38 -07:00
3d59218906 chore: stop tracking controller-query scratch files (.sta.json); gitignore temp patterns
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 12:23:20 -07:00
719789b499 sync: auto-sync from HOWARD-HOME at 2026-06-18 12:21:23
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-18 12:21:23
2026-06-18 12:22:42 -07:00
a700435174 sync: auto-sync from Mikes-MacBook-Air.local at 2026-06-18 10:17:39
Author: Mike Swanson
Machine: Mikes-MacBook-Air.local
Timestamp: 2026-06-18 10:17:39
2026-06-18 10:17:40 -07:00
15d367e5d7 session-log: ACG website Phase 3B enhancements + PIM RMM onboarding
Phase 3B enhancements deployed to ww9:
- Radio show promotion (header badge + promo bar)
- 3-step visual funnel after Trust section
- Strengthened calculator CTAs (primary button + service links)
- Increased vertical rhythm with responsive section padding

Patriot Internal Medicine onboarded to GuruRMM:
- Client created with two locations (Tucson and Sonoita)
- Enrollment keys vaulted (NORTH-WOLF-6270, LIGHT-HARBOR-9617)
- Bot alerts posted to #dev-alerts

Also: pulled curated brand assets from Gitea, answered pfSense ping question

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-06-18 10:14:24 -07:00
b8e0988569 sync: auto-sync from HOWARD-HOME at 2026-06-18 09:36:06
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-18 09:36:06
2026-06-18 09:36:14 -07:00
f84a6cc901 sync: auto-sync from HOWARD-HOME at 2026-06-18 08:29:03
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-18 08:29:03
2026-06-18 08:29:11 -07:00
6e5ce8fa0a sync: auto-sync from HOWARD-HOME at 2026-06-18 08:15:50
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-18 08:15:50
2026-06-18 08:16:10 -07:00
28 changed files with 13091 additions and 242 deletions

View File

@@ -32,6 +32,9 @@
- [AAD Connect msDS-KeyCredentialLink writeback](reference_aadconnect_keycredlink_writeback.md) — "completed-export-errors" + 8344 INSUFF_ACCESS_RIGHTS on a protected admin account = WHfB key writeback blocked by AdminSDHolder. Diagnose with csexport /f:x; fix with dsacls WP;msDS-KeyCredentialLink on AdminSDHolder + SDProp.
- [UniFi Site Manager cloud API](reference_unifi_site_manager_api.md) — `api.ui.com` + `X-API-KEY` (vault `services/unifi-site-manager`) = remote access to the WHOLE ACG UniFi fleet (~36 consoles) outside UOS. Tier1 `/v1/hosts|sites|devices|isp-metrics` = inventory+health+WAN. Tier2 CONNECTOR `/v1/connector/consoles/{id}/proxy/network/api/s/default/stat/{device,sta}` = **full UOS parity** (per-radio cu_total airtime + per-client RSSI) for ANY console, remote. Backend `unifi-wifi/scripts/gw-sitemanager.sh` (`fleet|devices|sites|isp|net`). Standalone UDM WAN SSH usually firewalled; per-console SSH pw at `clients/<slug>/udm-ssh`.
- [reference_sqlx_migrations_immutable](reference_sqlx_migrations_immutable.md) -- NEVER edit an already-applied sqlx migration file — even a comment. sqlx::migrate! checksums each file at compile time and validates against _sqlx_migrations at startup; a changed checksum crash-loops the server with "migration N was previously applied but has been modified". Code review MUST flag any edit to an applied migration.
- [AD2 SSH MTU blackhole](ad2-ssh-mtu-blackhole.md) — AD2 SSH "lockouts"/mid-session read-errors over the Dataforth OpenVPN were a PMTU blackhole (tunnel PMTU ~1424 vs adapter MTU 1500), NOT a ban/account-lockout/flaky tunnel. Fix: pin the OpenVPN adapter MTU to 1400 (done on GURU-5070 via its SYSTEM RMM agent); permanent = `mssfix 1360` on the OpenVPN server. Diagnose over RMM, not SSH.
- [DSCA33/45 resolved via Hoffman](project_dsca33_45_resolved_via_hoffman.md) — The "lost" DSCA33/45 spec files are recoverable from the Hoffman API (original certs survived the wipe); do NOT ask John. 56/58 models mined into projects/dataforth-dos/dsca33-45-templates.json; only DSCA33-1948 + DSCA45-1746 (24 units) lack an original. AD2 handoff: DSCA33-45-HOFFMAN-RECOVERY-2026-06-18.md.
- [AD2 comms via sync only](ad2-comms-via-sync-only.md) — The AD2 Dataforth-box Claude session is coord-API-isolated (Gitea only); coord msg/lock/todo never reach it. Coordinate with AD2 ONLY via git /sync (committed docs + ## Note blocks).
## Users
- [Howard Enos](user_howard.md) — Mike's brother, technician, full access. Machines: ACG-TECH03L, Howard-Home (authoritative in users.json).

View File

@@ -0,0 +1,21 @@
---
name: ad2-comms-via-sync-only
description: The AD2 (Dataforth) Claude session is coord-API-isolated — reach it ONLY via git /sync (committed notes/docs), never coord messages/locks
metadata:
type: feedback
---
The AD2 Dataforth-box Claude session is **network-isolated from the ACG coord API** (172.16.3.30 —
the Dataforth network can't reach it); it only has Gitea/git access. So coord-API **messages, locks,
and todos NEVER reach AD2**. ALL inter-session coordination with AD2 must go through git **`/sync`**:
committed handoff docs and `## Note for <user>` blocks in synced session logs, which AD2 reads when
it pulls. A coord lock on an AD2-only file (e.g. `datasheet-exact.js`) is also meaningless — only the
AD2 session edits that box.
**Why:** burned a round of `coord msg send AD2` + lock that were silent no-ops (Mike: "You can't
coord with AD2 — all comms needs to be via sync").
**How to apply:** to hand work to or coordinate with the AD2 session, write it into a committed doc
(e.g. `projects/dataforth-dos/*HANDOFF*.md`) and/or a `## Note for <user>` block in a session log,
then `/sync`. Do NOT use the coord skill for AD2. (Coord API is still fine for non-isolated ACG
machines.) [[prefer-ssh-over-rmm]]

View File

@@ -0,0 +1,40 @@
---
name: ad2-ssh-mtu-blackhole
description: AD2 SSH "lockouts"/mid-session timeouts over the Dataforth OpenVPN were an MTU/PMTU blackhole, not a ban/account-lockout/flaky tunnel; fix = pin the tunnel adapter MTU to 1400
metadata:
type: project
---
AD2 (Dataforth, `192.168.0.6`) SSH from the fleet over OpenVPN (client subnet `192.168.6.x`)
intermittently looked "locked out": sessions **authenticated fine**, then died mid-session with
`Read error from remote host 192.168.6.2 ... Unknown error [postauth]` and
`ssh_dispatch_run_fatal: Connection from authenticating user sysadmin ... Connection timed out [preauth]`.
Small/interactive commands often worked; bulk reads + `scp` stalled.
**Root cause (diagnosed 2026-06-18 via RMM — SSH itself was the failing channel, so don't diagnose it over SSH):**
- NOT account lockout — Windows lockout threshold is 5/30min but **zero 4740 events**; `sysadmin` never locked.
- NOT an IP ban — **no IPBan/wail2ban/RdpGuard**, **0 inbound firewall block rules**.
- NOT auth — **every** `Accepted publickey for sysadmin` succeeded.
- NOT load — AD2 was CPU ~11%, 11.7 GB RAM free.
- It was a **PMTU blackhole.** OpenVPN tunnel path MTU is **~1424** (DF ping: wire 1424 passes,
1428 drops). But GURU-5070's OpenVPN adapter (`Local Area Connection`, ifIndex 12, IP
`192.168.6.2`) was set to **MTU 1500** → TCP negotiated MSS 1460 → full-size bulk/scp segments
exceeded the tunnel and were **silently dropped (DF set)**, while sub-MTU interactive packets
passed. That is why it presented as random "lockouts" that got worse with bulk transfer.
**Fix applied (2026-06-18):** `Set-NetIPInterface -InterfaceIndex 12 -AddressFamily IPv4 -NlMtuBytes 1400`
run via **GURU-5070's own RMM agent** (`819df0c8...`, runs as `nt authority\system` = elevated; the
elevated lever on the local box when you can't self-elevate from the Claude shell). Validated: a
**1.41 MB single-session SSH transfer to AD2 completed in 9s, no read error** (previously blackholed).
`~/.ssh/config` `ad2` block annotated + tightened keepalives (`ServerAliveInterval 15`,
`ServerAliveCountMax 4`, `ConnectTimeout 20`).
**Durability / permanent fix:** `Set-NetIPInterface` is registry-persistent, but **OpenVPN Connect may
reset the adapter MTU to 1500 on reconnect** — re-apply if SSH bulk transfers start stalling again
(check `Get-NetIPInterface -InterfaceIndex 12`). The real permanent fix is **server-side on the
Dataforth OpenVPN server: `mssfix 1360` (or `push "tun-mtu 1400"`)** so every fleet client clamps
automatically — `192.168.6.4` showed the identical symptom, so this is fleet-wide, not 5070-only.
Corrects the earlier wrong attribution ("flaky VPN tunnel" / "my rapid scp+ssh bursts triggering a
ban") — the tunnel is up and stable for small packets; only over-MSS segments were dropped. See
[[prefer-ssh-over-rmm]] (RMM-as-fallback guidance still holds; the *reason* was MTU, not a flaky VPN).

View File

@@ -0,0 +1,27 @@
---
name: project_dsca33_45_resolved_via_hoffman
description: DSCA33/45 "lost spec files" are recoverable from the Hoffman API (original certs survived the wipe) — do NOT request spec files from John; mine templates from Hoffman instead
metadata:
type: project
---
The DSCA33/DSCA45 main spec files lost in Dataforth's cryptolocker wipe (which blocked rendering
~8,763 certs and prompted "ask John for the spec files") are **recoverable** — the original software
published correct DSCA33/45 certs to the **Hoffman API** before the wipe, and our null-skipping
pipeline never overwrote them. **Do not ask John for spec files.** Supersedes the FIX2-5 handoff's
TODO 2 and the `ad2`-branch memory `project_dsca33_45_spec_gap` (which says "blocked, need John").
Mined **56 of 58 models** straight from Hoffman into `projects/dataforth-dos/dsca33-45-templates.json`
(per model: `accOut`, verbatim 2-line `accHeader`, Final-Test `rows` of name+spec, and a known-good
`_srcSerial`). Only 2 niche models have no original anywhere: **DSCA33-1948 (16u)**, **DSCA45-1746 (8u)**.
Coverage: ~7,157 units already correct + live on Hoffman (no action); ~1,580 not-yet-uploaded units
need rendering from the mined templates + AD2's already-derived slotMaps.
AD2 handoff + the critical gate: `projects/dataforth-dos/DSCA33-45-HOFFMAN-RECOVERY-2026-06-18.md`.
**Critical:** validate each model's render byte-for-byte against its Hoffman original BEFORE enabling
live DSCA33/45 rendering — once the renderer returns non-null, the pipeline stops skipping these and
will re-push/UPDATE the 7,157 good originals on the next cycle (safe only if the render matches).
Hoffman read API: `GET https://www.dataforth.com/api/v1/TestReportDataFiles/{serial}` (returns
`{SerialNumber,Content,CreatedAtUtc,UpdatedAtUtc}`); creds vault `clients/dataforth/hoffman-product-api`.
Miner: `projects/dataforth-dos/tools/mine-hoffman-dsca.py`. AD2 access notes: [[ad2-ssh-mtu-blackhole]].

12
.gitignore vendored
View File

@@ -112,3 +112,15 @@ temp/
.claude/wiki_staging/*
!.claude/wiki_staging/README.md
.tmp-*
# Controller/API scratch files (transient session temp; never commit)
.sta.json
.dev.json
.nc.json
.nc2.json
.cj_tmp
.q*
.po_*.json
.put*.json
.hdr
.csrf_tmp

View File

@@ -0,0 +1,76 @@
# Cascades — Network Logging / Observability Plan (SPEC — build later)
- **Created:** 2026-06-17 (Howard-Home / claude-main)
- **Status:** PLAN ONLY — no infra changes made. For a scheduled build.
- **Goal:** Capture + retain a searchable record of **device drops / kicks / disconnects** and the
telemetry to **root-cause the ongoing Cascades network issues** (2.4 GHz congestion, sticky
clients, roaming/min-RSSI deauths — see `reports/2026-06-16-unifi-full-audit.md`).
## The problem we found (2026-06-17)
- **The UniFi controller is NOT retaining client history.** A 7-day pull of the Cascades site's
`stat/event` AND `stat/alarm` returned **zero** records (auth/site fine — client/device queries
return data). So when a phone/device drops or is kicked, **nothing is recorded** -> the network
is a black box after the fact.
- **pfSense logs locally but in tiny circular buffers** (clog) that roll over in hours — no useful
history, no search.
- => We must **capture events at the source and ship them to a store with retention + search**.
pfSense and UniFi are log *sources*; neither is a retention/search platform on its own.
## Where the collector lives — decision
| Candidate | Verdict |
|---|---|
| **CS-SERVER** | **NO.** Fragile EOL DC (Dell R610, ~16 yr, **degraded OS RAID-1**, single-DC data-loss risk, I/O-bound). Adding syslog ingestion load is unacceptable. |
| **pfSense / UniFi alone** | Sources only. pfSense local retention ~hours; UniFi retains ~0 client events. Live view yes, forensics no. |
| **Synology cascadesDS (`192.168.0.120`)** | **PREFERRED on-site collector.** DSM up on :5001 (vault `clients/cascades-tucson/synology-cascadesds`). Built-in **Log Center** = a syslog server (retention + search + notifications), no Docker needed. Becoming backup-only anyway -> light syslog duty fits, keeps logs local + off CS-SERVER. |
| Jupiter (`172.16.3.20`, ACG office Docker) | Fallback if a richer stack (Graylog/Loki) is wanted; cross-site (Cascades -> office). Use only if on-site Synology is ruled out. |
**Recommendation:** Synology **Log Center** as the on-site syslog collector. If cascadesDS turns
out to be a Plus/x86 model with spare RAM, **Container Manager** can later add **Graylog** or
**Grafana Loki** for richer search/dashboards/alerting — but Log Center alone meets the core ask.
## Sources to configure (ship syslog -> Synology Log Center, UDP/TCP 514)
1. **pfSense** (`192.168.0.1`): Status -> System Logs -> Settings -> **Remote Logging**: server =
Synology IP:514; select **System, Firewall, DHCP, Gateway**. (DHCP lease grant/expire/decline =
device-drop + IP-churn signal; firewall = blocked traffic.)
2. **UniFi controller + Cascades APs** (UOS `172.16.3.29`, site `va6iba3v`): Settings -> System ->
enable **Remote Logging / syslog** to the Synology, include **client events / debug** so the
**APs emit assoc/DEAUTH-with-reason-code + RSSI-at-disconnect + roam** events — the gold data for
"who got kicked and why." Confirm AP syslog is forwarded (not just controller app log).
3. **(Optional) switches** — port up/down/flap events (the ~25 underspeed ports + 3 offline
switches in the audit are suspects).
## Client time-series snapshotter (fills the controller's history gap)
Because the controller isn't keeping client history, add a small **poller** (every 1-2 min) that
hits the controller API `/stat/sta` for the Cascades site and appends per-client rows:
`ts, mac, hostname, ap, band, channel, rssi, tx_retry%, satisfaction, is_wired`.
- **Where to run:** Synology Task Scheduler + a script, or a small container; or cross-site on
GuruRMM (`172.16.3.30`) via cron; or a coord-scheduled job. Store as SQLite/CSV (or into the
collector if Graylog/Loki is chosen).
- **Why:** lets us answer "did device X drop because RSSI cratered / it stuck to a far AP / 2.4 GHz
airtime saturated" — correlating drops with the documented RF problems. Pairs with the existing
`unifi-wifi` skill collectors (`watch-ap.sh`, `radio-usage.sh`, `neighbor-collect.sh`).
## Alerting (phase 2)
From Log Center (or Graylog/Loki): notify (Discord via `post-bot-alert.sh` / `discord-dm`) on AP
reboot, switch-port flap, repeated deauths for a tracked device, or DHCP pool pressure.
## Retention
Target 30-90 days searchable (HIPAA-adjacent network metadata; no PHI in syslog). Size the Synology
Log Center archive / volume accordingly; rotate/compress older.
## Build steps (when scheduled)
1. Confirm cascadesDS **model + RAM + DSM version** (determines Log Center-only vs Container Manager
for Graylog/Loki). Cred: vault `clients/cascades-tucson/synology-cascadesds`.
2. Install/enable **Log Center** (Package Center) -> enable **syslog server** (514), set retention.
3. Point **pfSense** remote syslog at it (sources above) — verify receipt.
4. Enable **UniFi controller + AP** remote syslog (with client/deauth events) — verify AP deauth
events arrive with reason + RSSI.
5. Deploy the **client snapshotter** (cron/Task Scheduler) — verify rows accumulating.
6. (Optional) Container Manager -> Graylog/Loki+Grafana for dashboards; wire alerting.
7. Validate: force a test device off WiFi -> confirm a searchable deauth event with reason + RSSI.
## Open items
- Confirm cascadesDS model/RAM/Docker capability (step 1).
- Confirm no PHI traverses syslog (network metadata only) for the HIPAA file.
- Decide retention window + alert thresholds.
- If on-site is rejected -> fall back to Jupiter (Graylog/Loki) cross-site.

View File

@@ -0,0 +1,42 @@
# Cascades — Voice (VLAN 30) Device Inventory
Living tracker of devices migrated onto the isolated **VOICE VLAN 30** (`10.0.30.0/24`).
Built/cutover started 2026-06-17. Runbook: `voice-vlan-cutover.md`. PPSK key (Poly WiFi):
vault `clients/cascades-tucson/wifi-voice-ppsk`.
- DHCP pool `10.0.30.100-.250` (dynamic; no reservations). Gateway `10.0.30.1`, DNS `8.8.8.8/1.1.1.1`.
- Isolation: internet/cloud-PBX only; blocked from PHI/LAN/VLAN20/mgmt (verified `pfctl -sr`).
## On VOICE so far
| Lease IP | MAC | Type | Location / Owner | Status |
|---|---|---|---|---|
| 10.0.30.201 | e4:e7:49:52:3a:06 | Vertical-Remote desktop (wired, USW-16-PoE p16) | Vertical mgmt jump box (LogMeIn/RDP) | On VOICE; re-leased after power-on |
| 10.0.30.202 | 48:25:67:64:8a:88 | Poly (WiFi, CSCNet voice PPSK) | **Accounting Director office — Lauren Hasselman** | On VOICE; **dial tone + outbound call to cell verified** |
| 10.0.30.203 | 48:25:67:d0:b8:ac | Poly (WiFi, CSCNet voice PPSK) | **Life Enrichment office 132** | On VOICE |
| 10.0.30.204 | 48:25:67:a3:f8:3b | Poly (WiFi, CSCNet voice PPSK) | **Front desk phone** | On VOICE |
| 10.0.30.205 | 48:25:67:64:93:34 | Poly (WiFi, CSCNet voice PPSK) | **Front desk courtesy phone** | On VOICE |
| 10.0.30.206 | 48:25:67:64:91:ea | Poly (WiFi, CSCNet voice PPSK) | **Kitchen Director — Alyssa** (via Dining Room AP) | On VOICE |
| 10.0.30.207 | 48:25:67:64:8f:0b | Poly (WiFi, CSCNet voice PPSK) | **Kitchen phone** (via Kitchen AP) | On VOICE |
| 10.0.30.208 | 48:25:67:64:94:ba | Poly (WiFi, CSCNet voice PPSK) | **Chef phone** (via Kitchen AP) | On VOICE |
| 10.0.30.209 | 48:25:67:64:89:6e | Poly (WiFi, CSCNet voice PPSK) | **Tamra** (via AP 217) | On VOICE |
| 10.0.30.210 | 48:25:67:d0:b1:83 | Poly (WiFi, CSCNet voice PPSK) | **Crystal** (via AP 217) | On VOICE |
| 10.0.30.211 | 48:25:67:64:8e:ae | Poly (WiFi, CSCNet voice PPSK) | **Megan** (via AP 217) | On VOICE |
| 10.0.30.212 | 48:25:67:64:93:25 | Poly (WiFi, CSCNet voice PPSK) | **Lois Lane** — room 206 | On VOICE |
| 10.0.30.213 | 48:25:67:64:93:4f | Poly (WiFi, CSCNet voice PPSK) | **Medtech phone** — room 206 | On VOICE |
| 10.0.30.214 | 48:25:67:64:8f:1d | Poly (WiFi, CSCNet voice PPSK) | **Christina Durpas** — room 206 | On VOICE |
| 10.0.30.215 | 48:25:67:64:86:aa | Poly (WiFi, CSCNet voice PPSK) | **Veronica Feller** — room 206 | On VOICE |
| 10.0.30.216 | 48:25:67:64:92:6b | Poly (WiFi, CSCNet voice PPSK) | **Lupe Sanchez** (via AP 324) | On VOICE |
| 10.0.30.217 | 48:25:67:d0:ae:3e | Poly (WiFi, CSCNet voice PPSK) | **Memory Care reception** (via Memcare Nurse Station AP) | On VOICE |
| 10.0.30.218 | 48:25:67:d0:af:10 | Poly (WiFi, CSCNet voice PPSK) | **Shelby Trozzi** — MemCare Director | On VOICE |
| 10.0.30.219 | 48:25:67:d0:b4:26 | Poly (WiFi, CSCNet voice PPSK) | **Karen Rossini** — room 515 | On VOICE |
| 10.0.30.220 | 48:25:67:64:95:6b | Poly (WiFi, CSCNet voice PPSK) | **Christine** (last name ~Nyuda — VERIFY) — room 515 | On VOICE |
| 10.0.30.221 | 48:25:67:64:91:cf | Poly (WiFi, CSCNet voice PPSK) | **Salon phone** (via salon AP) | On VOICE |
| 10.0.30.222 | 48:25:67:64:81:8e | Poly (WiFi, CSCNet voice PPSK) | **Meredith** — room 140 | On VOICE |
| 10.0.30.223 | 48:25:67:64:8f:14 | Poly (WiFi, CSCNet voice PPSK) | **Ashley** — room 103 | On VOICE |
## Still to migrate
- **Poly (WiFi): 22 of 22 DONE** ✓ — all wireless phones migrated to VOICE.
- **AudioCodes (8, wired USW-16-PoE ports 1-8): 0 of 8 done** — flip port -> VOICE **+ PoE Power-Cycle** each to re-DHCP. MACs (OUI `00:90:8f`): see runbook appendix.
> Note: lease IPs are dynamic — a phone may pull a different `.1xx`/`.2xx` on renewal. Track by **MAC + location**, not IP.

View File

@@ -91,6 +91,20 @@ restore from on-box auto-backups, eliminating the duplicate dhcpd, **resetting +
5. **Optional monitoring:** alert on DHCP-not-completing / duplicate-dhcpd / mass device-disconnect
so a future event is caught in minutes.
## Post-Recovery Casualties / Lessons (updated 2026-06-18)
- **Kitchen thermal printer (iPad POS ticket printer)** — reported "disconnected from the network"
and would not print the morning after. Root cause: it powered up **during the DHCP-down window**
of the recovery (duplicate dhcpd + 2nd-floor switch not passing offers), never got an IP, and
cached a disconnected state without retrying once the network was healthy. **Power-cycling the
printer** forced a fresh DHCP request against the now-healthy network and it resumed printing
(confirmed printing iPad tickets). Not a printer or network fault — a device that needed a manual
kick post-recovery.
- **Recovery-checklist item:** after a network/power outage, **power-cycle any device that booted
during the DHCP-down window (printers, POS, IoT, cameras, appliances)** — they frequently do not
re-acquire DHCP on their own even after the network is restored. Sweep for stragglers proactively
rather than waiting for user reports.
## Reference Information
- **pfSense:** `192.168.0.1`, Plus 25.07-RELEASE, ZFS. WAN: Cox (`igc0`, 184.191.143.x). LAN: `igc1`

View File

@@ -0,0 +1,81 @@
# Cascades — power-outage follow-up: OpenVPN flapping root cause + kitchen printer post-outage casualty
- **Date:** 2026-06-18
- **Machine:** Howard-Home
- **Client:** Cascades of Tucson
- **Continuation of:** 2026-06-17 power-outage incident (`clients/cascades-tucson/reports/2026-06-17-power-outage-incident.md`)
## User
- **User:** Howard Enos (howard)
- **Machine:** Howard-Home
- **Role:** tech
## Session Summary
Short follow-up session on the 2026-06-17 Cascades power outage. Two items.
Diagnosed why the Howard-Home OpenVPN Connect tunnel to Cascades pfSense kept disconnecting/
reconnecting. Read the pfSense OpenVPN server log (`/var/log/openvpn.log`): the disconnects are
caused by a configured **inactivity timeout**`Howard/... Inactivity timeout (--inactive),
exiting` firing at ~5 min (connected 23:23:52 -> dropped 23:28:57 ~= 305s), after which OpenVPN
Connect auto-reconnects. Ruled OUT duplicate-CN (0 "will cause previous active session" events),
WAN instability (Cox gateway stable since the 20:47 recovery), and TLS/auth errors (clean auth each
time; the "IP packet with unknown IP version=0" line is cosmetic). It is a configured idle-disconnect,
not a fault. Fix = raise/disable the OpenVPN server `--inactive` timeout (keepalive pings do NOT
reset it — `--inactive` measures tunnel data). Proposed, not applied (standing no-change-without-go rule).
Second: the kitchen thermal printer (iPad POS ticket printer) reported "disconnected from the network"
and would not print the morning after the outage; Howard power-cycled it and it resumed printing iPad
tickets. Root cause: it powered up DURING the DHCP-down window of the recovery (duplicate dhcpd +
2nd-floor switch not passing offers), never got an IP, cached a disconnected state, and did not retry
once the network was healthy. The power-cycle forced a fresh DHCP request against the now-healthy
network. Not a printer or network fault. Ran a read-only straggler sweep on pfSense (pulled recent
dhcpd.log, per-MAC DISCOVER vs ACK): 13/13 active DISCOVER senders are completing, 0 stuck — network
healthy. Noted that "gave-up" casualties like the printer are INVISIBLE to a DHCP scan (they stopped
requesting), so expect a few more "won't connect" reports today, each fixed by a power-cycle.
Updated the incident report with the printer casualty + a recovery-checklist lesson; synced.
## Key Decisions
- **OpenVPN flapping = configured `--inactive` idle timeout, not instability** — diagnosed from the
server log rather than guessing; ruled out duplicate-CN / WAN / TLS. Fix proposed (raise/disable inactive), not applied.
- **Printer = power-outage DHCP-down-window casualty** — correct fix was the power-cycle (re-DHCP);
no network change needed. Captured as a recovery-checklist item (power-cycle devices that booted during the DHCP-down window).
- **A DHCP-log scan cannot find gave-up casualties** (they stop requesting) — so the realistic plan is
reactive (power-cycle as reports come in), not a proactive scan.
## Configuration Changes
- No infrastructure changes. pfSense access was read-only (OpenVPN log, dhcpd log).
- Repo: updated `clients/cascades-tucson/reports/2026-06-17-power-outage-incident.md` (added "Post-Recovery
Casualties / Lessons" section — kitchen printer + the power-cycle-stragglers checklist item).
## Infrastructure & Servers
- **Cascades pfSense** `192.168.0.1`, Plus 25.07. OpenVPN server `ovpns1`, user `Howard` (client IP pool
192.168.10.x; this session it got 192.168.10.2). Server has an **`--inactive` idle timeout ~300s** that
drops idle clients. WAN = Cox (igc0, 184.191.143.x / dpinger WAN_DHCP + WANCOAX_DHCP). pfSense logs are
PLAIN TEXT (read with tail/grep, not clog).
- OpenVPN client on Howard-Home: OpenVPN Connect (IV_GUI_VER=OCWindows_3.9.0-5008), public src 98.168.18.21.
- **Kitchen thermal printer:** iPad POS ticket printer (exact IP/MAC not captured); resolved by power-cycle.
## Commands & Outputs
- OpenVPN flap cause: `grep -i "inactivity timeout" /var/log/openvpn.log` -> `Howard/... Inactivity timeout (--inactive), exiting`; duplicate-CN count = 0.
- Straggler sweep: pulled `tail -5000 /var/log/dhcpd.log` locally -> python per-MAC DISCOVER vs ACK -> 13 senders, 13 completing, 0 stuck.
## Pending / Incomplete Tasks
- **OpenVPN flapping fix:** raise/disable the pfSense OpenVPN server `--inactive` timeout (proposed; needs go).
- **Watch for more post-outage stragglers** (printers/POS/IoT that gave up) — power-cycle each as reported.
- Carryover from the outage (unchanged): rotate the exposed Synology credential (vault history commit 1fbc0e1);
enable AutoConfigBackup; UPS coverage/runtime/clean-shutdown review; 5GHz Option B + 2.4 Low->Medium bump
(plus the auto-channel change still needs a proper data-driven re-plan).
## Reference Information
- Incident report: `clients/cascades-tucson/reports/2026-06-17-power-outage-incident.md` (updated).
- Prior session logs (same outage): `2026-06-17-howard-cascades-power-outage-recovery-and-5ghz.md`,
`2026-06-17-howard-cascades-poly-phone-drops-network-smoothing.md`.
- Memory: `reference_pfsense_25_07_ops.md`, `feedback_cascades.md` #4 (no prod change without discussing).
- pfSense access: `bash .claude/skills/unifi-wifi/scripts/pfsense-ssh.sh cascades-tucson run "<cmd>"`.

View File

@@ -0,0 +1,79 @@
## User
- **User:** Howard Enos (howard)
- **Machine:** Howard-Home
- **Role:** tech
## Session Summary
Diagnosed slow performance on Lupe Sanchez's workstation **DESKTOP-TRCIEJA** (Cascades of Tucson), reported as the machine being slow when opening Excel files on the desktop. Located the agent via `rmm-search` (GuruRMM agent `c9bf1a2d-bfdc-401e-9cc8-f9e90bb19587`, online, Windows 11 Pro build 22000) and ran a full performance diagnostic over the GuruRMM agent in SYSTEM context.
The diagnostic identified two converging root causes. First, the hardware is end-of-life: a Gateway ZX6971 all-in-one with an **Intel Core i3-2120 (Sandy Bridge, 2011, 2C/4T)** and **8 GB RAM with only ~2.1 GB free** (27%), running Windows 11 on an unsupported CPU. The SSD itself (DREVO X1 SSD, 224 GB, 44% free) is healthy. Second, and the direct cause of slow Excel opens, the machine is running **two real-time antivirus engines simultaneously**: ACG's Bitdefender Endpoint Security Tools (keep) plus the previous MSP's leftover Datto stack, which bundles a real-time scanner. Both engines scan every file on access, and on a weak 2-core CPU under memory pressure that produces the exact "slow to open Excel" symptom. OneDrive was ruled out — the desktop is local (`C:\Users\LupeSanchez\Desktop`, 50 files / 278 MB, no cloud placeholders), not redirected via Known Folder Move.
Recon of uninstall entries showed the "Datto AV" reported by SecurityCenter2 is actually the leftover Datto EDR/RMM stack: **Datto RMM (CentraStage)**, **Datto EDR Agent (Infocyte)**, and the bundled **Endpoint Protection SDK (DattoAV)** under `C:\Program Files\infocyte\agent\dattoav\`. The Cascades wiki already flags this same leftover Datto (CentraStage + Infocyte) stack for fleet-wide cleanup. Bitdefender (v8.26.6.644, services running) is properly installed.
I proposed removing the leftover Datto stack in order (RMM -> EDR -> AV SDK, so Datto RMM cannot re-push the AV) and disabling the Adobe Acrobat PDFMaker Excel add-in. Before acting on a security/management-agent removal I asked Howard to confirm scope. **Howard declined all changes** — the decision is to replace the machine instead. No changes were made to the endpoint; this was diagnostic-only.
## Key Decisions
- **No remediation performed — machine to be replaced.** Given the 2011-era i3-2120 / 8 GB hardware is EOL and unsupported for Win11, Howard opted to order a replacement rather than spend time uninstalling the Datto stack on a box being retired.
- **Split the diagnostic into small section scripts.** A combined ~7 KB multi-line PowerShell body failed agent-side (exit -1, "Failed to execute command"); breaking it into <2 KB section scripts (`diag_a/b/c`) each ran cleanly. Logged as friction in `errorlog.md`.
- **Ruled out OneDrive/Known Folder Move early** — desktop is local with no cloud placeholders, so the slowness is not on-demand-file hydration.
- **Identified dual real-time AV as the direct cause** of slow Excel opens, distinct from the hardware weakness.
## Problems Encountered
- **Heredoc quoting failure** building the combined diagnostic inline in Bash ("unexpected EOF") due to embedded single quotes — switched to writing the script to a file and feeding it via `jq --rawfile`.
- **Agent rejected the large combined script** (exit -1, "Failed to execute command") despite only ~7 KB and valid syntax; a minimal sanity command ran fine, confirming the agent chokes on a large multi-line `-Command` body. Resolved by splitting into three small section scripts. Logged via `log-skill-error.sh ... --friction`.
## Configuration Changes
- **Endpoint (DESKTOP-TRCIEJA): none.** Diagnostic-only; no software removed or settings changed.
- Repo: created and then deleted temporary diagnostic scripts in the working tree (`diag_perf.ps1`, `diag_a.ps1`, `diag_b.ps1`, `diag_c.ps1`, `diag_recon.ps1`) — none committed.
- `errorlog.md`: appended one `--friction` entry (rmm large-command-body failure).
## Credentials & Secrets
None discovered or created this session. RMM auth via existing vault path `infrastructure/gururmm-server.sops.yaml` (unchanged).
## Infrastructure & Servers
- **DESKTOP-TRCIEJA** — Lupe Sanchez workstation, Cascades of Tucson, site CascadesTucson.
- GuruRMM agent ID: `c9bf1a2d-bfdc-401e-9cc8-f9e90bb19587` (resolve live by hostname; UUIDs change on re-enroll). Agent version 0.6.66.
- Hardware: Gateway ZX6971 AIO, serial `DOGDGAA0012200249A6300`, Intel i3-2120 2C/4T, 7.9 GB RAM, DREVO X1 SSD 224 GB (Healthy, SMART PredictFailure=False), C: 97.2 GB free of 222.8 GB.
- OS: Windows 11 Pro build 22000 (i3-2120 not Win11-supported).
- Active console user: `lupesanchez` (profile `C:\Users\LupeSanchez`).
- **Leftover Datto stack (previous MSP, flagged for cleanup):**
- Datto RMM (CentraStage) — service `CagService`, uninstall `C:\Program Files (x86)\CentraStage\uninst.exe` (NSIS, `/S`).
- Datto EDR Agent (Infocyte) v3.17.1.5409 — uninstall `"C:\Program Files\infocyte\agent\agent.exe" --uninstall`.
- Endpoint Protection SDK / DattoAV v1.0.2510.6851 — quiet uninstall `"C:\Program Files\infocyte\agent\dattoav\Endpoint Protection SDK\endpointprotection.exe" uninstallSdk`.
- **Bitdefender (ACG, keep):** Endpoint Security Tools v8.26.6.644; services EPSecurityService / EPProtectedService / EPRedline running. Defender in passive state (correct).
- **Excel/Office:** Microsoft 365 Apps for business 16.0.20026.20168; `EXCEL.EXE` at `C:\Program Files\Microsoft Office\Root\Office16\`.
- Network note: System log shows recurring Event 2505 — duplicate computer name on the network ("another computer on the network has the same name").
## Commands & Outputs
- `bash .claude/scripts/rmm-search.sh desktop-trcieja` -> 1 match, Cascades of Tucson, online.
- Diagnostic dispatched cmd `c31c6552-1abd-41eb-b269-159cab42360d` (combined script) -> **failed exit -1** "Failed to execute command".
- Sanity cmd `acb9eba4...` -> `PONG from DESKTOP-TRCIEJA as nt authority\system`; `quser` -> `lupesanchez console 1 Active`, logon 6/18/2026 9:27 AM.
- Section scripts ran clean. Top processes by memory: EXCEL 540 MB, Memory Compression 427 MB, EPSecurityService (Bitdefender) 408 MB / 589 CPU-s, OUTLOOK 349 MB, endpointprotection (DattoAV) 333 MB / 611 CPU-s, AEMAgent (Datto RMM) 132 MB.
- AV via SecurityCenter2: Datto AV (multiple states), Windows Defender (0x60100 passive), Bitdefender Endpoint Security Tools.
- `Get-MpComputerStatus`: Defender RTP False, SigAge 65535 (Defender stood down — expected with Bitdefender present).
- Excel add-ins (user LupeSanchez): Adobe PDFMaker LoadBehavior=3 (loaded), MicrosoftDataStreamerforExcel, PowerPivotExcelClientAddIn.
- Disk/stability events (7d): Event 98 x13, Event 153 x4, Event 11 x2 — occasional SATA I/O retries (SMART healthy).
- `log-skill-error.sh "rmm" "...large multi-line powershell body fails exit -1; split into <2KB section scripts" --friction` -> logged.
- Two `[RMM]` bot alerts posted to #dev-alerts (dispatch + result summary).
## Pending / Incomplete Tasks
- **Order replacement workstation for Lupe Sanchez** (Howard's action). EOL i3-2120 / 8 GB.
- On the new machine: provision GuruRMM + Bitdefender only; do **not** carry over the Datto stack (CentraStage + Infocyte/DattoAV).
- Fleet-wide at Cascades: the leftover Datto (CentraStage + Infocyte) cleanup remains open per the wiki — DESKTOP-TRCIEJA is another instance of it.
- Optional follow-ups (now moot if replacing): resolve duplicate computer name on the network (Event 2505); the Acrobat PDFMaker add-in remains enabled.
- No wiki/ticket entry created this session (offered, not requested).
## Reference Information
- GuruRMM agent: `c9bf1a2d-bfdc-401e-9cc8-f9e90bb19587` (DESKTOP-TRCIEJA).
- RMM API: `http://172.16.3.30:3001`; vault `infrastructure/gururmm-server.sops.yaml`.
- Diagnostic command IDs: `c31c6552-...` (failed combined), `acb9eba4-...` (sanity), `bae771da-ce6e-4b14-be38-b66c07fd96bf` (recon).
- Wiki: `wiki/clients/cascades-tucson.md` (line ~415: leftover Datto RMM/EDR cleanup item).

View File

@@ -0,0 +1,73 @@
## User
- **User:** Howard Enos (howard)
- **Machine:** Howard-Home
- **Role:** tech
## Session Summary
Howard reported that the Synology Drive sync from the Cascades Synology NAS (cascadesDS, 192.168.0.120) to CS-SERVER was not syncing all files/folders — some folders showed empty on the server, specifically the "Server" folder and its "ALdoc" content. Investigated end-to-end across both the CS-SERVER Drive Client (via GuruRMM) and the Synology Drive Server (via direct SSH over the site VPN).
Established the architecture: a Synology Drive **Client** (v7.5.0.16085) runs on CS-SERVER under the Windows `sysadmin` user, authenticating to the NAS as the DSM account **`Sync`** (uid 1116, home `/var/services/homes/Sync`), one-way **download** (mode:1) of that `Sync` user's "My Drive" (`/volume1/homes/Sync/Drive/`) into `D:\Shares\Main`. The folders Howard saw empty (`Server`, `homes`, `home`, `Managment`, `Sales Dept`, `Downloads`) are **empty stub directories at the source** inside the Sync user's My Drive — leftovers from the prior FreeFileSync/GoodSync migration (`sync.ffs_db`, `_gsdata_` are in the same folder). CS-SERVER was faithfully mirroring empty folders. The "ALdoc" data actually lives at `/volume1/homes/Sync/Drive/Documents/ALDocs` (under Documents, which syncs fine) and is already present on CS-SERVER at `D:\Shares\Main\Documents\ALDocs` (17,694 files). The real `/volume1/Server` share (2,486 files, 1.9 G) is a separate shared folder that is NOT in the Drive sync scope at all.
Ruled out several false leads: not encrypted/locked shares (data lists fine as admin), not a selective-sync blacklist (blacklist only excludes `/Backup`, `/Moments`, dotfiles, `.lnk/.pst/.swp/.tmp`), and not a dead Drive Server. `synopkg status` falsely reported the package "stopped" (status 263 "failed to get unit status"), but the systemd unit `pkgctl-SynologyDrive` is **active** with `cloud-daemon` running and 6690 listening; the `code: -2`/`bio error` lines in the client log are long-poll fallbacks, not a failed server.
Howard clarified intent: this is a **file-server migration** (Synology -> CS-SERVER); he needs all department-share data staged and kept current as users are moved, then the Synology retired. He accepted handling user home data separately (users move it into redirected profiles that already back up to the server), which removes `homes` from the bulk sync. With `homes` out of scope, a Synology Drive **Team Folder** sync of the department shares becomes viable. Produced a full setup walkthrough (Admin Console Team Folder enable + low versioning; Drive Client Download-only tasks into a clean `D:\Shares\_SynMigration\<share>` staging area; validation; per-share cutover choreography). No changes were made — diagnosis + plan only.
## Key Decisions
- **Use Synology Drive Team Folders (not robocopy) for the department shares**, now that `homes` is handled per-user. Earlier robocopy recommendation was driven by the homes 228 GB limitation; with homes excluded, the native real-time Team Folder path is acceptable and matches "permanent real-time sync."
- **Exclude `homes` from the bulk sync** — Synology Drive cannot enable Sync on the `homes`/`home` system shares, and Howard will migrate user home data via redirected profiles instead.
- **Sync into NEW empty staging folders (`D:\Shares\_SynMigration\<share>`), never into a live share** — a one-way download task makes the local folder mirror the NAS and could delete/overwrite an existing share's files. Keeps current shares + NTFS/SMB permissions untouched.
- **Set Drive version history low/off** on the migration team folders to limit space + indexing load on the aging NAS.
- **Per-share cutover rule:** a share is mirrored from Synology OR live on CS-SERVER, never both; disable the download task at cutover so the now-authoritative server copy is not overwritten.
- **Pilot on the `Server` share first** (smallest real share, 1.9 G / 2,486 files, and the one that triggered the report) before templating the rest.
## Problems Encountered
- **`synopkg status SynologyDrive` falsely reported "stopped"** (status 263, "failed to get unit status") while the service was actually running. Resolved by checking the systemd unit directly: `systemctl is-active pkgctl-SynologyDrive` = active, with `cloud-daemon.exe`/`cloud-monitor` under the slice and 6690 listening.
- **Initial RMM grep matched the wrong log** — searching for "Server" hit `auto_updater.log` ("update server"/"Windows Server"). Re-ran against `daemon.log` specifically to get the real sync connection + event lines.
- **Local-vs-NAS folder-name mismatch** initially confused the mapping (local `Documents`/`Company Web Docs`/`FD`/`ITSvc` are not `/volume1` share names). Resolved by `find`-ing those folder names on the NAS, which located the real sync root at `/volume1/homes/Sync/Drive/`.
- **Background NAS `du` jobs failed (exit 255)** after the temp askpass dir was cleaned mid-run; harmless (read-only) and the needed sizes were already captured.
## Configuration Changes
None. Diagnosis and planning only — no changes made to CS-SERVER, the Synology, or the repo (other than this session log).
## Credentials & Secrets
- No new credentials discovered or created. Synology admin creds already vaulted at `clients/cascades-tucson/synology-cascadesds.sops.yaml` (username `admin`); used read-only for SSH diagnostics via the unifi-wifi-style SSH_ASKPASS pattern (system OpenSSH).
- The Drive Client authenticates to the NAS as DSM account **`Sync`** (uid 1116). Its password is stored inside the Drive Client config on CS-SERVER, not in the vault; not needed for this work.
## Infrastructure & Servers
- **CS-SERVER** (192.168.2.254) — GuruRMM agent `c39f1de7-d5b6-45ae-b132-e06977ab1713` (online). Synology Drive Client v7.5.0.16085 installed, runs as Windows `sysadmin`. Client config/logs: `C:\Users\sysadmin\AppData\Local\SynologyDrive\` (logs in `\log`, sync session in `\data\session\2`, filters in `\data\session\2\conf\*.filter`).
- **cascadesDS Synology NAS** — 192.168.0.120. DSM 7.2.1-69057. Synology Drive Server **3.5.0-26088**, systemd unit `pkgctl-SynologyDrive` (active), protocol port **6690** (SSL). Reachable directly over the site VPN (ports 22/5000/5001/6690 open).
- **Sync architecture:** Drive Client connection conn_id 2, `username:sync`, `mode:1` (one-way download), `is_index_home:0`, `user_is_admin:1`; source `/volume1/homes/Sync/Drive/` -> dest `D:\Shares\Main`.
- **NAS shared folders (real data):** `/volume1/Server` (2,486 files, 1.9 G), `/volume1/Management` (13,712 files, 5.5 G), `/volume1/homes` (141,545 files, ~228 G), `/volume1/Public` (~50 G), `/volume1/SalesDept` (~12-23 G), `/volume1/chat`, `/volume1/Activities`, plus legacy `/volume1/Sandra Fish`, `/volume1/pacs`, `/volume1/web`.
- **Sync user My Drive (`/volume1/homes/Sync/Drive/`) per-folder file counts (source truth):** Documents=519 top (18,193 recursive, incl. `ALDocs`), Public=49, SalesDept=102, chat=1, Company Web Docs=79, FD=154, ITSvc=1; EMPTY stubs: Server=0, homes=0, home=0, Managment=0, "Sales Dept"=0, Downloads=0.
- **ALDocs:** source `/volume1/homes/Sync/Drive/Documents/ALDocs`; present on CS-SERVER at `D:\Shares\Main\Documents\ALDocs` (17,694 files). NOT under any "Server" folder.
## Commands & Outputs
- Locate CS-SERVER agent: `bash .claude/scripts/rmm-search.sh cs-server cascades` -> `c39f1de7-...` online.
- RMM recon of `D:\Shares\Main` showed empty `Server`/`homes`/`Managment`/`Sales Dept`/`Downloads`/`home` vs populated `Documents`(18 GB)/`SalesDept`(23 GB)/`Public`(2.7 GB)/etc.
- Drive Client connection line (daemon.log): `...server_ip:192.168.0.120, server_port:6690, ... username:sync, ... mode:1, ... is_index_home: 0, user_is_admin: 1 ...` and `... 'D:\Shares\Main/Server/New folder' ... is one-way downloading, ignore event.`
- SSH askpass pattern (system OpenSSH, no sshpass): `SSH_ASKPASS=<helper> SSH_ASKPASS_REQUIRE=force DISPLAY=:0 ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no admin@192.168.0.120 'sh -s' <<heredoc`.
- NAS source truth: `find /volume1/homes/Sync/Drive/Server -type f | wc -l` = 0; `find .../Documents/ALDocs -type f | wc -l` = 26 (top) with many subfolders.
- Drive Server real state: `systemctl is-active pkgctl-SynologyDrive` = `active` (synopkg reported stop falsely); `netstat -tlnp | grep 6690` = LISTEN.
## Pending / Incomplete Tasks
1. **Confirm in-scope share list** — default `Server, Management, Public, SalesDept, chat, Activities`; decide on legacy `Sandra Fish`/`pacs`/`web`. Note `Culinary, IT, Receptionist, directoryshare` already exist as CS-SERVER shares (exclude unless re-pull wanted).
2. **Confirm where ALDocs lives among the REAL shares** (not just the Sync user's My-Drive copy) so a Team Folder definitely captures it before the old My-Drive task is retired. Some curated folders (Company Web Docs, ITSvc, Quickbooks) were found to also exist under `/volume1/Public`.
3. **Execute the Team Folder migration** (pending go-ahead): Admin Console enable Team Folders + low versioning; Drive Client Download-only tasks into `D:\Shares\_SynMigration\<share>`; pilot on `Server`, validate, template the rest.
4. **Per-share cutover** later: create production SMB share + AD-group NTFS/SMB perms (per `phase2-server-prep.md`), repoint GPO drive maps, disable that share's download task.
5. Optional: save the walkthrough as a migration runbook under `clients/cascades-tucson/docs/migration/`.
## Reference Information
- Migration plan docs: `clients/cascades-tucson/docs/migration/phase4-synology.md` (§6 Synology transition), `phase2-server-prep.md` (§4c sync, §4d share perms).
- Synology DSM: `http://192.168.0.120:5000` (Synology Drive Admin Console for Team Folders + Version Control).
- Vault: `clients/cascades-tucson/synology-cascadesds.sops.yaml`.
- CS-SERVER Drive Client paths: `C:\Users\sysadmin\AppData\Local\SynologyDrive\{log,data\session\2}`.
- Proposed staging root: `D:\Shares\_SynMigration\<share>` (new, separate from live shares and from the existing `D:\Shares\Main` Drive staging).

View File

@@ -0,0 +1,139 @@
# Cascades — Voice VLAN 30 live migration (all Poly + desktop) + network-logging plan
## User
- **User:** Howard Enos (howard)
- **Machine:** Howard-Home
- **Role:** tech
## Session Summary
Continuation of the 2026-06-17 VOICE VLAN 30 build (see `2026-06-17-howard-voice-vlan30-build.md`).
This session executed the live device migration onto the isolated VLAN 30 and produced a spec for
network observability. Work spanned the 06-17 -> 06-18 date boundary.
First, the Vertical-Remote management desktop was moved. Howard set USW-16-PoE port 16 native VLAN
to VOICE, but the desktop kept its old `192.168.2.180` lease. Diagnosis (pfSense + UniFi
controller) showed nothing misconfigured: re-VLANing a wired port does not bounce the NIC link, so
Windows held its old lease and its unicast renewal to the old DHCP server was (correctly) blocked by
the VOICE isolation rules. A UniFi client block/unblock is a MAC filter, not a link bounce, so it
had no effect. Fixed by bouncing port 16 via the controller API (PUT rest/device port_overrides
forward:disabled then restore, preserving ports 1-8) — the desktop re-DHCP'd to 10.0.30.201.
Second, Howard re-keyed the 22 Poly WiFi phones to the voice PPSK over ~2 hours. As each phone
joined, the controller `/stat/sta` was polled to map the new 10.0.30.x lease to the phone's
location/owner. A WiFi re-auth is itself a fresh DHCP, so the Poly phones needed no bounce. The
first phone (Lauren Hasselman, Accounting Director) was validated end-to-end: dial tone + an
outbound call to a cell phone. All 22 Poly phones plus the desktop (23 devices) ended up on VOICE,
each pulling a clean lease and isolated from PHI/LAN/VLAN20/mgmt. A living inventory doc was created
(`docs/network/voice-phone-inventory.md`) and the wiki Voice-VLAN entry flipped PLANNED -> IN
PROGRESS.
Third, Howard raised the need for network logging to track devices that drop/get kicked and to
root-cause the ongoing Cascades network issues. Investigation found the UniFi controller is
retaining ZERO client events/alarms for the Cascades site over 7 days, and pfSense logs locally in
tiny circular buffers — i.e., drop/kick history is not being captured at all. A "plan only" spec was
written (`docs/network/network-logging-plan.md`) recommending the Synology cascadesDS (DSM Log
Center syslog server) as the on-site collector (CS-SERVER ruled out as the fragile EOL DC), with
pfSense + UniFi/AP syslog as sources and a 1-2 min client snapshotter to fill the controller's
history gap.
Finally, a sync hit a rebase conflict because controller-query scratch files written to the repo
CWD (.sta.json etc.) were swept into a commit by `git add -A`, and a stray locked curl.exe held the
file. Killed the process, untracked .sta.json, gitignored the temp patterns, and pushed clean.
## Key Decisions
- **Desktop cutover via port bounce, not NIC change.** Confirmed desktop is DHCP; the fix for a
stuck lease after re-VLAN is a link bounce (port disable/enable or PoE power-cycle), not a NIC
reconfig and not a UniFi client block/unblock.
- **Read drop/kick state from the UniFi controller, not pfSense SSH**, after pfSense sshd began
rate-limiting following many rapid SSH calls. Controller API (`/stat/sta`) was the healthy path
and also gives AP/location hints.
- **Track phones by MAC + location, not IP** (leases are dynamic; a phone may renew to a different
10.0.30.x).
- **Network-logging collector = Synology Log Center, NOT CS-SERVER.** CS-SERVER is the fragile
EOL/degraded-RAID single DC; adding syslog ingestion is unacceptable. pfSense/UniFi are sources,
not retention/search stores. Synology keeps it on-site and off the DC.
- **Plan only for logging** (per Howard) — spec written, build scheduled later.
## Problems Encountered
- **Desktop stuck on 192.168.2.180 after port moved to VLAN 30** — stale DHCP lease; renewal
blocked by VOICE isolation. Resolved by bouncing port 16 via controller API -> re-DHCP to
10.0.30.201.
- **UniFi controller PUT returned HTTP 403** — UniFi OS requires a CSRF token on writes. Resolved by
reading `x-updated-csrf-token` from the login response headers and sending `X-CSRF-Token`.
- **pfSense SSH began failing (exit 255)** while ping still succeeded — sshd rate-limiting after many
rapid `pfsense-ssh.sh` calls. Switched to the UniFi controller API for subsequent reads.
- **Git-Bash `/tmp` path mismatch** — msys `curl -o /tmp/x.json` wrote where Windows python could not
read (FileNotFoundError). Switched to CWD-relative scratch files.
- **Scratch files committed + rebase blocked** — CWD-relative `.sta.json` got swept into a commit by
sync's `git add -A`, and a stray locked `curl.exe` (PID 25252) held the file, blocking the rebase.
Killed the process, `git rm --cached .sta.json`, gitignored `.sta.json`/`.dev.json`/`.q*`/etc.,
committed, and pushed. (Lesson: write API scratch OUTSIDE the repo or use the ignored `.tmp-` prefix.)
- **Earlier errorlog rebase conflict** (concurrent GURU-5070 entry) — resolved keeping both entries.
## Configuration Changes
- **Created** `clients/cascades-tucson/docs/network/voice-phone-inventory.md` — living inventory, 23
devices on VOICE (desktop + 22 Poly) with MAC/IP/location.
- **Created** `clients/cascades-tucson/docs/network/network-logging-plan.md` — observability spec
(build later).
- **Updated** `clients/cascades-tucson/docs/network/voice-vlan-cutover.md` — added the
bounce-to-re-DHCP CRITICAL step; fixed stale NIC-change/OpenVPN/reservation references.
- **Updated** `wiki/clients/cascades-tucson.md` — Voice VLAN PLANNED -> IN PROGRESS, two locations.
- **Updated** `.claude/memory/MEMORY.md` + created `project_cascades_isolated_vlan_pattern.md` (prior
session, synced).
- **Updated** `.gitignore` — ignore controller-query scratch patterns; `git rm --cached .sta.json`.
- **pfSense (prior session, this thread):** VOICE rule protocol TCP -> Any via PHP config API.
- **UniFi:** bounced USW-16-PoE port 16 (disable/restore) via controller API — temporary, restored
to exact original (native VOICE, forward:customize).
## Credentials & Secrets
- **VOICE PPSK key** `V0!c38863171` — vault `clients/cascades-tucson/wifi-voice-ppsk.sops.yaml`
(created prior session, pushed). Entered on all 22 Poly phones this session.
- UniFi controller RW: vault `infrastructure/uos-server-network-api-rw` (used for reads + the port
bounce). pfSense admin: vault `clients/cascades-tucson/pfsense-firewall`. Synology: vault
`clients/cascades-tucson/synology-cascadesds`.
## Infrastructure & Servers
- **VOICE VLAN 30:** `10.0.30.0/24`, gw `10.0.30.1` (pfSense `igc1.30`/opt241), DHCP `.100-.250`,
DNS `8.8.8.8/1.1.1.1`. Isolation = Guest-clone (any-proto quick blocks to 192.168.0.0/22 +
10.0.0.0/8 + 172.16.0.0/12, then pass any).
- **UniFi VOICE network** id `6a32e0194e709ad31ad161e6` (VLAN Only). USW-16-PoE mac
`d8:b3:70:21:94:5f`, device_id `685f39078e65331c46ef7e90`. UOS `172.16.3.29:11443`, site
`va6iba3v`.
- **Synology cascadesDS** `192.168.0.120` (DSM up on :5001) — proposed logging collector.
- **Jupiter** `172.16.3.20` (Unraid/Docker, hosts UniFi VM) — fallback collector host.
## Commands & Outputs
- Controller client poll (mapping phones): `POST /api/auth/login` -> GET
`/proxy/network/api/s/va6iba3v/stat/sta`, filter `network==VOICE or vlan==30`.
- Port bounce: `PUT /proxy/network/api/s/va6iba3v/rest/device/<id>` body
`{"port_overrides":[... port16 forward:disabled ...]}` with `X-CSRF-Token`, then restore.
- Drop/kick history check: `POST .../stat/event {"within":168,"_limit":5000}` and `.../stat/alarm`
both returned **0** records for Cascades -> controller not retaining client history.
- Result: 23 devices on VOICE (`10.0.30.201` desktop + `.202`-`.223` the 22 Poly).
## Pending / Incomplete Tasks
- **8 wired AudioCodes** (USW-16-PoE ports 1-8) — flip port -> VOICE + **PoE power-cycle** each to
re-DHCP. Not yet done.
- **Christine's last name** (room 515, `10.0.30.220`, mac `48:25:67:64:95:6b`) — flagged VERIFY in
the inventory (Howard unsure; "~Nyuda").
- **Network logging build** — execute `network-logging-plan.md` (step 1: confirm Synology
model/RAM/DSM -> Log Center-only vs Container Manager Graylog/Loki).
- Confirm phones register to cloud PBX (assumed; dial-tone proven on one) — add Part A 5b pinhole
only if a phone fails to register.
## Reference Information
- Runbook: `clients/cascades-tucson/docs/network/voice-vlan-cutover.md`
- Inventory: `clients/cascades-tucson/docs/network/voice-phone-inventory.md`
- Logging plan: `clients/cascades-tucson/docs/network/network-logging-plan.md`
- Prior log: `clients/cascades-tucson/session-logs/2026-06/2026-06-17-howard-voice-vlan30-build.md`
- Memory: `.claude/memory/project_cascades_isolated_vlan_pattern.md`
- Vault PPSK: `clients/cascades-tucson/wifi-voice-ppsk.sops.yaml`

View File

@@ -0,0 +1,119 @@
# 2026-06-18 — Darrell Delphen — Outlook email links failing (ISP SNI block)
## User
- **User:** Mike Swanson (mike)
- **Machine:** GURU-5070
- **Role:** admin
## Session Summary
Darrell Delphen reported that links in Outlook email would not open on one workstation
(DDDOffice072023), failing with "can't reach this page" / `ERR_CONNECTION_ABORTED` against a
`url.emailprotection.link` URL, while links opened from Gmail worked fine. The failing host is
Intermedia's Email Protection "Safe Link" rewriter — every link in Intermedia-protected mail is
rewritten to `https://url.emailprotection.link/...`, so all Outlook-delivered links routed through it.
Diagnosis was done entirely over GuruRMM against agent `000ed57d-fd05-4001-871c-244f43155c16`. DNS
resolved correctly and TCP 443 connected, but the TLS handshake died with SChannel `0x80090326`
(SEC_E_ILLEGAL_MESSAGE — "message received was unexpected or badly formatted"). The endpoint's TLS
stack was clean: FIPS off, no SCHANNEL protocol/cipher overrides, no cipher-suite GPO, only Windows
Defender (no third-party AV/proxy/VPN/LSP/WFP callout). The same node `199.193.205.140` handshook
successfully with the real SNI from GURU-5070 on a different network, proving the origin was healthy
and the interference was on the endpoint's path. A blast-radius sweep showed only
`url.emailprotection.link` failed while `google.com`, `microsoft.com`, `outlook.office365.com`,
`cloudflare.com`, `badssl.com`, and even `login.serverdata.net` (same Intermedia infra) all succeeded;
MTU was fine. An SNI-varied handshake to the same IP isolated it conclusively: `example.com` and
`login.serverdata.net` SNIs succeeded 12/12 while `url.emailprotection.link` failed 12/12, across
interleaved source ports — deterministic, SNI-keyed, not flow-hash/LAG. Root cause: a network device
on the path performing SNI-based content inspection that corrupted the handshake for that one hostname.
The gateway turned out to be an ISP-provided Extreme **EXOS** device the client had no login for. The
fix path was therefore ISP escalation. An escalation packet was drafted, and Cloudflare WARP was
installed on the workstation as an interim workaround — tunneling past the SNI block. With WARP
connected, egress moved to Cloudflare (104.28.152.216) and the real-SNI handshake succeeded (TLS 1.2,
HTTP 200). The ISP then disabled a "NetIQ" web/URL-filtering feature on the gateway, which cleared the
block at the source. After WARP was disconnected the native ISP path was verified working (5/5
handshakes + HTTP 200, egress back to 167.89.210.225), so WARP was uninstalled and the machine
returned to normal. Work was documented and billed on Syncro ticket #32437 — private technical note,
customer-facing/emailed summary, and 1.0h remote labor ($150.00, invoice #1650728058).
## Key Decisions
- Diagnosed exclusively via repeated GuruRMM `SslStream`/`Test-NetConnection` probes rather than
asking the client to run tools — faster and reproducible.
- Used an SNI-varied handshake to the *same fixed IP* as the decisive test. It separated
destination/server problems from path interference and proved the block was keyed on the SNI string.
- Ran a 12x repeatability test per SNI to rule out a faulty LAG/ECMP member (intermittent, 5-tuple
keyed) vs deliberate content matching (deterministic). Result was 0/12 vs 12/12 — deterministic.
- Chose Cloudflare WARP as the interim workaround because the block is SNI-based; any tunnel that
encrypts egress past the EXOS hides the SNI. WARP is the lightest deploy.
- Installed/connected WARP in stages (install, then connect, then verify) so each step could be
confirmed before flipping the tunnel, given the agent bounces on network-stack changes.
- Emailed the customer a plain-language summary (do_not_email:false) and kept the technical detail in
a hidden note.
## Problems Encountered
- **Shell state not persisting between Bash calls** — `$TOKEN`/`$RMM` from `rmm-auth.sh` were gone on
the next call (first dispatch produced no output). Fixed by `eval "$(rmm-auth.sh)"` inside every Bash
invocation.
- **PowerShell single-quotes collided with bash single-quoted `SCRIPT='...'`** — embedded
`'C:\Program Files\...'` terminated the bash string (`FilesCloudflareCloudflare: command not found`).
Fixed by defining the script via a quoted heredoc `SCRIPT=$(cat <<'PS' ... PS)`.
- **WARP install/connect bounced the RMM agent** — commands returned `interrupted` ("Agent restarted
during execution") because installing/connecting WARP resets the network stack/WFP. The agent
auto-reconnected (through WARP after connect); verified state with follow-up commands.
- **First WARP uninstall found nothing** — the product registers as **"Cloudflare One Client"**, not
"Cloudflare WARP", so the DisplayName filter missed it. Found the GUID
`{9E49837E-2971-413F-9587-119FA819E572}` and removed via `msiexec /x`.
## Configuration Changes
- **Endpoint DDDOffice072023:** Cloudflare WARP (Cloudflare One Client 2026.4.1390.0) installed,
registered, connected, then later disconnected and **fully uninstalled**. Net change to the machine: none.
- **ISP gateway (not us):** ISP disabled the "NetIQ" web/URL-filtering feature on the Extreme EXOS device.
- No changes to the ClaudeTools repo code.
## Credentials & Secrets
None discovered, created, or rotated this session.
## Infrastructure & Servers
- **Endpoint:** DDDOffice072023 — Windows, GuruRMM agent `000ed57d-fd05-4001-871c-244f43155c16`
(v0.6.66), RMM client "AZ Computer Guru" / site "Discovery test site". LAN gateway 192.168.1.1,
ISP egress 167.89.210.225, WARP egress (while active) 104.28.152.216.
- **ISP gateway:** Extreme Networks **EXOS** device, ISP-provided/managed (client has no login). Was
running a "NetIQ" URL-filtering feature doing SNI inspection.
- **Blocked destination:** `url.emailprotection.link` → CNAME `urlrs.gslb.serverdata.net` → A
199.193.205.140 (Intermedia Email Protection link-rewriter). GSLB pool also advertises
199.193.200.65 / 64.78.20.65 / 162.244.196.65, all TCP-dead from any network (not an ISP block).
- **GuruRMM API:** http://172.16.3.30:3001 (JWT via `rmm-auth.sh`).
## Commands & Outputs
- Decisive SNI test (same IP, varied SNI), via RMM PowerShell:
- SNI `url.emailprotection.link``0x80090326` SEC_E_ILLEGAL_MESSAGE (0/12 ok)
- SNI `example.com` / `login.serverdata.net` → TLS 1.2 AES256 (12/12 ok)
- Off-network control (GURU-5070) to 199.193.205.140 with real SNI → OK TLS 1.2.
- Post-ISP-fix native verify: real-SNI handshake 5/5 OK + `Invoke-WebRequest` HEAD → HTTP 200,
egress 167.89.210.225.
- WARP removal: `msiexec /x {9E49837E-2971-413F-9587-119FA819E572} /qn /norestart` → exit 0;
warp-svc/warp-cli/install-dir/uninstall-entry all gone.
## Pending / Incomplete Tasks
- None functional. Block is resolved at the ISP. If the issue recurs, suspect the EXOS "NetIQ"
feature being re-enabled — re-run the SNI-varied handshake test to confirm.
- Optional: if WARP is ever rolled to more machines as a workaround, harden it (force auto-connect,
lock client) and note egress moves to Cloudflare (breaks office-static-IP allowlisting).
## Reference Information
- **Syncro ticket:** #32437 (id 112766479) — https://computerguru.syncromsp.com/tickets/112766479
- Private note id 419714810; public/emailed summary id 419714813
- Line item id 42925426 (Labor - Remote Business 1.0h @ $150)
- Invoice #1650728058, total $150.00; status Invoiced
- **Customer:** Darrell Delphen (Syncro customer_id 35996725), no prepaid block.
- **SChannel error:** 0x80090326 = SEC_E_ILLEGAL_MESSAGE (handshake message malformed/unexpected) —
signature of in-path TLS/SNI tampering when paired with same-IP success off-network.

View File

@@ -17,6 +17,20 @@ Categories (the `[type]` tag): _(none)_ = skill/command execution failure ·
<!-- Append entries below this line -->
2026-06-18 | Howard-Home | git/sync-temp-files | [friction] controller-query scratch (.sta.json/.dev.json/.q*) written to repo CWD got swept into the commit by sync.sh 'git add -A', then a stray locked .sta.json blocked the rebase. Fix: write API scratch OUTSIDE the repo (or use the already-ignored .tmp- prefix); gitignored the patterns [ctx: ref=howard-home /tmp friction family]
2026-06-18 | Howard-Home | rmm | [friction] agent returns exit -1 'Failed to execute command' on a ~7KB multi-line powershell body sent as one command; split into <2KB section scripts and each ran fine [ctx: host=DESKTOP-TRCIEJA agent=0.6.66]
2026-06-18 | GURU-5070 | coord/ad2-comms | [correction] tried to coordinate with the AD2 session via coord API msg+lock; AD2 is network-isolated (Gitea only, no coord API) so those were no-ops. ALL inter-session comms with AD2 must go via git /sync (committed notes/docs).
2026-06-18 | GURU-5070 | syncro | comment POST piped straight to jq failed with 'jq: parse error: Invalid numeric literal at line 1 col 10' and left it AMBIGUOUS whether the note posted (GET-verify showed it had NOT); per no-retry rule had to GET first, then re-post. Robust pattern that worked: jq -n payload to a file, POST with --data-binary @file, capture response to a file, then GET-verify by subject. Skill's curl|jq comment pattern should adopt this. [ctx: ticket=32441 skill=syncro pattern=curl-pipe-jq]
2026-06-18 | GURU-5070 | post-bot-alert | Discord POST failed (non-200/unreachable) [ctx: channel=#bot-alerts http=400 resp={"message": "The request body contains invalid JSON.", "code": 50109}]
2026-06-18 | GURU-5070 | ssh/ad2 | [correction] attributed AD2 SSH timeouts to a flaky VPN tunnel + my rapid scp/ssh bursts; real cause = OpenVPN adapter MTU 1500 vs tunnel PMTU ~1424 -> TCP MSS blackhole that drops bulk/scp segments (DF set) while small cmds pass. Fix: tunnel adapter MTU 1400 [ctx: ref=feedback_prefer_ssh_over_rmm]
2026-06-18 | GURU-5070 | bash/env | [friction] /tmp curl-write then Windows-python read mismatch; wrote .claude/tmp + absolute path fixed it [ctx: ref=feedback_tmp_path_windows]
2026-06-18 | Howard-Home | pfsense-ssh/logs | [friction] used clog on pfSense 25.07 logs (now plain-text ASCII) -> empty output -> wrongly concluded DHCP log was empty / dhcpd not serving; cost a hypothesis. Read pfSense 25.07 logs with tail/grep/cat directly, NOT clog [ctx: ref=reference_pfsense_25_07_ops client=cascades-tucson]
2026-06-18 | AD2 | vault | real vault.sh not found at resolved vault_path; vault read failed [ctx: path=D:/vault/scripts/vault.sh]

View File

@@ -0,0 +1,42 @@
# 8B/5B/SCM Render Fix — Diagnosis + Stage/Verify Results (2026-06-18)
Driving the ~5,148 unpublished 8B/5B/SCM PASS records (render-coverage gap, same class as DSCA).
## Root cause (validated)
1. **parseRawData bug (general):** a PASS/FAIL line is wrongly consumed as the step-response line
for non-DSCA families that omit the `"0","0",v` line (8B45/8B49/5B39/SCM5B33...) -> drops the
first Final-Test group -> measurement-count mismatch -> null. Fix: change the step-skip guard to
`if (!((family === 'DSCA' || skipStepIfStatus) && looksLikeStatus))` and pass `skipStepIfStatus`
only for templated models (so non-templated models are byte-unchanged).
2. **No per-model Final-Test template:** one hardcoded `DATA_LINES['8B']` (RTD-shaped) -> wrong
param names/specs for non-RTD models (8B45 is frequency-input == DSCA45). Needs per-model
templates, same as DSCA. The "published" siblings render null too (legacy content on Hoffman;
`api_uploaded_at` was back-populated from Hoffman inventory, NOT from our render).
## Artifacts
- `8b5bscm-templates.json` — 136 models mined from Hoffman originals (accOut, accHeader, rows, _srcSerial).
- Verify harness pattern: patch a TEMP copy of datasheet-exact.js (parse-fix + template-gated
Final-Test, footer skip), render each model's published `_srcSerial`, content-normalized compare
to its Hoffman original. (Never touches the live renderer.)
## Verify results (template-gated Final-Test only; NO slotmaps/precision/accuracy-label work yet)
Content-only (cosmetic header/spacing deferred, per the DSCA byte-fidelity decision):
- 15 models content-perfect (>=0.99) — publishable as-is.
- 17 within precision-tuning distance (0.93-0.97) — single value-row rounding diffs.
- 27 @ 0.85-0.93, 7 < 0.85.
- 70 NULL — render-count mismatch -> need per-model slotmaps.
- By family: 8B avg 0.97, SCM5B 0.93 (strong); 8B38 0.78; 7B ~0.88 (separate parse path).
## Remaining work = the SAME machinery AD2 built for DSCA
1. Per-model **slotmaps** for the 70 nulls (AD2 `_derive_slotmaps.js`) — derive by matching rendered
measured values to the Hoffman original's displayed values.
2. **QB single-precision rounding** (AD2 `Math.fround`) — closes the 0.93-0.97 precision diffs.
3. **Frequency/AAC accuracy-block labels + stimulus formatting** — the still-open DSCA45/33 piece
(8B45/8B49/5B45 are frequency-input; SCM5B33 is AAC). Solving once covers BOTH 8B/5B AND DSCA45/33.
4. 8B38 (0.78) + 7B family (separate SCM7B path) — family-specific.
## Recommendation
Converge with AD2's mature DSCA machinery rather than re-implement in parallel in the shared
`datasheet-exact.js`: feed the mined `8b5bscm-templates.json` into AD2's template/slotmap/rounding
path and finish the frequency/AAC accuracy renderer once for DSCA + 8B/5B/SCM together.
The 15 content-clean models can be published immediately if a partial win is wanted.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,106 @@
# DSCA33 / DSCA45 — Recover the "lost" specs from Hoffman (Handoff to AD2)
**For:** the Claude session on AD2 (`C:\Shares\testdatadb`). **Ref:** Syncro #32441.
**Supersedes** the FIX2-5 handoff's *TODO 2 (request DSCA33/45 main spec files from John)*
**we do not need John.** The original specs are recoverable from the Hoffman API.
---
## The finding (why this changes the plan)
The DSCA33/DSCA45 "main spec" records (DSCMAIN/DSCOUT with SENTYPE/MAXIN/input-type) were
lost in the cryptolocker wipe, so `render-datasheet.js` bails (null render) and the pipeline
**skips** those models. BUT the **original software published correct DSCA33/45 certs to the
Hoffman API before the wipe**, and our broken renderer never overwrote them (it skips null
renders). They are still there, pristine.
Verified from GURU-5070 against the public API
(`GET https://www.dataforth.com/api/v1/TestReportDataFiles/{serial}`, OAuth client-creds,
vaulted `clients/dataforth/hoffman-product-api`):
| Family | Models | Mineable from Hoffman | Units already correct+live on Hoffman | No original anywhere |
|---|---|---|---|---|
| DSCA33 | 35 | **34** | 2,633 / 3,397 | **DSCA33-1948 (16 units)** |
| DSCA45 | 23 | **22** | 4,524 / 5,413 | **DSCA45-1746 (8 units)** |
So of the ~8,763 "blocked" certs: **~7,157 are already correct and live on the public site**
(no action needed), **~1,580 not-yet-uploaded units** just need rendering, and only
**24 units across 2 niche models** have no original to recover.
---
## The artifact (already built — use it, don't re-derive)
`projects/dataforth-dos/dsca33-45-templates.json`**56 models**, mined from the Hoffman
originals with the same `===`-rule column-span extractor as STAGE 1. Schema is a **superset of
`dsca-templates.json`**:
```json
"DSCA45-05E": {
"accOut": "Output (mA)",
"accHeader": [
" Frequency Calculated Measured",
" (Hz) Output (mA) Output (mA)* Error (%) Status"
],
"rows": [ {"name":"Supply Current","spec":"< 105 mA"}, ... ],
"_srcSerial": "176326-2"
}
```
- `rows` = the Final-Test `Parameter | Specification` list (incl. the spec-less rows like
`240 VAC Withstand`, `Hi-Pot`, and section sub-heads `Zero-Crossing Input` / `TTL Input`
**kept**, same skip-rule reconciliation as STAGE 2).
- `accHeader` = the **verbatim 2-line accuracy header** from the original. Use it — DSCA33/45
introduce header tokens the 92-model set never had (see flags) and the frequency-input layout.
- `_srcSerial` = a known already-uploaded serial for that model → your validation oracle.
Spot-checked DSCA33-07C and DSCA45-05E row-for-row against their live Hoffman originals: exact.
Regenerate if needed: `python projects/dataforth-dos/tools/mine-hoffman-dsca.py <map.json> <out.json>`.
---
## Flags (DSCA33/45 differ from the 92 you already did)
1. **New accOut tokens:** `Output (VDC)` and `Output (mADC)` (DSCA33 current/voltage DC outputs),
not just `Output (V)`/`Output (mA)`. Your accuracy-block must emit the verbatim token.
2. **Model-specific accuracy input label:** DSCA33 uses `Vin (mVAC)` / `Vin (VAC)` / `Iin (AAC)` /
`Iin (mAAC)`. **Use the `accHeader` lines** rather than synthesizing from a (missing) spec field.
3. **DSCA45 is frequency-input:** two-line super-header `Frequency` / `(Hz)` and a frequency
sweep in the accuracy block — structurally unlike the voltage/current-input models. Confirm the
accuracy renderer reproduces it (the `accHeader` gives you the exact text).
4. Your **DSCA33/45 slotMaps are already derived** and come from the same original layout as these
rows, so order should align — verify during validation.
---
## Plan
1. **Backup first** (fresh `pg_dump` + VSS) per FIX2-5 discipline. Save-state `datasheet-exact.js`.
2. **Load** `dsca33-45-templates.json` for `family in (DSCA33, DSCA45)`, same wiring as STAGE 2
(template `rows` drive names+specs; map raw_data STATUS groups positionally via the slotMaps;
QB `Math.fround` rounding; data-driven loadNote). For the accuracy block, drive the header from
`accHeader` / `accOut`.
3. **VALIDATE against Hoffman (the gate — stronger than STAGE 3):** for each model, render its
`_srcSerial` (an already-uploaded unit) and **content-normalized byte-compare against
`GET /api/v1/TestReportDataFiles/{_srcSerial}`**. Require a clean match **per model** before that
model is allowed to render/publish.
> **CRITICAL — do not enable live DSCA33/45 rendering until each model passes.** The moment the
> renderer returns non-null for DSCA33/45, the pipeline stops skipping them and will **re-push and
> UPDATE the ~7,157 already-correct originals** on the next cycle. That is only safe if the render
> byte-matches the original — which the per-model gate proves. A mismatched model would overwrite
> good customer certs. Gate hard.
4. **Publish the gap:** for validated models, render + `uploadBySerialNumbers` the **not-yet-uploaded**
units (~1,580; `api_uploaded_at IS NULL`). Already-uploaded units return `Unchanged` (idempotent).
5. **Leave blocked (24 units):** `DSCA33-1948` (16), `DSCA45-1746` (8) — no Hoffman original. Low
priority; only these would ever need John, and they look like one-off custom part numbers.
Commit to `ad2`; update #32441 (hidden notes). The remote operator sees it on sync.
---
## Reference
- Templates: `projects/dataforth-dos/dsca33-45-templates.json` (56 models)
- Miner: `projects/dataforth-dos/tools/mine-hoffman-dsca.py`
- Hoffman API creds: vault `clients/dataforth/hoffman-product-api` (read = `GET .../TestReportDataFiles/{serial}`)
- Memory: `project_dsca33_45_spec_gap` (updated — resolved via Hoffman, not John)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
# 8B/5B/SCM Render Fix — Handoff to the AD2 Session (2026-06-18)
## User
- **User:** Mike Swanson (mike)
- **Machine:** GURU-5070
- **Role:** admin
## Note for mike (AD2 session — datasheet-exact.js owner)
I drove the 8B/5B/SCM unpublished-render investigation from GURU-5070. Root cause is the
**same class as DSCA**, and finishing it needs **your existing DSCA machinery** — handing it over
rather than reimplementing in the file you're actively editing.
**What's done + committed (you'll have it on pull):**
- `projects/dataforth-dos/8b5bscm-templates.json`**136 models** mined from Hoffman originals
(accOut, accHeader, rows[name,spec], _srcSerial). Also on the box at
`C:\Shares\testdatadb\8b5bscm-templates.json`.
- Validated **general parseRawData fix**: a PASS/FAIL line is wrongly consumed as the step line
for non-DSCA families that omit the `"0","0",v` line (8B45/8B49/5B39/SCM5B33...). Change the
guard to `if (!((family === 'DSCA' || skipStepIfStatus) && looksLikeStatus))` and pass
`skipStepIfStatus` only for templated models (non-templated byte-unchanged).
- Proven the **Final-Test template branch generalizes** to 8B/5B/SCM (the 8 minimal patches are in
`8B5BSCM-RENDER-VERIFY-2026-06-18.md`). Stage+verify vs Hoffman (content-normalized): **8B avg
0.97, SCM5B 0.93**; 15 models content-perfect.
**What's left = your DSCA tools, applied to 8b5bscm-templates.json:**
1. **slotmaps** (`_derive_slotmaps.js`) — 70 models + most unpublished units render null on a
measurement-count mismatch; they need per-model slotMaps. THIS is the unlock for the ~5,148 units.
2. **Math.fround QB rounding** — closes ~17 precision-distance models.
3. **frequency/AAC accuracy renderer** — 8B45/8B49/5B45 (frequency) + SCM5B33 (AAC) need the
accuracy input/output labels + stimulus formatting the renderer can't do yet. **This is the SAME
unsolved code blocking your DSCA45/DSCA33** — solve once, covers both.
Recommend wiring 8b5bscm-templates into your template/slotmap/rounding path and finishing the
freq/AAC accuracy block once for DSCA + 8B/5B/SCM together. Full detail + the verify harness:
`projects/dataforth-dos/8B5BSCM-RENDER-VERIFY-2026-06-18.md`. I published the one already-renderable
unit (8B32-01 165669-15); everything else awaits the slotmaps above.
## Status
- 8B/5B/SCM unpublished render: diagnosed + 136 templates mined/committed; remaining work handed to
AD2 (slotmaps / QB rounding / freq-AAC accuracy). Live UI redesign deployed; /api/search sort + UI
presets/publish wiring done earlier today.

View File

@@ -12,7 +12,7 @@
--hover:#f1f5f9; --sel:#e0e7ff; --desk:#e7ecf1;
--mono:ui-monospace,"SFMono-Regular",Consolas,"Liberation Mono",monospace;
--sans:Inter,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
--r:6px; --insp:480px;
--r:6px; --insp:500px;
}
*{box-sizing:border-box}
html,body{height:100%;margin:0}
@@ -112,6 +112,10 @@
.pill{display:inline-block;font-size:10.5px;font-weight:700;padding:1px 8px;border-radius:4px;font-family:var(--mono)}
.pill.PASS{background:var(--pass-bg);color:var(--pass-ink)}
.pill.FAIL{background:var(--fail-bg);color:var(--fail-ink)}
.tag{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:2px 8px;border-radius:20px;font-family:var(--sans)}
.tag::before{content:"";width:7px;height:7px;border-radius:50%}
.tag.pub{background:var(--pass-bg);color:var(--pass-ink)} .tag.pub::before{background:var(--pass-ink)}
.tag.unpub{background:#fef3c7;color:#92400e} .tag.unpub::before{background:#d97706;background:none;box-shadow:inset 0 0 0 1.5px #d97706}
.web{font-size:13px}
.pager{display:flex;align-items:center;gap:10px;padding:7px 12px;border-top:1px solid var(--border);font-size:12px;color:var(--ink-2)}
.pager button{font-size:12px;height:28px;padding:0 11px;border:1px solid var(--border-strong);border-radius:var(--r);background:#fff;cursor:pointer;color:var(--ink)}
@@ -200,7 +204,7 @@
<div class="selbar" id="selbar">
<span id="selcount">0 selected</span>
<button id="copySel">Copy serials</button>
<button id="repushSel" disabled title="Re-publish to the website — needs an API endpoint">Re-push ▴</button>
<button id="repushSel" title="Publish / re-publish the selected serials to the public website">Re-push ▴</button>
<span class="sp" style="flex:1"></span>
<button id="selclear" style="border:0;background:none">clear</button>
</div>
@@ -240,7 +244,7 @@
const API='';
const $=id=>document.getElementById(id);
const state={q:'',serial:'',model:'',result:'',station:'',logtype:'',from:'',to:'',size:50,page:0,total:0,
selected:null,rows:[],sort:'',dir:'desc',checks:new Set(),force:''};
selected:null,rows:[],sort:'',dir:'desc',checks:new Set(),force:'',webStatus:''};
let timer=null, certTimer=null;
const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
const fmtDate=d=>d?String(d).slice(0,10):'';
@@ -265,7 +269,8 @@ $('mode').onclick=()=>{ const seq=['auto','serial','model','text']; state.force=
function params(forExport){
const p=new URLSearchParams();
for(const k of ['q','serial','model','result','station','logtype','from','to']) if(state[k]) p.set(k,state[k]);
if(state.sort){ p.set('sort',state.sort); p.set('dir',state.dir); } // needs API; harmless if ignored
if(state.sort){ p.set('sort',state.sort); p.set('dir',state.dir); } // honored by /api/search (whitelisted)
if(state.webStatus) p.set('web_status',state.webStatus); // on=published, off=not yet published
if(!forExport){ p.set('limit',state.size); p.set('offset',state.page*state.size); }
return p;
}
@@ -321,14 +326,14 @@ function select(id,auto){
<dt>Result</dt><dd><span class="pill ${r.overall_result}">${esc(r.overall_result)}</span></dd>
<dt>Log</dt><dd>${esc(r.log_type)}</dd>
${r.work_order?`<dt>WO</dt><dd>${esc(r.work_order)}</dd>`:''}
<dt>Web</dt><dd>${r.api_uploaded_at?'published '+fmtDate(r.api_uploaded_at):'not published'}</dd></dl>`;
<dt>Web</dt><dd>${r.api_uploaded_at?`<span class="tag pub">Published</span> <span style="color:var(--ink-3)">${fmtDate(r.api_uploaded_at)}</span>`:'<span class="tag unpub">Not published</span>'}</dd></dl>`;
const ds=API+'/api/datasheet/'+id;
$('acts').style.display='flex';
$('acts').innerHTML=`<a class="pri" href="${ds}?format=html" target="_blank">Open ↗</a>
<button onclick="printCert()">Print</button>
<a href="${ds}?format=txt" download="${esc(r.serial_number)}.txt">TXT</a>
<a href="${ds}?format=html" download="${esc(r.serial_number)}.html">HTML</a>
<button disabled title="Re-publish this cert — needs a POST /api/publish endpoint">Push to Web ▴</button>`;
<button onclick="pushWeb('${id}',this)" title="Publish / re-publish this cert to the public website">Push to Web ▴</button>`;
$('insp').classList.add('open');
// lazy-load the certificate so fast arrow/typing stays snappy
$('viewer').innerHTML='<div class="state"><div class="skel" style="width:60%"></div></div>';
@@ -338,18 +343,23 @@ function select(id,auto){
}
function loadCert(ds){
$('viewer').innerHTML='<iframe id="dsframe" title="datasheet"></iframe>';
const f=$('dsframe'); f.onload=()=>{styleCert();fitCert();}; f.src=ds+'?format=html';
const f=$('dsframe'); f.onload=()=>{styleCert();fitCert();setTimeout(fitCert,120);setTimeout(fitCert,350);}; f.src=ds+'?format=html';
}
function styleCert(){ const f=$('dsframe'); try{ const doc=f.contentDocument; if(!doc)return;
if(!doc.getElementById('_inj')){ const s=doc.createElement('style'); s.id='_inj';
s.textContent='html,body{background:#fff!important;margin:0!important}body{padding:22px 26px!important;color:#0f172a}pre{margin:0;font-family:'+getComputedStyle(document.body).getPropertyValue('--mono')+';font-size:12.5px;line-height:1.32}';
s.textContent='html,body{background:#fff!important;margin:0!important}body{padding:16px 20px!important;color:#0f172a}pre{margin:0;font-family:'+getComputedStyle(document.body).getPropertyValue('--mono')+';font-size:12.5px;line-height:1.32}';
(doc.head||doc.documentElement).appendChild(s); }
}catch(e){} }
function fitCert(){ const f=$('dsframe'); if(!f)return; try{ const doc=f.contentDocument; if(!doc)return;
const root=doc.documentElement; root.style.zoom='';
const nat=Math.max(doc.body?doc.body.scrollWidth:0,root.scrollWidth), av=f.clientWidth-2;
root.style.zoom=(nat>av)?Math.max(.45,av/nat):1;
f.style.height=Math.ceil((doc.body?doc.body.scrollHeight:600)*(nat>av?av/nat:1)+4)+'px';
function fitCert(){ const f=$('dsframe'); if(!f)return; try{ const doc=f.contentDocument; if(!doc||!doc.body)return;
const b=doc.body, root=doc.documentElement;
// measure natural content width (transform doesn't reflow, so text never rewraps)
b.style.transform='none'; b.style.width='max-content'; b.style.transformOrigin='0 0';
root.style.overflow='hidden';
const nat=Math.max(b.scrollWidth,root.scrollWidth), av=f.clientWidth-12; // widest line + right margin
if(!nat){ return; } // not laid out yet — the retry will catch it
const k=nat>av ? Math.max(.4, av/nat) : 1;
b.style.transform='scale('+k+')';
f.style.height=Math.ceil(b.scrollHeight*k+2)+'px'; // size the frame to the scaled cert, no v-scroll-in-frame
}catch(e){} }
function printCert(){ const f=$('dsframe'); if(f&&f.contentWindow){f.contentWindow.focus();f.contentWindow.print();} }
window.addEventListener('resize',()=>{fitCert();});
@@ -363,6 +373,31 @@ $('copySel').onclick=()=>{ const sns=state.rows.filter(r=>state.checks.has(Strin
navigator.clipboard&&navigator.clipboard.writeText(sns); $('copySel').textContent='Copied ✓'; setTimeout(()=>$('copySel').textContent='Copy serials',1200); };
$('selclear').onclick=()=>{ state.checks.clear(); [...$('rows').querySelectorAll('[data-ck]')].forEach(c=>c.checked=false); $('ckAll').checked=false; updateSel(); };
/* ---------- publish to public website (POST /api/upload, idempotent) ---------- */
async function doUpload(payload){
const r=await fetch(API+'/api/upload',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const d=await r.json().catch(()=>({})); if(!r.ok) throw new Error(d.error||('HTTP '+r.status)); return d;
}
async function pushWeb(id,btn){
const r=state.rows.find(x=>x.id==id); const sn=r?r.serial_number:id;
if(!confirm('Publish '+sn+' to the public Dataforth website now?')) return;
const t=btn.textContent; btn.disabled=true; btn.textContent='Publishing…';
try{ const d=await doUpload({ids:[+id]});
btn.textContent=d.errors?('✕ '+d.errors+' err'):(d.skipped&&!(d.created+d.updated+d.unchanged)?'skipped':'Published ✓');
setTimeout(search,500); // refresh WEB status from the DB
}catch(e){ btn.textContent='✕ '+e.message.slice(0,16); btn.disabled=false; }
}
async function pushSelected(){
const ids=[...state.checks].map(Number); if(!ids.length) return;
if(!confirm('Publish '+ids.length+' selected serial(s) to the public Dataforth website now?')) return;
const b=$('repushSel'),t=b.textContent; b.disabled=true; b.textContent='Publishing…';
try{ const d=await doUpload({ids});
b.textContent='✓ '+((d.created||0)+(d.updated||0))+' pushed'+(d.skipped?(' · '+d.skipped+' skip'):'');
setTimeout(()=>{b.textContent=t;b.disabled=false;search();},1400);
}catch(e){ b.textContent='✕ failed'; b.disabled=false; alert('Push failed: '+e.message); }
}
$('repushSel').onclick=pushSelected;
/* ---------- sort ---------- */
function updateSortHeads(){ [...document.querySelectorAll('thead th.s')].forEach(th=>{
const on=th.dataset.s===state.sort; th.querySelector('.arr')?.remove();
@@ -385,12 +420,12 @@ $('pageSize').onchange=e=>{state.size=+e.target.value;state.page=0;search();};
$('prev').onclick=()=>{if(state.page>0){state.page--;search();$('twrap').scrollTop=0;}};
$('next').onclick=()=>{state.page++;search();$('twrap').scrollTop=0;};
$('reset').onclick=()=>{ ['serial','model','q','result','station','logtype','from','to'].forEach(k=>state[k]='');
state.sort='';state.checks.clear();updateSel(); $('omni').value='';$('mode').textContent=state.force||'auto';
state.sort='';state.webStatus='';state.checks.clear();updateSel(); $('omni').value='';$('mode').textContent=state.force||'auto';
$('fFrom').value=$('fTo').value=$('fStation').value=$('fLog').value=$('fModel').value=''; state.page=0; search(); };
$('menu').onclick=()=>document.body.classList.toggle('rail-open');
/* ---------- presets ---------- */
function clearAll(){ ['serial','model','q','result','station','logtype','from','to'].forEach(k=>state[k]='');
function clearAll(){ ['serial','model','q','result','station','logtype','from','to'].forEach(k=>state[k]=''); state.webStatus='';state.sort='';
$('omni').value='';$('mode').textContent=state.force||'auto';$('fFrom').value=$('fTo').value=$('fStation').value=$('fLog').value=$('fModel').value=''; }
function applyPreset(fn){ clearAll(); fn(); state.page=0; state.checks.clear(); updateSel(); document.body.classList.remove('rail-open'); search(); }
const PRESETS=[
@@ -398,11 +433,11 @@ const PRESETS=[
{ic:'✕',label:'Failures',fn:()=>{state.result='FAIL';}},
{ic:'•',label:'Today',fn:()=>{const t=isoD(new Date());state.from=t;state.to=t;}},
{ic:'7',label:'Last 7 days',fn:()=>{const d=new Date();d.setDate(d.getDate()-7);state.from=isoD(d);}},
{ic:'▴',label:'Latest uploads',fn:()=>{state.webStatus='on';state.sort='api_uploaded_at';state.dir='desc';}},
{ic:'○',label:'Not yet published',fn:()=>{state.webStatus='off';}},
];
const SOON=[
{label:'Latest upload batch',why:'needs sort=uploaded in /api/search'},
{label:'Retested units',why:'needs a retest flag in the pipeline'},
{label:'Not yet published',why:'needs a published filter in /api/search'},
{label:'Retested units',why:'needs a retest flag in the pipeline (next)'},
];
function renderPresets(){
$('presets').innerHTML='';

View File

@@ -1,27 +1,17 @@
/**
* API Routes for Test Data Database
*
* Fixed version - uses a single persistent database connection instead of
* opening and closing on every request. WAL journal mode enabled for
* concurrent read support. Limit parameter capped at 1000.
* PostgreSQL version - uses pg.Pool via database/db.js.
* All route handlers are async. FTS uses tsvector/plainto_tsquery.
*/
const express = require('express');
const path = require('path');
const Database = require('better-sqlite3');
const db = require('../database/db');
const { generateDatasheet } = require('../templates/datasheet');
const router = express.Router();
// ---------------------------------------------------------------------------
// Singleton database connection - opened once at module load
// ---------------------------------------------------------------------------
const DB_PATH = path.join(__dirname, '..', 'database', 'testdata.db');
const db = new Database(DB_PATH, { readonly: false });
db.pragma('journal_mode = WAL');
db.pragma('busy_timeout = 5000');
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
@@ -42,107 +32,88 @@ function clampOffset(value) {
// ---------------------------------------------------------------------------
// GET /api/search
// Search test records
// Query params: serial, model, from, to, result, q, station, logtype, limit, offset
// Query params: serial, model, from, to, result, q, station, logtype, web_status, limit, offset
// ---------------------------------------------------------------------------
router.get('/search', (req, res) => {
router.get('/search', async (req, res) => {
try {
const { serial, model, from, to, result, q, station, logtype } = req.query;
const { serial, model, from, to, result, q, station, logtype, workorder, web_status } = req.query;
const limit = clampLimit(req.query.limit || 100);
const offset = clampOffset(req.query.offset || 0);
let sql = 'SELECT * FROM test_records WHERE 1=1';
const conditions = [];
const params = [];
let paramIdx = 0;
const addParam = (val) => {
paramIdx++;
params.push(val);
return '$' + paramIdx;
};
if (q) {
// Full-text search using tsvector
conditions.push(`search_vector @@ plainto_tsquery('english', ${addParam(q)})`);
}
if (serial) {
sql += ' AND serial_number LIKE ?';
params.push(serial.includes('%') ? serial : `%${serial}%`);
const val = serial.includes('%') ? serial : `%${serial}%`;
conditions.push(`serial_number LIKE ${addParam(val)}`);
}
if (workorder) {
conditions.push(`work_order = ${addParam(workorder)}`);
}
if (model) {
sql += ' AND model_number LIKE ?';
params.push(model.includes('%') ? model : `%${model}%`);
const val = model.includes('%') ? model : `%${model}%`;
conditions.push(`model_number LIKE ${addParam(val)}`);
}
if (from) {
sql += ' AND test_date >= ?';
params.push(from);
conditions.push(`test_date >= ${addParam(from)}`);
}
if (to) {
sql += ' AND test_date <= ?';
params.push(to);
conditions.push(`test_date <= ${addParam(to)}`);
}
if (result) {
sql += ' AND overall_result = ?';
params.push(result.toUpperCase());
conditions.push(`overall_result = ${addParam(result.toUpperCase())}`);
}
if (station) {
sql += ' AND test_station = ?';
params.push(station);
conditions.push(`test_station = ${addParam(station)}`);
}
if (logtype) {
sql += ' AND log_type = ?';
params.push(logtype);
conditions.push(`log_type = ${addParam(logtype)}`);
}
if (q) {
// Full-text search - rebuild query with FTS
sql = `SELECT test_records.* FROM test_records
JOIN test_records_fts ON test_records.id = test_records_fts.rowid
WHERE test_records_fts MATCH ?`;
params.length = 0;
params.push(q);
if (serial) {
sql += ' AND serial_number LIKE ?';
params.push(serial.includes('%') ? serial : `%${serial}%`);
}
if (model) {
sql += ' AND model_number LIKE ?';
params.push(model.includes('%') ? model : `%${model}%`);
}
if (station) {
sql += ' AND test_station = ?';
params.push(station);
}
if (logtype) {
sql += ' AND log_type = ?';
params.push(logtype);
}
if (result) {
sql += ' AND overall_result = ?';
params.push(result.toUpperCase());
}
if (from) {
sql += ' AND test_date >= ?';
params.push(from);
}
if (to) {
sql += ' AND test_date <= ?';
params.push(to);
}
if (req.query.web_status === 'off') {
conditions.push('api_uploaded_at IS NULL');
} else if (req.query.web_status === 'on') {
conditions.push('api_uploaded_at IS NOT NULL');
}
sql += ' ORDER BY test_date DESC, serial_number';
sql += ' LIMIT ? OFFSET ?';
params.push(limit, offset);
const where = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
const records = db.prepare(sql).all(...params);
// whitelisted sort (prevents injection); NULLS LAST so e.g. unpublished rows don't lead an api_uploaded_at sort
const SORTABLE = { serial_number:'serial_number', model_number:'model_number', test_date:'test_date', overall_result:'overall_result', test_station:'test_station', log_type:'log_type', api_uploaded_at:'api_uploaded_at' };
const sortCol = SORTABLE[req.query.sort];
const sortDir = String(req.query.dir || '').toLowerCase() === 'asc' ? 'ASC' : 'DESC';
const orderBy = sortCol ? `ORDER BY ${sortCol} ${sortDir} NULLS LAST, serial_number` : 'ORDER BY test_date DESC, serial_number';
const dataSql = `SELECT * FROM test_records ${where} ${orderBy} LIMIT ${addParam(limit)} OFFSET ${addParam(offset)}`;
const countSql = `SELECT COUNT(*) as count FROM test_records ${where}`;
const countParams = params.slice(0, paramIdx - 2); // exclude limit/offset
// Get total count
let countSql = sql.replace(/SELECT .* FROM/, 'SELECT COUNT(*) as count FROM')
.replace(/ORDER BY.*$/, '');
countSql = countSql.replace(/LIMIT \? OFFSET \?/, '');
const countParams = params.slice(0, -2);
const total = db.prepare(countSql).get(...countParams);
const [records, countRow] = await Promise.all([
db.query(dataSql, params),
db.queryOne(countSql, countParams),
]);
res.json({
records,
total: total?.count || records.length,
total: countRow?.count ? parseInt(countRow.count, 10) : records.length,
limit,
offset
});
@@ -156,9 +127,9 @@ router.get('/search', (req, res) => {
// GET /api/record/:id
// Get single record by ID
// ---------------------------------------------------------------------------
router.get('/record/:id', (req, res) => {
router.get('/record/:id', async (req, res) => {
try {
const record = db.prepare('SELECT * FROM test_records WHERE id = ?').get(req.params.id);
const record = await db.queryOne('SELECT * FROM test_records WHERE id = $1', [req.params.id]);
if (!record) {
return res.status(404).json({ error: 'Record not found' });
@@ -176,21 +147,102 @@ router.get('/record/:id', (req, res) => {
// Generate datasheet for a record
// Query params: format (html, txt)
// ---------------------------------------------------------------------------
router.get('/datasheet/:id', (req, res) => {
router.get('/datasheet/:id', async (req, res) => {
try {
const record = db.prepare('SELECT * FROM test_records WHERE id = ?').get(req.params.id);
const record = await db.queryOne('SELECT * FROM test_records WHERE id = $1', [req.params.id]);
if (!record) {
return res.status(404).json({ error: 'Record not found' });
}
const format = req.query.format || 'html';
const datasheet = generateDatasheet(record, format);
if (format === 'html') {
res.type('html').send(datasheet);
// Try exact-match formatter first
const { loadAllSpecs, getSpecs } = require('../parsers/spec-reader');
const { generateExactDatasheet } = require('../templates/datasheet-exact');
const specMap = loadAllSpecs();
const specs = getSpecs(specMap, record.model_number);
const exactTxt = generateExactDatasheet(record, specs);
if (exactTxt && format === 'html') {
// Render exact-match TXT as styled HTML page
const escaped = exactTxt
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
const html = `<!DOCTYPE html>
<html>
<head>
<title>Test Data Sheet - ${record.serial_number}</title>
<style>
body {
margin: 0;
padding: 20px;
background: #f0f0f0;
display: flex;
justify-content: center;
}
.page {
background: white;
padding: 40px 30px;
max-width: 720px;
width: 100%;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
border: 1px solid #ccc;
}
pre {
font-family: 'Courier New', Courier, monospace;
font-size: 11px;
line-height: 1.4;
margin: 0;
white-space: pre;
overflow-x: auto;
}
.toolbar {
position: fixed;
top: 10px;
right: 10px;
display: flex;
gap: 8px;
}
.toolbar button {
padding: 8px 16px;
border: 1px solid #999;
background: white;
cursor: pointer;
font-size: 13px;
border-radius: 4px;
}
.toolbar button:hover { background: #e0e0e0; }
@media print {
body { background: white; padding: 0; }
.page { box-shadow: none; border: none; padding: 0; }
.toolbar { display: none; }
}
</style>
</head>
<body>
<div class="toolbar">
<button onclick="window.print()">Print</button>
<button onclick="window.open('/api/datasheet/${record.id}/pdf')">Download PDF</button>
<button onclick="window.close()">Close</button>
</div>
<div class="page">
<pre>${escaped}</pre>
</div>
</body>
</html>`;
res.type('html').send(html);
} else if (exactTxt && format === 'txt') {
res.type('text/plain').send(exactTxt);
} else {
res.type('text/plain').send(datasheet);
// Fall back to generic template
const datasheet = generateDatasheet(record, format);
if (format === 'html') {
res.type('html').send(datasheet);
} else {
res.type('text/plain').send(datasheet);
}
}
} catch (err) {
console.error(`[${new Date().toISOString()}] [DATASHEET ERROR] ${err.message}`);
@@ -198,45 +250,83 @@ router.get('/datasheet/:id', (req, res) => {
}
});
// ---------------------------------------------------------------------------
// GET /api/datasheet/:id/pdf
// Generate PDF datasheet for a record (on-demand download)
// ---------------------------------------------------------------------------
router.get('/datasheet/:id/pdf', async (req, res) => {
try {
const record = await db.queryOne('SELECT * FROM test_records WHERE id = $1', [req.params.id]);
if (!record) {
return res.status(404).json({ error: 'Record not found' });
}
const { loadAllSpecs, getSpecs } = require('../parsers/spec-reader');
const { generateExactDatasheet } = require('../templates/datasheet-exact');
const PDFDocument = require('pdfkit');
const specMap = loadAllSpecs();
const specs = getSpecs(specMap, record.model_number);
let txt = generateExactDatasheet(record, specs);
// Fall back to generic datasheet if exact-match formatter doesn't support this family
if (!txt) {
txt = generateDatasheet(record, 'txt');
}
if (!txt) {
return res.status(422).json({ error: 'Could not generate datasheet (missing specs or data)' });
}
const doc = new PDFDocument({
size: 'LETTER',
margins: { top: 36, bottom: 36, left: 36, right: 36 }
});
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="${record.serial_number}.pdf"`);
doc.pipe(res);
doc.font('Courier').fontSize(9.5);
const lines = txt.split(/\r?\n/);
for (const line of lines) {
doc.text(line, { lineGap: 1 });
}
doc.end();
} catch (err) {
console.error(`[${new Date().toISOString()}] [PDF ERROR] ${err.message}`);
res.status(500).json({ error: err.message });
}
});
// ---------------------------------------------------------------------------
// GET /api/stats
// Get database statistics
// ---------------------------------------------------------------------------
router.get('/stats', (req, res) => {
router.get('/stats', async (req, res) => {
try {
const stats = {
total_records: db.prepare('SELECT COUNT(*) as count FROM test_records').get().count,
by_log_type: db.prepare(`
SELECT log_type, COUNT(*) as count
FROM test_records
GROUP BY log_type
ORDER BY count DESC
`).all(),
by_result: db.prepare(`
SELECT overall_result, COUNT(*) as count
FROM test_records
GROUP BY overall_result
`).all(),
by_station: db.prepare(`
SELECT test_station, COUNT(*) as count
FROM test_records
WHERE test_station IS NOT NULL AND test_station != ''
GROUP BY test_station
ORDER BY test_station
`).all(),
date_range: db.prepare(`
SELECT MIN(test_date) as oldest, MAX(test_date) as newest
FROM test_records
`).get(),
recent_serials: db.prepare(`
SELECT DISTINCT serial_number, model_number, test_date
FROM test_records
ORDER BY test_date DESC
LIMIT 10
`).all()
};
const [totalRow, byLogType, byResult, byStation, dateRange, recentSerials] = await Promise.all([
db.queryOne('SELECT COUNT(*) as count FROM test_records'),
db.query('SELECT log_type, COUNT(*) as count FROM test_records GROUP BY log_type ORDER BY count DESC'),
db.query('SELECT overall_result, COUNT(*) as count FROM test_records GROUP BY overall_result'),
db.query(`SELECT test_station, COUNT(*) as count FROM test_records
WHERE test_station IS NOT NULL AND test_station != ''
GROUP BY test_station ORDER BY test_station`),
db.queryOne('SELECT MIN(test_date) as oldest, MAX(test_date) as newest FROM test_records'),
db.query(`SELECT DISTINCT serial_number, model_number, test_date
FROM test_records ORDER BY test_date DESC LIMIT 10`),
]);
res.json(stats);
res.json({
total_records: parseInt(totalRow.count, 10),
by_log_type: byLogType.map(r => ({ ...r, count: parseInt(r.count, 10) })),
by_result: byResult.map(r => ({ ...r, count: parseInt(r.count, 10) })),
by_station: byStation.map(r => ({ ...r, count: parseInt(r.count, 10) })),
date_range: dateRange,
recent_serials: recentSerials,
});
} catch (err) {
console.error(`[${new Date().toISOString()}] [STATS ERROR] ${err.message}`);
res.status(500).json({ error: err.message });
@@ -247,30 +337,22 @@ router.get('/stats', (req, res) => {
// GET /api/filters
// Get available filter options (test stations, log types, models)
// ---------------------------------------------------------------------------
router.get('/filters', (req, res) => {
router.get('/filters', async (req, res) => {
try {
const filters = {
stations: db.prepare(`
SELECT DISTINCT test_station
FROM test_records
WHERE test_station IS NOT NULL AND test_station != ''
ORDER BY test_station
`).all().map(r => r.test_station),
log_types: db.prepare(`
SELECT DISTINCT log_type
FROM test_records
ORDER BY log_type
`).all().map(r => r.log_type),
models: db.prepare(`
SELECT DISTINCT model_number, COUNT(*) as count
FROM test_records
GROUP BY model_number
ORDER BY count DESC
LIMIT 500
`).all()
};
const [stations, logTypes, models] = await Promise.all([
db.query(`SELECT DISTINCT test_station FROM test_records
WHERE test_station IS NOT NULL AND test_station != ''
ORDER BY test_station`),
db.query('SELECT DISTINCT log_type FROM test_records ORDER BY log_type'),
db.query(`SELECT DISTINCT model_number, COUNT(*) as count FROM test_records
GROUP BY model_number ORDER BY count DESC LIMIT 500`),
]);
res.json(filters);
res.json({
stations: stations.map(r => r.test_station),
log_types: logTypes.map(r => r.log_type),
models: models.map(r => ({ ...r, count: parseInt(r.count, 10) })),
});
} catch (err) {
console.error(`[${new Date().toISOString()}] [FILTERS ERROR] ${err.message}`);
res.status(500).json({ error: err.message });
@@ -281,51 +363,60 @@ router.get('/filters', (req, res) => {
// GET /api/export
// Export search results as CSV
// ---------------------------------------------------------------------------
router.get('/export', (req, res) => {
router.get('/export', async (req, res) => {
try {
const { serial, model, from, to, result, station, logtype } = req.query;
let sql = 'SELECT * FROM test_records WHERE 1=1';
const conditions = [];
const params = [];
let paramIdx = 0;
const addParam = (val) => {
paramIdx++;
params.push(val);
return '$' + paramIdx;
};
if (serial) {
sql += ' AND serial_number LIKE ?';
params.push(serial.includes('%') ? serial : `%${serial}%`);
const val = serial.includes('%') ? serial : `%${serial}%`;
conditions.push(`serial_number LIKE ${addParam(val)}`);
}
if (model) {
sql += ' AND model_number LIKE ?';
params.push(model.includes('%') ? model : `%${model}%`);
const val = model.includes('%') ? model : `%${model}%`;
conditions.push(`model_number LIKE ${addParam(val)}`);
}
if (from) {
sql += ' AND test_date >= ?';
params.push(from);
conditions.push(`test_date >= ${addParam(from)}`);
}
if (to) {
sql += ' AND test_date <= ?';
params.push(to);
conditions.push(`test_date <= ${addParam(to)}`);
}
if (result) {
sql += ' AND overall_result = ?';
params.push(result.toUpperCase());
conditions.push(`overall_result = ${addParam(result.toUpperCase())}`);
}
if (station) {
sql += ' AND test_station = ?';
params.push(station);
conditions.push(`test_station = ${addParam(station)}`);
}
if (logtype) {
sql += ' AND log_type = ?';
params.push(logtype);
conditions.push(`log_type = ${addParam(logtype)}`);
}
sql += ' ORDER BY test_date DESC, serial_number LIMIT 10000';
if (req.query.web_status === 'off') {
conditions.push('api_uploaded_at IS NULL');
} else if (req.query.web_status === 'on') {
conditions.push('api_uploaded_at IS NOT NULL');
}
const records = db.prepare(sql).all(...params);
const where = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
const sql = `SELECT * FROM test_records ${where} ORDER BY test_date DESC, serial_number LIMIT 10000`;
const records = await db.query(sql, params);
// Generate CSV
const headers = ['id', 'log_type', 'model_number', 'serial_number', 'test_date', 'test_station', 'overall_result', 'source_file'];
@@ -348,16 +439,119 @@ router.get('/export', (req, res) => {
}
});
// ---------------------------------------------------------------------------
// GET /api/workorder/:wo
// Get work order details and all associated test lines
// ---------------------------------------------------------------------------
router.get('/workorder/:wo', async (req, res) => {
try {
const wo = req.params.wo;
const [header, lines, testRecords] = await Promise.all([
db.queryOne('SELECT * FROM work_orders WHERE wo_number = $1', [wo]),
db.query('SELECT * FROM work_order_lines WHERE wo_number = $1 ORDER BY test_date, test_time', [wo]),
db.query(
'SELECT id, log_type, model_number, serial_number, test_date, test_station, overall_result, work_order FROM test_records WHERE work_order = $1 ORDER BY serial_number',
[wo]
),
]);
res.json({
work_order: header || { wo_number: wo },
lines,
test_records: testRecords,
});
} catch (err) {
console.error(`[${new Date().toISOString()}] [WO ERROR] ${err.message}`);
res.status(500).json({ error: err.message });
}
});
// ---------------------------------------------------------------------------
// GET /api/workorder-search?q=<query>
// Search work orders by number (prefix match)
// ---------------------------------------------------------------------------
router.get('/workorder-search', async (req, res) => {
try {
const q = req.query.q || '';
if (q.length < 2) {
return res.json({ results: [] });
}
const results = await db.query(
'SELECT wo_number, wo_date, program, test_station FROM work_orders WHERE wo_number LIKE $1 ORDER BY wo_date DESC LIMIT 50',
[q + '%']
);
res.json({ results });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ---------------------------------------------------------------------------
// Cleanup function for graceful shutdown
// ---------------------------------------------------------------------------
function cleanup() {
async function cleanup() {
try {
db.close();
await db.close();
} catch (err) {
console.error(`[${new Date().toISOString()}] [CLEANUP ERROR] ${err.message}`);
}
}
/**
* POST /api/upload
*
* Body: { ids?: number[], serialNumbers?: string[], all_unuploaded?: boolean }
*
* Pushes selected records to the Dataforth website API. Accepts either a set
* of record IDs (resolved to serial_number + checked for exported status), a
* direct list of serial numbers, or all_unuploaded:true to push every PASS
* record where api_uploaded_at IS NULL.
*
* Response: { created, updated, unchanged, errors, skipped, processed, sns }
*/
router.post('/upload', async (req, res) => {
try {
const { ids, serialNumbers, all_unuploaded } = req.body || {};
const { uploadBySerialNumbers } = require('../database/upload-to-api');
let sns = [];
if (all_unuploaded) {
const rows = await db.query(
`SELECT DISTINCT serial_number FROM test_records
WHERE overall_result = 'PASS'
AND api_uploaded_at IS NULL
ORDER BY serial_number`
);
sns = rows.map(r => r.serial_number);
} else if (Array.isArray(ids) && ids.length > 0) {
const placeholders = ids.map((_, i) => `$${i + 1}`).join(',');
const rows = await db.query(
`SELECT DISTINCT serial_number FROM test_records
WHERE id IN (${placeholders})
AND overall_result = 'PASS'`,
ids,
);
sns = rows.map(r => r.serial_number);
} else if (Array.isArray(serialNumbers) && serialNumbers.length > 0) {
sns = [...new Set(serialNumbers)];
} else {
return res.status(400).json({ error: 'provide ids[], serialNumbers[], or all_unuploaded=true' });
}
if (sns.length === 0) {
return res.json({ created:0, updated:0, unchanged:0, errors:0, skipped:0, processed:0, sns:[] });
}
const result = await uploadBySerialNumbers(sns);
res.json({ ...result, processed: sns.length, sns });
} catch (err) {
console.error(`[UPLOAD] ${err.message}`);
res.status(500).json({ error: err.message });
}
});
module.exports = router;
module.exports.cleanup = cleanup;

View File

@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""
Mine per-model DSCA33/DSCA45 Final-Test templates from the ORIGINAL certs stored
on Dataforth's Hoffman API (the spec files lost in the cryptolocker event are
recoverable here because the original software published these before the wipe).
Input : a JSON map [{"m": model, "s": serial}, ...] of UPLOADED serials.
Output: dsca33-45-templates.json (schema-compatible with dsca-templates.json:
{ model: { "accOut": "...", "rows": [ {"name","spec"}, ... ] } })
+ a human report on stdout.
Same extraction as the STAGE-1 extractor: the '===' rule under the Final-Test
"Parameter ... Measured" header gives exact column spans; name = Parameter col,
spec = Specification col. Keeps the richest sheet (most rows) per model.
"""
import json, re, sys, time, urllib.request, urllib.parse, os
TOKEN_URL = "https://login.dataforth.com/connect/token"
API_BASE = "https://www.dataforth.com"
CID, CSEC, SCOPE = "dataforth.onprem.sync", "Trxvwee2234-Awer8723-2", "dataforth.web"
def get_token():
body = urllib.parse.urlencode({
"grant_type": "client_credentials", "client_id": CID,
"client_secret": CSEC, "scope": SCOPE}).encode()
req = urllib.request.Request(TOKEN_URL, body,
{"Content-Type": "application/x-www-form-urlencoded"})
return json.loads(urllib.request.urlopen(req, timeout=30).read())["access_token"]
def get_cert(serial, tok):
url = f"{API_BASE}/api/v1/TestReportDataFiles/{urllib.parse.quote(serial)}"
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {tok}"})
try:
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())
except urllib.error.HTTPError as e:
if e.code == 404: return None
raise
def col_spans(sep):
return [(m.start(), m.end()) for m in re.finditer(r"=+", sep)]
def extract(t):
lines = t.replace("\r\n", "\n").split("\n")
ahi = next((i for i, l in enumerate(lines)
if "Error (%)" in l and "Status" in l), -1)
acc_hdr = lines[ahi] if ahi >= 0 else ""
# capture the verbatim 2-line accuracy header (super-header + column line) so
# AD2 can reproduce the model-specific input label + VDC/mADC/Hz headers exactly
acc_header = [lines[ahi - 1].rstrip(), lines[ahi].rstrip()] if ahi > 0 else []
m = re.search(r"Output \([^)]*\)|Vout \([^)]*\)", acc_hdr)
acc_out = m.group(0) if m else "?"
fi = next((i for i, l in enumerate(lines) if "FINAL TEST RESULTS" in l), -1)
if fi < 0: return None
hi = next((i for i in range(fi + 1, len(lines))
if re.search(r"Parameter\s+Measured", lines[i])), -1)
if hi < 0: return None
sep = lines[hi + 1] if hi + 1 < len(lines) else ""
if "=" not in sep: return None
cols = col_spans(sep)
if len(cols) < 4: return None
pc, mc, sc, stc = cols[0], cols[1], cols[2], cols[3]
rows = []
for i in range(hi + 2, len(lines)):
l = lines[i]
if re.search(r"Check List|^\s*_{5,}", l): break
if not l.strip(): continue
name = l[pc[0]:mc[0]].strip()
spec = l[sc[0]:stc[0]].strip()
if not name and not spec: continue
rows.append({"name": name, "spec": spec})
return {"accOut": acc_out, "rows": rows, "accHdr": acc_hdr.strip(),
"accHeader": acc_header}
def main():
mp = json.load(open(sys.argv[1]))
outpath = sys.argv[2]
tok = get_token()
by_model = {} # model -> best {accOut, rows, accHdr, serial}
meta = {} # model -> diagnostics
missing = []
for row in mp:
model, serial = row["m"], row["s"]
cert = get_cert(serial, tok)
if not cert or not cert.get("Content"):
missing.append((model, serial)); continue
tpl = extract(cert["Content"])
if not tpl:
meta.setdefault(model, {}).setdefault("noextract", []).append(serial); continue
cur = by_model.get(model)
if not cur or len(tpl["rows"]) > len(cur["rows"]):
tpl["serial"] = serial
by_model[model] = tpl
# build schema-compatible output
out = {}
for model in sorted(by_model):
t = by_model[model]
out[model] = {"accOut": t["accOut"], "accHeader": t["accHeader"],
"rows": t["rows"], "_srcSerial": t["serial"]}
with open(outpath, "w") as f:
json.dump(out, f, indent=0)
# report
fams = {}
print(f"=== Mined {len(out)} models from Hoffman -> {outpath} ===\n")
print(f"{'MODEL':<14} {'rows':>4} {'accOut':<16} src-serial accuracy-header")
for model in sorted(out):
t = by_model[model]
fam = model.split("-")[0]
fams[fam] = fams.get(fam, 0) + 1
flag = " <-- LOW" if len(t["rows"]) < 3 else ""
print(f"{model:<14} {len(t['rows']):>4} {t['accOut']:<16} {t['serial']:<11} {t['accHdr'][:60]}{flag}")
print("\nper-family models mined:", dict(fams))
distinct_accout = sorted(set(o["accOut"] for o in out.values()))
print("distinct accOut tokens:", distinct_accout)
if missing:
print(f"\n[WARN] {len(missing)} serials returned 404 (not on Hoffman):",
missing[:10], "..." if len(missing) > 10 else "")
no_tpl = [m for m in {r['m'] for r in mp} if m not in out]
if no_tpl:
print(f"\n[WARN] models with NO usable template ({len(no_tpl)}):", no_tpl)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""
Same-origin preview proxy for the testdatadb front-end.
Serves the static prototype from ROOT and reverse-proxies /api/* to the live
AD2 testdatadb server, so the app AND the cert iframe share one origin
(http://127.0.0.1:PORT). That lets the same-origin-only cert styling/fit logic
(styleCert/fitCert read iframe.contentDocument) actually run during preview —
which it can't when the iframe is loaded cross-origin straight from AD2.
Usage: python preview-proxy.py <port> <root-dir> [target]
"""
import http.server, socketserver, urllib.request, urllib.error, os, sys
PORT = int(sys.argv[1])
ROOT = os.path.abspath(sys.argv[2]) if len(sys.argv) > 2 else "."
TARGET = sys.argv[3] if len(sys.argv) > 3 else "http://192.168.0.6:3000"
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path.startswith("/api/"):
try:
with urllib.request.urlopen(TARGET + self.path, timeout=30) as r:
body = r.read(); ct = r.headers.get("Content-Type", "application/octet-stream")
self._send(200, ct, body)
except urllib.error.HTTPError as e:
self._send(e.code, "application/json", e.read())
except Exception as e:
self._send(502, "text/plain", str(e).encode())
else:
p = self.path.split("?")[0]
p = "/index.html" if p == "/" else p
fp = os.path.join(ROOT, p.lstrip("/"))
if os.path.isfile(fp):
ct = "text/html; charset=utf-8" if fp.endswith(".html") else "application/octet-stream"
self._send(200, ct, open(fp, "rb").read())
else:
self._send(404, "text/plain", b"not found")
def do_POST(self):
if self.path.startswith("/api/"):
n = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(n) if n else b""
req = urllib.request.Request(
TARGET + self.path, data=body, method="POST",
headers={"Content-Type": self.headers.get("Content-Type", "application/json")})
try:
with urllib.request.urlopen(req, timeout=120) as r:
self._send(200, r.headers.get("Content-Type", "application/json"), r.read())
except urllib.error.HTTPError as e:
self._send(e.code, "application/json", e.read())
except Exception as e:
self._send(502, "text/plain", str(e).encode())
else:
self._send(404, "text/plain", b"not found")
def _send(self, code, ct, body):
self.send_response(code)
self.send_header("Content-Type", ct)
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
def log_message(self, *a): pass
class S(socketserver.ThreadingTCPServer):
allow_reuse_address = True
print(f"preview proxy: http://127.0.0.1:{PORT} root={ROOT} -> {TARGET}")
S(("127.0.0.1", PORT), H).serve_forever()

View File

@@ -0,0 +1,161 @@
## User
- **User:** Mike Swanson (mike)
- **Machine:** Mikes-MacBook-Air
- **Role:** admin
## Session Summary
Continued ACG website redesign from Phase 3A, implementing Phase 3B enhancements based on Grok's review recommendations. Deployed radio show promotion elements, 3-step visual funnel, strengthened calculator CTAs, and increased vertical rhythm for better layout density. All changes deployed to ww9.azcomputerguru.com and verified live.
Pulled new curated brand assets from Gitea (commit 311a45a) containing style guide, logos, icons, fonts, and letterhead. The brand kit was trimmed from 34M to 14M, removing legacy materials while keeping forward-relevant assets.
Onboarded Patriot Internal Medicine (PIM) to GuruRMM with two locations: Tucson and Sonoita. Created client, provisioned both sites, vaulted enrollment keys, and posted alerts to #dev-alerts.
Responded to user question about pfSense shell ping permission denied error, providing diagnostic steps for shell user permissions, ping binary permissions, firewall rules, and gateway/interface issues.
## Key Decisions
- **Radio promo placement:** Added both header badge (LIVE with subtle pulse) and under-hero promo bar for maximum visibility without overwhelming the design
- **Funnel positioning:** Placed 3-step funnel after Trust section to guide visitor→lead→sale flow early in the page journey
- **Calculator CTA strategy:** Made "Build your exact price" the primary button in pricing teaser, and added inline calculator links to all 6 service descriptions to drive traffic from every service mention
- **Vertical rhythm approach:** Used responsive `clamp(2.75rem, 5.5vw, 4.25rem)` for section padding instead of fixed values, with additional margin-bottom on dense grids
- **Vault naming convention:** Used `gururmm-site-tucson.sops.yaml` and `gururmm-site-sonoita.sops.yaml` for clarity (rather than site-main) since client has multiple locations
## Problems Encountered
**Problem 1: Initial RMM API connectivity failures**
- **Symptom:** Empty responses and timeout errors when creating sites
- **Root cause:** Shell session environment variables ($TOKEN, $RMM) not persisting between Bash tool calls
- **Resolution:** Used `eval "$(bash .claude/scripts/rmm-auth.sh)"` at the start of each compound operation to ensure fresh auth state
**Problem 2: Gemini review tool called with URL instead of file path**
- **Symptom:** `agy review http://ww9.azcomputerguru.com` failed with file not found
- **Root cause:** Gemini's review mode expects file paths, not URLs
- **Resolution:** Used Grok's text mode instead after fetching live HTML/CSS/JS from ww9 to construct comprehensive review prompt
## Configuration Changes
**Files Modified:**
- `projects/acg-website-showcase/multipage/index.html` - Added radio promo bar, 3-step funnel, strengthened CTAs, service calculator links
- `projects/acg-website-showcase/multipage/css/styles.css` - Added radio badge pulse animation, funnel styling, increased section padding, service link styles
**Files Created:**
- `clients/patriot-internal-medicine/gururmm-site-tucson.sops.yaml` (vault)
- `clients/patriot-internal-medicine/gururmm-site-sonoita.sops.yaml` (vault)
**Commits:**
- `4b631f6` - Phase 3A enhancements (timing system, hover states, dark mode fix)
- `29b33c6` - Phase 3B enhancements (radio promo, funnel, CTAs, layout density)
- `7843700` - Patriot Internal Medicine vault entries (vault repo)
## Credentials & Secrets
**Patriot Internal Medicine - Tucson Site:**
- Client ID: `9c69dc3c-5fa1-4878-8b9d-f69c36df2a67`
- Site ID: `8c4fcf01-5fca-4a40-83e7-75b87f658b72`
- Site Code: `NORTH-WOLF-6270`
- API Key: `grmm_Yk1zC1LBvTyKkNkqLrsj2ediMzQKhMWp`
- Vault: `clients/patriot-internal-medicine/gururmm-site-tucson.sops.yaml`
**Patriot Internal Medicine - Sonoita Site:**
- Client ID: `9c69dc3c-5fa1-4878-8b9d-f69c36df2a67`
- Site ID: `2ec7dcc5-91c3-4182-bfba-cd2cf9a43349`
- Site Code: `LIGHT-HARBOR-9617`
- API Key: `grmm_sAoviYq56TGkr6UoxPLvDXomXMOHYDK9`
- Vault: `clients/patriot-internal-medicine/gururmm-site-sonoita.sops.yaml`
## Infrastructure & Servers
**RMM Server:**
- API Base: `http://172.16.3.30:3001`
- Auth: JWT via vault credentials at `infrastructure/gururmm-server.sops.yaml`
**IX Server (ww9 hosting):**
- IP: `172.16.3.10`
- Hostname: `ix.azcomputerguru.com`
- SSH: Port 22, root user
- cPanel account: `azcomputerguru`
- ww9 path: `/home/azcomputerguru/public_html/ww9/`
- Vault: `infrastructure/ix-server.sops.yaml`
**ww9 Website:**
- URL: http://ww9.azcomputerguru.com
- Domain config: `/var/cpanel/userdata/azcomputerguru/ww9.azcomputerguru.com`
## Commands & Outputs
**Deploy Phase 3B to ww9:**
```bash
rsync -avz --delete -e "ssh -p 22" projects/acg-website-showcase/multipage/ root@172.16.3.10:/home/azcomputerguru/public_html/ww9/
# Output: sent 19813361 bytes, total size 23500190, speedup 1.19
```
**Create RMM client:**
```bash
curl -s -X POST "$RMM/api/clients" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"name":"Patriot Internal Medicine"}'
# Response: {"id":"9c69dc3c-5fa1-4878-8b9d-f69c36df2a67",...}
```
**Verify deployment:**
```bash
curl -s http://ww9.azcomputerguru.com/ | grep "radio-promo"
# Confirmed: radio promo bar present
curl -s http://ww9.azcomputerguru.com/ | grep "funnel-steps"
# Confirmed: 3-step funnel present
```
## Pending / Incomplete Tasks
**ACG Website (Optional Phase 3C):**
- Homepage section reordering (current: Hero→Trust→Funnel→Story→Services→Pricing→Dispatch→CTA)
- Radio promo compact versions for Services/Pricing pages
**Patriot Internal Medicine:**
- Deploy agents to Tucson and Sonoita endpoints
- Run onboarding diagnostics after first agents enroll
- Create wiki article for client
## Reference Information
**Phase 3B Enhancements Deployed:**
1. Radio Show Promotion:
- Header LIVE badge: `<span class="radio-badge">&#9654; LIVE</span>` with 2s pulse animation
- Promo bar: Show name, time (9AM in accent box), station KVOI 1030AM, call-in 520-790-2020
- Ticker: Bottom marquee (from Phase 3A), pauseable on hover
2. 3-Step Visual Funnel (after Trust section):
- Step 1: Build your estimate → Calculator
- Step 2: Talk it through → Contact
- Step 3: Month-to-month start (no lock-in)
- Interactive number badges (48×48px) with hover lift + color fill
3. Strengthened Calculator CTAs:
- Pricing teaser: "Build your exact price" promoted to primary button
- All 6 service cards: Added "See what this costs →" inline links
4. Increased Vertical Rhythm:
- Section padding: `clamp(2.75rem, 5.5vw, 4.25rem)`
- Service list: +1.5× base margin-bottom
- Dispatch grid: +1.5× base margin-bottom
**Brand Assets Available:**
- Location: `/projects/acg-website-showcase/brand-kit/`
- Style Guide: `AZComputerGuru_StyleGuide.pdf`
- Logos: `logo-transwhite.png`, `logo-flatwhite.jpg`, `guru-vector.eps`
- Icons: `Guru Icons/` (16-512px)
- Font: `Lato Font/` (full family + OFL license)
- Colors: `Colors.png`
- Social: `social-avatar.jpg`
- Letterhead: `acg-letterhead-2025.png` + `.docx`
**RMM Enrollment URLs:**
- Tucson: https://rmm.azcomputerguru.com/install/NORTH-WOLF-6270
- Sonoita: https://rmm.azcomputerguru.com/install/LIGHT-HARBOR-9617
**Git Commits:**
- ClaudeTools main: `29b33c6` (Phase 3B enhancements)
- Vault main: `7843700` (PIM enrollment keys)
**Bot Alerts Posted:**
- `[RMM] Mike onboarded client 'Patriot Internal Medicine' + site 'Tucson' (NORTH-WOLF-6270)`
- `[RMM] Mike onboarded client 'Patriot Internal Medicine' + site 'Sonoita' (LIGHT-HARBOR-9617)`

View File

@@ -2,7 +2,7 @@
type: client
name: cascades-tucson
display_name: Cascades of Tucson
last_compiled: 2026-06-17
last_compiled: 2026-06-18
compiled_by: HOWARD-HOME/claude-main
sources:
- session-logs/2026-03-24-session.md
@@ -46,6 +46,7 @@ sources:
- clients/cascades-tucson/session-logs/2026-06/2026-06-15-howard-cs-server-raid-vpn-reset.md
- clients/cascades-tucson/session-logs/2026-06/2026-06-16-howard-vertical-voice-vlan-plan.md
- clients/cascades-tucson/docs/network/voice-vlan-cutover.md
- clients/cascades-tucson/docs/network/voice-phone-inventory.md
- clients/cascades-tucson/reports/2026-06-16-unifi-full-audit.md
- clients/cascades-tucson/reports/2026-06-16-2.4ghz-remediation-runbook.md
- clients/cascades-tucson/docs/overview.md
@@ -65,6 +66,13 @@ sources:
- clients/cascades-tucson/docs/proposals/kpi-dashboard.md
- clients/cascades-tucson/docs/proposals/kpi-dashboard-onepager.md
- .claude/memory/project_cascades_kpi_dashboard.md
- clients/cascades-tucson/session-logs/2026-06/2026-06-17-howard-cascades-poly-phone-drops-network-smoothing.md
- clients/cascades-tucson/session-logs/2026-06/2026-06-17-howard-cascades-power-outage-recovery-and-5ghz.md
- clients/cascades-tucson/session-logs/2026-06/2026-06-17-howard-cs-server-drive-review-and-spike-question.md
- clients/cascades-tucson/session-logs/2026-06/2026-06-17-howard-voice-vlan30-build.md
- clients/cascades-tucson/session-logs/2026-06/2026-06-18-howard-cascades-outage-followup-openvpn-printer.md
- clients/cascades-tucson/session-logs/2026-06/2026-06-18-howard-synology-drive-sync-diagnosis.md
- clients/cascades-tucson/session-logs/2026-06/2026-06-18-howard-lupesanchez-desktop-trcieja-perf-diag.md
backlinks:
- projects/gururmm
- wiki/systems/uos-server
@@ -129,12 +137,13 @@ Because per-user **Intune** never provisioned tenant-wide (`INTUNE_A = PendingIn
- Shelby Trozzi -- MemCare Director (MDIRECTOR-PC)
- Chris Knight -- Accounting / Business Office (same access tier as Lauren Hasselman); chris.knight@cascadestucson.com (alias: c.knight@cascadestucson.com). **Workstation setup 2026-06-08:** machine **DESKTOP-N5G1ROO** (Win 11 Pro for Workstations) domain-joined + GuruRMM-enrolled (agent `205025ee-2676-4498-8a27-e88562a6f69a`), Office installed. AD account `chris.knight` (OU=Administrative) finished to match Lauren. Mailbox remains cloud-only/unsynced (same split state as Lauren).
- JD Martin -- Syncro-confirmed contact (jd.martin@cascadestucson.com); role not yet documented.
- Lupe Sanchez -- staff (DESKTOP-TRCIEJA). EOL workstation (Gateway ZX6971 AIO, i3-2120, 8 GB RAM, Win11 unsupported). **Decision 2026-06-18: replace machine** (dual-AV + EOL hardware causing slow Excel; no remediation on current box). GuruRMM agent `c9bf1a2d-bfdc-401e-9cc8-f9e90bb19587` (resolve live by hostname; UUIDs change on re-enroll).
- **Syncro contact emails (authoritative):** ashley.jensen@, jd.martin@, crystal.rodriguez@, John.trozzi@, meredith.kuhn@, accounting@/accountingassistant@cascadestucson.com.
- **Billing rate:** $175/hr all labor (prepaid block customer)
- **Hours remaining:** **55.75 hrs (live Syncro pull 2026-06-16).** Most recent draws: 0.5h remote 2026-06-10 Meredith locked Word doc (ticket #32403, 56.75->56.25); 0.5h remote 2026-06-12 shared mailboxes Grievances+Surveys (ticket #32417, 56.25->55.75). Always live-check via `GET /customers/20149445` before billing.
- **Hours remaining:** **55.75 hrs (live Syncro pull 2026-06-18).** Most recent draws: 0.5h remote 2026-06-10 Meredith locked Word doc (ticket #32403, 56.75->56.25); 0.5h remote 2026-06-12 shared mailboxes Grievances+Surveys (ticket #32417, 56.25->55.75). No draws from 2026-06-17/18 sessions (advisory/diagnostic only). Always live-check via `GET /customers/20149445` before billing.
- **Syncro customer ID:** 20149445
- **Managed devices (Syncro):** 29 (live pull 2026-06-16)
- **Active tickets:** Syncro live pull 2026-06-16 shows **0 open tickets.** See session logs for recent work. #32370 (eFax/scanner onsite) was confirmed [New]/open on 2026-06-13 -- verify/likely closed.
- **Managed devices (Syncro):** 29 (live pull 2026-06-18)
- **Active tickets:** Syncro live pull 2026-06-18 shows **0 open tickets.** See Active Work section for open non-Syncro follow-ups. #32370 (eFax/scanner onsite) was confirmed [New]/open on 2026-06-13 -- verify/likely closed.
- #110680053 / #32303 -- Entra / domain migration project. Status: **Invoiced** as of 2026-06-05. Plan: `C:\Users\Howard\.claude\plans\wise-discovering-panda.md`
- #109412123 -- Entra setup project (verify status)
- #32403 -- Meredith locked Word doc (0.5h remote, billed 2026-06-10, Invoiced)
@@ -151,14 +160,16 @@ Because per-user **Intune** never provisioned tenant-wide (`INTUNE_A = PendingIn
| CS-SERVER | 192.168.2.254 | DC, DNS, DHCP (no scopes), File Server, Hyper-V host, Print Server | Windows Server 2019 Standard | Dell PowerEdge R610 (~2009 hardware, 16+ years old). **Single DC -- CRITICAL risk. No backup until 2026-06-15.** GuruRMM agent ID: `c39f1de7-d5b6-45ae-b132-e06977ab1713` (re-enrolled; always resolve the agent live by hostname, never hardcode the UUID). **OS RAID-1 mirror DEGRADED (2026-06-15) -- see hardware warning below.** |
| CS-SERVER iDRAC | 192.168.2.65 | Out-of-band management | -- | Dell OOB interface |
| CS-QB (Hyper-V VM on CS-SERVER) | 192.168.2.228 | (label "VoIP server" -- STALE) | -- | **2026-06-16 recon: SMB/445 only, no SIP response -- NOT a live SIP PBX.** Phones appear cloud-registered (Vertical). Label predates the wireless-phone transition; revisit/retire. |
| cascadesDS (Synology NAS) | 192.168.0.120 | NAS / legacy file storage | DSM | Port 5000 HTTP. Workgroup name is "CASCADES" -- same as AD short name, causing Kerberos auth failures from domain-joined machines. Slated to become backup-only. |
| pfSense Firewall | 192.168.0.1 | Perimeter firewall, inter-VLAN routing, DHCP/DNS | pfSense Plus 25.07-RELEASE | Netgate device. cert CN=pfSense-685f277aa6886. Dual-WAN. All DHCP (CS-SERVER DHCP role has no scopes). 199 DHCP subnets (per-unit /28 VLANs, assisted-living L2 isolation). SSH shell access works (no interactive menu). Admin vault: `clients/cascades-tucson/pfsense-firewall`. OpenVPN user Howard: vault `clients/cascades-tucson/pfsense-openvpn-howard`. |
| cascadesDS (Synology NAS) | 192.168.0.120 | NAS / legacy file storage | DSM 7.2.1-69057 | Port 5000 HTTP. Workgroup name is "CASCADES" -- same as AD short name, causing Kerberos auth failures from domain-joined machines. Slated to become backup-only. **Synology Drive Server 3.5.0-26088** (active, port 6690 SSL). Current Drive sync: CS-SERVER Drive Client (v7.5.0.16085, runs as sysadmin) syncs Sync-user My Drive (`/volume1/homes/Sync/Drive/`) -> `D:\Shares\Main` (one-way download). Real shared folders (Server 1.9 G, Management 5.5 G, Public ~50 G, SalesDept ~23 G, etc.) are NOT in scope -- Team Folder migration pending. |
| pfSense Firewall | 192.168.0.1 | Perimeter firewall, inter-VLAN routing, DHCP/DNS | pfSense Plus 25.07-RELEASE | Netgate device. cert CN=pfSense-685f277aa6886. Dual-WAN. All DHCP (CS-SERVER DHCP role has no scopes). 199 DHCP subnets (per-unit /28 VLANs, assisted-living L2 isolation). SSH shell access works (no interactive menu). Admin vault: `clients/cascades-tucson/pfsense-firewall`. OpenVPN user Howard: vault `clients/cascades-tucson/pfsense-openvpn-howard`. **Config vaulted 2026-06-17:** `clients/cascades-tucson/pfsense-config-backup-2026-06-17.sops.yaml`. pfSense is ZFS (power-loss resilient). Logs are PLAIN TEXT (not clog). |
**[CRITICAL] CS-SERVER hardware -- RAID degraded (2026-06-15):** Dell R610, basic SAS 6/iR controller (3 Gbps, no cache). The **OS RAID-1 mirror (Virtual Disk2 = C:, holds OS / AD / SQL / page file) is DEGRADED** -- Physical Disk 0:0:3 (320 GB WD SATA laptop drive) is Critical/Removed, leaving C: on a single surviving 320 GB Hitachi 5400 RPM spindle with ZERO redundancy. A 1.2 TB SAS disk (1:0:4) sits "Ready" but is the wrong size/type to rebuild the 320 GB mirror, so no auto-rebuild fired. D: is a separate healthy RAID-1 (2x 1.2 TB SAS). The degraded mirror on a slow laptop spindle is the root cause of "CS-SERVER slow" reports (random-I/O bound). With the single-DC, EOL (16+ yr) posture this is a data-loss emergency -- SSD rebuild-then-swap is a valid band-aid (image C: first; enterprise SATA SSD >= 320 GB; no TRIM through this controller) but the DC migration remains the real fix.
**[CRITICAL] CS-SERVER hardware -- RAID degraded (2026-06-15):** Dell R610, basic SAS 6/iR controller (3 Gbps, no cache). The **OS RAID-1 mirror (Virtual Disk2 = C:, holds OS / AD / SQL / page file) is DEGRADED** -- Physical Disk 0:0:3 (320 GB WD SATA laptop drive, `WDC WD3200BEVT`) is Critical/Removed, leaving C: on a single surviving 320 GB Hitachi `HTS545032B9A300` 5400 RPM spindle with ZERO redundancy. A 1.2 TB SAS disk (1:0:4) sits "Ready" but is the wrong size/type to rebuild the 320 GB mirror, so no auto-rebuild fired. D: is a separate healthy RAID-1 (2x 1.2 TB SAS). The degraded mirror on a slow laptop spindle is the root cause of "CS-SERVER slow" reports (random-I/O bound). With the single-DC, EOL (16+ yr) posture this is a data-loss emergency -- SSD rebuild-then-swap is a valid band-aid (image C: first; enterprise SATA SSD >= 480 GB, 2.5"; no TRIM through this controller; buy 2 identical: e.g. Solidigm D3-S4520 480 GB or Samsung PM893 480 GB; SATA negotiates to 3 Gbps; no Dell certified-drive lockout) but the DC migration remains the real fix. Gating: **verify cloud backup first full + image-based + retention before any drive work.**
**[INFO] Backup -- gap closed (2026-06-15):** Mike installed ACG cloud backup (MSP360/CloudBerry -> ACG-backup server) on CS-SERVER and started a backup, addressing the longstanding SS164.308(a)(7) "no backup" HIPAA gap. (Synology Active Backup for Business remains blocked -- ext4, not Btrfs.) Verify the first full completes and set retention.
**[WARNING] CS-SERVER endpoint-agent sprawl:** CS-SERVER is NOT in the ACG Bitdefender/GravityZone tenant (Cascades company id `66b0448e1e0441d02508bad8`; 3 endpoints there, CS-SERVER absent). Defender is replaced by a Syncro-managed "Endpoint Protection Service". The previous MSP's **Datto RMM/CentraStage + Datto EDR/Infocyte** are still installed on top of Syncro + GuruRMM + ScreenConnect + KPAX -- overlapping agents thrashing the degraded spindle. Clean up the Datto stack. (Infection sweep 2026-06-15: clean.)
**[WARNING] CS-SERVER endpoint-agent sprawl:** CS-SERVER is NOT in the ACG Bitdefender/GravityZone tenant (Cascades company id `66b0448e1e0441d02508bad8`; 3 endpoints there, CS-SERVER absent). Defender is replaced by a Syncro-managed "Endpoint Protection Service". The previous MSP's **Datto RMM/CentraStage + Datto EDR/Infocyte** are still installed on top of Syncro + GuruRMM + ScreenConnect + KPAX -- overlapping agents thrashing the degraded spindle. Clean up the Datto stack. (Infection sweep 2026-06-15: clean.) **DESKTOP-TRCIEJA is another confirmed instance** of the leftover-Datto-stack fleet-wide problem (2026-06-18) -- see Lupe Sanchez in Profile.
**[WARN] Power outage (2026-06-17):** Building power outage took the entire Cascades network down (all 77 APs + 12 switches, 0 clients). Root cause chain: pfSense was plugged into the **surge-only side of the UPS** (no battery) -- it hard-powered-off uncleanly. ZFS survived (pools healthy, config.xml valid). Dirty boot caused a **duplicate dhcpd** (DISCOVER->OFFER but no REQUEST/ACK) and a **2nd-floor switch (USL24PB `Switch 2nd Floor #2`, 192.168.2.193) with one-way L2 forwarding** that blocked DHCP OFFERs from reaching floor-2 APs. Howard killed the duplicate dhcpd + clean restart remotely; Mike: re-seated pfSense onto battery outlets, restored config from on-box auto-backup (12:20 version, VLAN30 intact), reset+re-adopted Switch 2nd Floor #2 (floors 3/4 followed), rebooted Cox modem (missed post-restore step that prolonged WAN issues). Network fully restored. Post-recovery casualties: devices that booted during the DHCP-down window cached a disconnected state and did not retry (kitchen thermal printer, POS ticket printer) -- power-cycle each as reported. Incident report: `clients/cascades-tucson/reports/2026-06-17-power-outage-incident.md`.
### Email & Identity
@@ -185,25 +196,29 @@ Because per-user **Intune** never provisioned tenant-wide (`INTUNE_A = PendingIn
### Network
- **ISP / WAN:** Dual-WAN Cox. WAN1 igc0 `184.191.143.62/30` (Cox Fiber, primary, gateway `184.191.143.61`) + WAN2 igc3 `72.211.21.217/27` (Cox Coax, secondary, static); `WAN_Group` gateway group; both active full-duplex, no loss events (verified 2026-06-16). Both WAN IPs added as Cascades Named Location in Entra (ID: `061c6b06-b980-40de-bff9-6a50a4071f6f`).
- **Firewall:** pfSense Plus **25.07-RELEASE** (Netgate) at `192.168.0.1`, cert CN=pfSense-685f277aa6886. Admin vault: `clients/cascades-tucson/pfsense-firewall`. SSH shell access works (no interactive menu). OpenVPN user Howard: vault `clients/cascades-tucson/pfsense-openvpn-howard` (split-tunnel; `route 192.168.0.0/22`; use OpenVPN GUI or OpenVPN Connect with DCO disabled for stability -- DCO/TAP instability seen 2026-06-16). pfSense-ssh.sh (unifi-wifi skill) provides scripted audit/dhcp/run access.
- **Firewall:** pfSense Plus **25.07-RELEASE** (Netgate) at `192.168.0.1`, cert CN=pfSense-685f277aa6886. Admin vault: `clients/cascades-tucson/pfsense-firewall`. SSH shell access works (no interactive menu). OpenVPN user Howard: vault `clients/cascades-tucson/pfsense-openvpn-howard` (split-tunnel; `route 192.168.0.0/22`; use OpenVPN GUI or OpenVPN Connect with DCO disabled for stability -- DCO/TAP instability seen 2026-06-16). pfSense-ssh.sh (unifi-wifi skill) provides scripted audit/dhcp/run access. **Logs are PLAIN TEXT on 25.07 -- read with tail/grep, NOT clog (clog returns empty).** pfSense has an **OpenVPN `--inactive` idle timeout (~300s)** configured on the server; it disconnects clients after ~5 min of no tunnel data (keepalive pings do NOT reset this counter). This is a config setting, not a fault -- raise/disable to fix the flapping (fix proposed 2026-06-18, not applied). **[OUTAGE 2026-06-17] pfSense was on UPS surge-only side -- moved to battery-backed outlets by Mike (rectified). On-box auto-backup (12:20 version) restored by Mike; config vaulted `clients/cascades-tucson/pfsense-config-backup-2026-06-17.sops.yaml`. Enable Netgate AutoConfigBackup to prevent off-box backup gap.**
- **[INFO] pfSense health check (2026-06-16):** gateway ruled out as WiFi factor -- DHCP not exhausted (270/~507 active ~53% on the AP/WiFi pool), unbound DNS up, both WANs full-duplex/stable, firewall states 28-31k/790k, load 0.6. Minor: igc3/WAN2 Intel I225/226 2.5G counter quirk (1707 input-errors+collisions logged, full-duplex active, no loss) -- not a fault, no action needed.
- **LAN / VLAN layout:** Primary staff/AP network `192.168.0.0/22` (pfSense .0.1, cascadesDS .0.120, UniFi APs + most WiFi clients on 192.168.2.x/3.x). DHCP pool 192.168.2.2-192.168.3.254 (~507 cap, ~270 active ~53%). Per-unit /28 VLANs: **199 DHCP subnets** total, mostly `10.x.y.0/28` per apartment (assisted-living L2 isolation) + Staff/Internal VLAN 20 (`10.0.20.0/24`, gw `10.0.20.1`) + Guest VLAN 50 (`10.0.50.0/24`, RFC1918 blocked). DHCP backend: ISC (Kea config present, dormant). Unbound DNS.
- **Switching:** Full UniFi. **77 U7-Pro APs** + **12 managed switches** (1st Floor USW-48 PoE core; floors 2-4 USW-Pro-24-PoE; MemCare USW-Pro-24-PoE; USW Lite 8 PoE; USW-16-PoE VoIP switch). **[WARN] ~25 switch ports linked at 100 Mbps but gig-capable** (systematic cabling/NIC issue, 1st/2nd/3rd-floor switches; investigate after WiFi Phase A). 3 offline switches: Switch 2nd Floor #2, Switch 4th Floor #2, USW Pro Max 16. PoE budgets healthy. Port p38 (1st Floor USW) 4.0% tx-drop rate. All managed on the shared UOS controller (172.16.3.29, HTTPS 11443; see [[uos-server]]); Cascades site short name `va6iba3v`, site_id `685f39068e65331c46ef6dd2`. **Mesh topology:** 2nd Floor Atrium is wireless-mesh parent for CC Bridge + salon (5 GHz backhaul ch36); 206 U7 Pro carries AP 108. Switch hardware replacement on floors 2/3/4 complete.
- **LAN / VLAN layout:** Primary staff/AP network `192.168.0.0/22` (pfSense .0.1, cascadesDS .0.120, UniFi APs + most WiFi clients on 192.168.2.x/3.x). DHCP pool 192.168.2.2-192.168.3.254 (~507 cap, ~270 active ~53%). Per-unit /28 VLANs: **199 DHCP subnets** total, mostly `10.x.y.0/28` per apartment (assisted-living L2 isolation) + Staff/Internal VLAN 20 (`10.0.20.0/24`, gw `10.0.20.1`) + Guest VLAN 50 (`10.0.50.0/24`, RFC1918 blocked) + **Voice VLAN 30** (`10.0.30.0/24`, gw `10.0.30.1`). DHCP backend: ISC (Kea config present, dormant). Unbound DNS.
- **Switching:** Full UniFi. **77 U7-Pro APs** + **12 managed switches** (1st Floor USW-48 PoE core; floors 2-4 USW-Pro-24-PoE; MemCare USW-Pro-24-PoE; USW Lite 8 PoE; USW-16-PoE VoIP switch). **[WARN] ~25 switch ports linked at 100 Mbps but gig-capable** (systematic cabling/NIC issue, 1st/2nd/3rd-floor switches; investigate after WiFi Phase A). 3 offline switches: Switch 2nd Floor #2, Switch 4th Floor #2, USW Pro Max 16. PoE budgets healthy. Port p38 (1st Floor USW) 4.0% tx-drop rate. All managed on the shared UOS controller (172.16.3.29, HTTPS 11443; see [[uos-server]]); Cascades site short name `va6iba3v`, site_id `685f39068e65331c46ef6dd2`. **Mesh topology:** 2nd Floor Atrium is wireless-mesh parent for CC Bridge + salon (5 GHz backhaul ch36); 206 U7 Pro carries AP 108. Switch hardware replacement on floors 2/3/4 complete. **Note: Switch 2nd Floor #2 (USL24PB, 192.168.2.193) was reset+re-adopted after the 2026-06-17 power outage -- it had one-way L2 forwarding blocking DHCP offers.**
- **WiFi SSIDs:**
- **CSCNet -- shared PPSK SSID.** `private_preshared_keys_enabled`; ~230 per-key->network mappings (most keys -> per-room resident VLANs 101-631; a few -> Default; one phone key -> Internal/VLAN 20). ~1,190 historical clients (residents' IoT/TVs, staff, phones). **Do NOT repoint the SSID to move a subset of clients** -- move at the PPSK level. wlanconf `685f39078e65331c46ef7ee5`; cred vault `clients/cascades-tucson/wifi-cscnet.sops.yaml`.
- **CSCNet -- shared PPSK SSID.** `private_preshared_keys_enabled`; ~230-242 per-key->network mappings (most keys -> per-room resident VLANs 101-631; a few -> Default; one phone key -> Internal/VLAN 20; one voice PPSK -> VOICE/VLAN 30). ~1,190 historical clients (residents' IoT/TVs, staff, phones). **Do NOT repoint the SSID to move a subset of clients** -- move at the PPSK level. wlanconf `685f39078e65331c46ef7ee5`; cred vault `clients/cascades-tucson/wifi-cscnet.sops.yaml`.
- CSC ENT -- legacy SSID, main LAN (192.168.0.0/22), being deprecated as migration proceeds
- Guest -- isolated, VLAN 50
- **Wireless RF status (live audit 2026-06-15/16 -- ~587 concurrent clients):**
- **2.4 GHz is the primary pain band:** avg TX-retry ~10%, cu_total 69-94% live, catastrophic neighbor BSSID density (ch6 ~33k BSSIDs, ch1 ~19k, ch11 ~17k). 27 of the 40 worst clients on 2.4 GHz (retry 11-42%), mostly IoT/legacy. Root cause: ~75 2.4 GHz radios running at auto (full) power in extreme density. Experience splits by band -- 5/6 GHz clients are fine; clients stuck on 2.4 GHz suffer.
- **5 GHz:** 80 MHz channel width on 76/77 APs (should be 40 MHz at this density). 55/77 radios on DFS channels (52-144). DFS concern is theoretical resilience, not current throughput: `dfs-check.sh` 2026-06-16 confirmed **ZERO real radar events fleet-wide** (55 DFS APs, full `dmesg` sweep). Measured retry DFS (8.4%) ~= non-DFS (9.0%). Still plan to move to non-DFS (UNII-1 36-48 + UNII-3 149-161) for resilience near Davis-Monthan AFB. NOTE: an earlier mid-session claim (2026-06-15 audit) that "DFS was the #1 problem" was an artifact of tooling bugs (raw counter + 15-AP head cap) and was withdrawn -- do not repeat it.
- **Wireless RF status (audit 2026-06-15/16 + changes through 2026-06-17 -- ~587 concurrent clients):**
- **2.4 GHz is the primary pain band:** avg TX-retry ~10%, cu_total 69-94% live, catastrophic neighbor BSSID density (ch6 ~33k BSSIDs, ch1 ~19k, ch11 ~17k). 27 of the 40 worst clients on 2.4 GHz (retry 11-42%), mostly IoT/legacy. Root cause: high radio density running at excessive TX power.
- **2.4 GHz Phase A status -- OVER-THINNED (as of 2026-06-17):** Floor-4 pilot (2026-06-16) applied 14/15 radios to 6 dBm (retry 13.2->9.5%, no coverage loss). Subsequently overnight 2026-06-17, Phase A was extended: **24 of 76 2.4 radios DISABLED + 42 set to Low (~6 dBm)**; Floors 5/6 + mesh untouched. Results DEGRADED: retry 17->23.4%, satisfaction 39->30 -- over-thinned. **Current recommendation: Low->Medium for the 42 at-Low radios.** Phase 0 (ping-check off, 3AM auto-upgrade disable, pfSense logging) + Phase 1 (combined radio_table PUT: ng power medium [42 radios], na ht 40 [76], na min_rssi -82 [69]) planned, dry-run clean, NOT applied -- pending explicit go-ahead.
- **5 GHz:** Auto-channel reassignment applied via UniFi 2026-06-17 (Howard) -- made co-channel overlap **WORSE** (25->30 co-channel pairs from 173 strong neighbor pairs). `dfs-check.sh` 2026-06-16: **ZERO real radar events fleet-wide** (DFS empirically low-risk). Plan: **Option B = combined per-AP PUT of 40MHz + non-DFS optimized channel plan + min-RSSI -82 relax.** Width + channel are coupled (width alone fixed only 7/25 pairs; non-DFS needs 40MHz). Dry-run clean; NOT applied. NOTE: an earlier mid-session claim (2026-06-15 audit) that "DFS was the #1 problem" was an artifact of tooling bugs and was withdrawn -- do not repeat it.
- **6 GHz:** active on 75 radios; only 1 client. Largest untapped, clean, non-DFS capacity -- band-steering 6E-capable clients to 6 GHz is the top opportunity.
- **AP-level satisfaction 95-100 fleet-wide.** Pain is in the client tail, presenting as "bad for SOME users."
- **Production change (2026-06-16):** Floor-4 2.4 GHz power-down pilot applied -- 14/15 radios to 6 dBm from ~23 dBm; avg retry 13.2->9.5% (~28% improvement); clients retained (no coverage loss). AP 445 lagged (config=Low but radio stayed 23dBm); left alone, harmless. AP 128 is disabled (intentionally). Disables for 445/428 held pending further validation. Remaining floors (1-3, 5-6) + full disable plan staged but NOT yet applied -- pending scope go-ahead from Howard.
- **AP-level satisfaction 95-100 fleet-wide.** Pain is in the client tail.
- **Config flags:** 6 APs with 2.4 min-RSSI OFF (615, 608, 505, 517, 622, salon); 4 APs off the 1/6/11 plan (128 disabled, 108 offline, 108U7 Pro auto, salon auto).
- **Known hardware:** AP 108 (Floor 1) offline pending a new cable run (expected). Stale duplicate controller object ("108" vs "108U7 Pro") to clean up separately.
- **Creds (vault refs only):** `infrastructure/uos-server-ssh-key` (SSH/Mongo), `infrastructure/uos-server-network-api-rw` (RW controller admin), `clients/cascades-tucson/unifi-ap-ssh` (per-AP device auth via site VPN), `clients/cascades-tucson/pfsense-firewall` (pfSense admin for pfsense-ssh.sh).
- **VoIP (vendor: Vertical -- Richard Turner <RTurner@vertical.com>):** Two phone fleets -- **8 AudioCodes** (OUI `00:90:8f`, WIRED on USW-16-PoE ports 1-8, Default/main LAN) and **22 Poly** (OUI `48:25:67`, WiFi via CSCNet PPSK -> VLAN 20 Internal). The **Vertical-Remote management desktop** (`192.168.2.180`, MAC `e4:e7:49:52:3a:06`, WIRED USW-16-PoE port 16, Default LAN, **static IP, no ACG login**) is RDP-only (recon 2026-06-16 -- not a PBX). No on-prem SIP PBX found -> phones appear to register to a **cloud/hosted PBX** (Vertical). Infra must stay static.
- **[PLANNED] Voice VLAN (VLAN 30) consolidation for the phones:** Segmentation left voice gear split (Poly on VLAN 20; AudioCodes + Vertical desktop on the main LAN), and main-LAN -> VLAN 20 is blocked at pfSense -- so the desktop can't reach the wireless phones and phone IPs drift. Fix: a dedicated isolated **VLAN 30 VOICE (`10.0.30.0/24`, gw `10.0.30.1`, pfSense igc1.30)** holding ALL phones + the Vertical desktop; internet egress allowed, firewalled off VLAN 20 / main LAN / PHI (HIPAA); Vertical's pfSense OpenVPN scoped to `10.0.30.0/24` via a Client-Specific-Override. Desktop is static + no ACG login -> Vertical sets it to DHCP (or grants temp access) at cutover; reserve `10.0.30.10`. Status: PLANNED -- vendor email sent 2026-06-16, awaiting Richard's confirm (cloud-PBX, desktop static, VPN cert CN) + a window. **Full runbook + recon: `clients/cascades-tucson/docs/network/voice-vlan-cutover.md`.**
- **VoIP (vendor: Vertical -- Richard Turner <RTurner@vertical.com>):** Two phone fleets -- **8 AudioCodes** (OUI `00:90:8f`, WIRED on USW-16-PoE ports 1-8, Default/main LAN) and **22 Poly** (OUI `48:25:67`, WiFi via CSCNet PPSK -> VLAN 20 Internal, migrating to VLAN 30). The **Vertical-Remote management desktop** (`10.0.30.201`, MAC `e4:e7:49:52:3a:06`, WIRED USW-16-PoE port 16, VOICE VLAN 30, **DHCP** -- confirmed not static, LogMeIn remote access, no pfSense OpenVPN) is live on VLAN 30. No on-prem SIP PBX found -> phones appear to register to a **cloud/hosted PBX** (Vertical).
- **[2026-06-17 SUBSTANTIALLY COMPLETE] Voice VLAN (VLAN 30) consolidation:** dedicated isolated **VLAN 30 VOICE (`10.0.30.0/24`, gw `10.0.30.1`, pfSense igc1.30, DHCP `.100-.250`, DNS `8.8.8.8/1.1.1.1`)** holding ALL phones + the Vertical desktop; internet/cloud-PBX egress only, firewalled off VLAN 20 / main LAN / PHI / mgmt (HIPAA). Isolation rules verified via `pfctl -sr` (clone of GUEST VLAN -- the only actually-isolated net). Voice PPSK key on CSCNet -> VOICE: vaulted `clients/cascades-tucson/wifi-voice-ppsk`. **Cutover status as of 2026-06-18 (live inventory: `docs/network/voice-phone-inventory.md`):**
- Vertical-Remote desktop (port 16): DONE -- `10.0.30.201`. Re-VLANing a wired port requires bouncing the link (port disable/enable via controller API using CSRF token); a UniFi client block/unblock is MAC-filter only, not a link bounce.
- **22 of 22 Poly WiFi phones: ALL DONE** -- re-keyed to voice PPSK, on `10.0.30.202-.223`. Dial-tone + outbound calls verified.
- **8 AudioCodes (wired, USW-16-PoE ports 1-8): 0/8 REMAINING.** Flip port native VLAN to VOICE + PoE power-cycle each to force re-DHCP.
- **Full runbook:** `clients/cascades-tucson/docs/network/voice-vlan-cutover.md`. Live inventory: `docs/network/voice-phone-inventory.md`.
### External Vendors & Mail Senders
@@ -216,17 +231,17 @@ Cascades' line-of-business / reporting SaaS (the systems they pull data OUT of,
| System | Function | Data-out path |
|---|---|---|
| **ALIS** (Medtelligent) | Clinical EHR (census/clinical) | Vendor reporting/export; API TBD. **HIPAA BAA required before PHI leaves it.** Their most important source. SSO live (see Entra section). |
| **ALIS** (Medtelligent) | Clinical EHR (census/clinical) | Vendor reporting/export; API TBD. **HIPAA -- BAA required before PHI leaves it.** Their most important source. SSO live (see Entra section). |
| **QuickBooks** | Accounting | QBO = API + connectors; Desktop = ODBC |
| **Bill.com** | AP/AR | REST API (most automatable) see mail-sender note above |
| **Bill.com** | AP/AR | REST API (most automatable) -- see mail-sender note above |
| **Relias** | Training / LMS | Reporting export / API (completion data) |
| **You've Got Leads** | Senior-living CRM | Reporting/export; API varies |
| **TELS** (Direct Supply) | Facilities management | Reporting export; API uncertain |
| **Focus HR** | HR / payroll | Export or vendor API (plan-dependent) |
| **Helpany** (app.safe-living.com) | Caregiver app | Niche likely export-only |
| **Helpany** (app.safe-living.com) | Caregiver app | Niche -- likely export-only |
| **POS** | Point of sale | Product TBD |
- **[PROPOSED] Unified KPI dashboard (Ashley Jensen request, 2026-06-17):** single dashboard pulling KPIs across the systems above. **Power BI on-prem Gateway is the WRONG frame** (it only bridges Power BI to on-prem sources, never cloud SaaS). Recommended path leans on their existing M365 Business Premium: **Phase 1** scheduled CSV/Excel exports SharePoint Power BI Pro dashboard on 35 KPIs (census/financials); **Phase 2** automate the API-capable systems (Bill.com, QuickBooks Online) via Power Automate. Niche senior-living apps stay on the export method (no ready connectors). Internal scoping: `clients/cascades-tucson/docs/proposals/kpi-dashboard.md`; client one-pager: `.../kpi-dashboard-onepager.md`. Status: parked, awaiting Ashley's day-one KPIs + freshness need + POS/Focus-HR specifics. Check whether ALIS offers a built-in analytics/data feed (could replace plumbing for their top source).
- **[PROPOSED] Unified KPI dashboard (Ashley Jensen request, 2026-06-17):** single dashboard pulling KPIs across the systems above. **Power BI on-prem Gateway is the WRONG frame** (it only bridges Power BI to on-prem sources, never cloud SaaS). Recommended path leans on their existing M365 Business Premium: **Phase 1** scheduled CSV/Excel exports -> SharePoint -> Power BI Pro dashboard on 3-5 KPIs (census/financials); **Phase 2** automate the API-capable systems (Bill.com, QuickBooks Online) via Power Automate. Niche senior-living apps stay on the export method (no ready connectors). Internal scoping: `clients/cascades-tucson/docs/proposals/kpi-dashboard.md`; client one-pager: `.../kpi-dashboard-onepager.md`. Status: parked, awaiting Ashley's day-one KPIs + freshness need + POS/Focus-HR specifics. Check whether ALIS offers a built-in analytics/data feed (could replace plumbing for their top source).
---
@@ -236,11 +251,13 @@ Cascades' line-of-business / reporting SaaS (the systems they pull data OUT of,
- **CS-SERVER iDRAC:** 192.168.2.65
- **pfSense admin (HTTPS):** https://192.168.0.1 -- vault: `clients/cascades-tucson/pfsense-firewall.sops.yaml`
- **pfSense SSH:** `ssh admin@192.168.0.1` (system OpenSSH; drops to shell directly, no interactive menu) -- vault admin cred: `clients/cascades-tucson/pfsense-firewall.sops.yaml`; pfsense-ssh.sh (unifi-wifi skill) for scripted access.
- **pfSense OpenVPN (Howard):** split-tunnel; vault: `clients/cascades-tucson/pfsense-openvpn-howard.sops.yaml` (user `Howard`; route 192.168.0.0/22). Use OpenVPN GUI or OpenVPN Connect with DCO disabled for stability. Note: Howard-Home is now 10.137.42.0/24 (renumbered 2026-06-16) -- Cascades 192.168.0.x now reachable over the VPN.
- **Synology DSM:** http://192.168.0.120:5000 -- vault: `clients/cascades-tucson/` (existing entry)
- **pfSense OpenVPN (Howard):** split-tunnel; vault: `clients/cascades-tucson/pfsense-openvpn-howard.sops.yaml` (user `Howard`; route 192.168.0.0/22). Use OpenVPN GUI or OpenVPN Connect with DCO disabled for stability. Note: Howard-Home is now 10.137.42.0/24 (renumbered 2026-06-16) -- Cascades 192.168.0.x now reachable over the VPN. Server has a configured `--inactive` idle timeout (~300s) that silently drops idle clients -- this is a config setting, not instability.
- **pfSense config backup (2026-06-17):** `clients/cascades-tucson/pfsense-config-backup-2026-06-17.sops.yaml`
- **Synology DSM:** http://192.168.0.120:5000 -- vault: `clients/cascades-tucson/synology-cascadesds.sops.yaml` (admin). Drive Server port 6690 (SSL). **[SECURITY] Synology Cloud Signin Portal credential (`clients/cascades-tucson/synology-signin-portal.sops.yaml`) was committed plaintext at vault commit 1fbc0e1 -- exposed in git history; encrypted go-forward but credential should be rotated.**
- **M365 admin:** admin@cascadestucson.com -- vault: `clients/cascades-tucson/m365-admin.sops.yaml`
- **M365 sysadmin:** sysadmin@cascadestucson.com -- vault: `clients/cascades-tucson/m365-sysadmin.sops.yaml`
- **WiFi CSCNet:** vault: `clients/cascades-tucson/wifi-cscnet.sops.yaml`
- **WiFi Voice PPSK (VLAN 30):** vault: `clients/cascades-tucson/wifi-voice-ppsk.sops.yaml`
- **MDM service account:** vault: `clients/cascades-tucson/mdm-service-account.sops.yaml`
- **svc-scan (scan-to-folder service account):** vault: `clients/cascades-tucson/svc-scan.sops.yaml`. AD account on CS-SERVER for the Accounting Brother's SMB scans.
- **ALIS SSO app registration:** vault: `clients/cascades-tucson/alis-sso-app-registration.sops.yaml`
@@ -250,6 +267,7 @@ Cascades' line-of-business / reporting SaaS (the systems they pull data OUT of,
- **UOS controller (HTTPS):** https://172.16.3.29:11443 (HTTPS 11443, not 8443) -- site `va6iba3v` / site_id `685f39068e65331c46ef6dd2`
- **GuruRMM -- RECEPTIONIST-PC:** agent ID `9c91d324-1073-449c-8cc0-45c5bccfc218` (flaky WebSocket, may lag fleet updates)
- **GuruRMM -- ASSISTMAN-PC (Meredith Kuhn):** agent ID `cf86fa5e-96a2-494d-9cb1-8be22a518ad0`
- **GuruRMM -- DESKTOP-TRCIEJA (Lupe Sanchez):** agent ID `c9bf1a2d-bfdc-401e-9cc8-f9e90bb19587` (resolve live by hostname; UUIDs change on re-enroll)
- **Remediation tool:** Full tiered app suite consented 2026-04-21. All six apps active: Security Investigator, Exchange Operator, User Manager, Tenant Admin, Defender Add-on, Intune Manager.
- **ComputerGuru Exchange Operator MSP app:** `b43e7342-5b4b-492f-890f-bb5a4f7f40e9` -- vault: `msp-tools/computerguru-exchange-operator.sops.yaml`.
- **Vault root:** `clients/cascades-tucson/` in vault repo
@@ -304,6 +322,7 @@ Cascades' line-of-business / reporting SaaS (the systems they pull data OUT of,
- **Stale Word owner (lock) files on cascadesDS shares:** Word creates a hidden `~$<truncated filename>` owner file when a document is opened; if the user's session ends without cleanly closing Word, the `~$` file is orphaned. **Fix:** delete the `~$` file(s). Confirmed 2026-06-10: five `~$` files dated 2024 on `\\cascadesds\Public\Company Web Docs\Staff Trainings\` caused false lock messages.
- **Accessing cascadesDS from RMM -- always use a user session, not CS-SERVER SYSTEM.** The domain-joined CS-SERVER machine account cannot authenticate to the Synology `Public` share because cascadesDS uses workgroup "CASCADES" (same short name as the AD domain), causing Kerberos auth failures. Workaround: run the command in the `user_session` context of a machine where the target user is actively logged in (e.g. ASSISTMAN-PC agent `cf86fa5e` for Meredith-accessible shares).
- **Synology Drive sync scope (as of 2026-06-18):** The Drive Client on CS-SERVER syncs only the **Sync DSM user's My Drive** (`/volume1/homes/Sync/Drive/`) into `D:\Shares\Main` -- one-way download (mode:1). The real department shared folders (`/volume1/Server`, `/volume1/Management`, `/volume1/Public`, `/volume1/SalesDept`, etc.) are **NOT** in this scope and are NOT currently mirrored to CS-SERVER. These require a separate Team Folder setup. Note: `synopkg status SynologyDrive` falsely returns "stopped" (status 263) even when the service is active -- verify via `systemctl is-active pkgctl-SynologyDrive` and `netstat -tlnp | grep 6690` instead.
### Browser / Edge
@@ -334,10 +353,13 @@ Cascades' line-of-business / reporting SaaS (the systems they pull data OUT of,
- **6 GHz is nearly unused.** 75 radios active; only 1 client. Largest untapped, clean, non-DFS capacity. Band-steering 6E-capable clients to 6 GHz is the highest-ROI tuning opportunity.
- **Switch audit (2026-06-16):** ~25 ports linked at 100 Mbps but gig-capable (systematic cabling/NIC issue, 1st/2nd/3rd-floor switches; investigate after WiFi Phase A). PoE budgets healthy. 3 offline switches: Switch 2nd Floor #2, Switch 4th Floor #2, USW Pro Max 16. Port p38 (1st Floor USW) 4.0% tx-drop rate.
- **AP-level satisfaction 95-100 fleet-wide.** Network is healthy on average; pain is in the client tail.
- **Remediation status (as of 2026-06-16 evening):**
- **Phase A (2.4 power-down to Low): PARTIALLY APPLIED.** Floor-4 pilot applied 2026-06-16 (14/15 radios to 6 dBm from ~23; avg retry 13.2->9.5%, cu_total 86->83%, clients retained -- no coverage loss). AP 445 lagged (left alone, harmless). Remaining floors 1-3, 5-6 + floor-2/misc mesh APs = staged, pending go-ahead per zone. AP 128 is disabled (intentionally, re-disable after any zone apply restores it).
- **Remediation status (as of 2026-06-17 -- OVER-THINNED):**
- **Phase A (2.4 power-down): EXTENDED + OVER-THINNED.** Floor-4 pilot (2026-06-16): 14/15 radios to 6 dBm, retry 13.2->9.5%, no coverage loss. Subsequently (overnight 2026-06-17): 24 of 76 2.4 radios DISABLED + 42 set to Low (~6 dBm); Floors 5/6 + mesh untouched. Results DEGRADED: retry 17->23.4%, satisfaction 39->30. **Current action needed: Low->Medium for the 42 at-Low radios** (Phase 0 + Phase 1 per the combined radio_table PUT plan, pending go-ahead).
- **Phase C (disable 9 redundant 2.4 radios): NOT applied.** Data-backed disable list (each has >=2 active-2.4 SNR neighbors): 127->128, 229->128, 248->348, 330->128, 445->347/348/247, 428->128, 622->505/615/608, Kitchen->Memcare TV room, Dining Room->memcare piano. Excludes mesh-protected APs (2nd Floor Atrium, CC Bridge, salon, 206 U7 Pro) and Memcare TV room. APs 445/428 disables held pending further validation.
- **Deferred levers (separate session):** min-data-rate raise (1->12 Mbps), band-steering (`apply-wlan bandsteer`), 2.4 min-RSSI on the 6 OFF APs (615, 608, 505, 517, 622, salon), 5 GHz 80->40 MHz + non-DFS channel plan, 6 GHz band-steering.
- **5 GHz -- auto-channel made things WORSE.** Auto-channel reassignment applied via UniFi (Howard, 2026-06-17): co-channel pairs 25->30. **Option B** (combined 40MHz + non-DFS channel plan + min-RSSI -82 relax via per-AP radio_table PUT) is the plan. Dry-run clean; NOT applied (pending go-ahead + evening window).
- **Deferred levers (separate session):** min-data-rate raise (1->12 Mbps), band-steering (`apply-wlan bandsteer`), 2.4 min-RSSI on the 6 OFF APs (615, 608, 505, 517, 622, salon), 6 GHz band-steering.
- **Poly phone drops (2026-06-17) -- CLOSED.** Root cause = intentional pfSense reboot on 2026-06-16 22:38:12 MST (one fleet-wide event; 28/30 phones each dropped once, all floors, including floors 5/6 untouched by radio work). Only a gateway-level event explains all-floors-at-once. Today's data (2026-06-17) back to ~99.77%. NOT a WiFi or DHCP issue.
- **DHCP is healthy.** pfSense dhcpd.log: 1241 ACK / 1 NAK / 0 no-free-leases (verified via direct tail/grep -- NOT clog). Per-room /28 HIPAA segmentation is intentional (fullest 12/13); do NOT flatten. `sta_dhcp_failures` metric is client/WiFi-side (frames lost at 100% retry), not pfSense-side.
- **Config flags:** 6 APs with 2.4 min-RSSI OFF (615, 608, 505, 517, 622, salon); 4 APs off the 1/6/11 plan (128 disabled, 108 offline, 108U7 Pro auto, salon auto).
- **Mesh topology:** 2nd Floor Atrium is wireless-mesh parent for CC Bridge + salon (5 GHz backhaul ch36); 206 U7 Pro carries AP 108. These must NEVER be disabled or powered down via zone command -- coverage-thin auto-excludes them.
- **Known hardware:** AP 108 (Floor 1) offline pending a new cable run (expected). Stale duplicate controller object ("108" vs "108U7 Pro") to clean up separately.
@@ -351,13 +373,27 @@ Cascades' line-of-business / reporting SaaS (the systems they pull data OUT of,
- **Prior diagnostic (2026-05-16):** cloud API only, read-only; identified 2.4 GHz saturation hypothesis. Controller access was blocked at the time. Live controller access gained 2026-06-15 when Mike vaulted the SSH key and RW admin.
- **Tooling note:** `live-stats.sh` accuracy bugs fixed 2026-06-15 (removed 15-AP head cap, switched satisfaction to device-level, switched TX-retries to `tx_retries_pct` rate field, sorted worst-client list by satisfaction). These bugs caused a mid-session misdiagnosis that was corrected before session end.
### Known Issues / Pending Hygiene (as of 2026-06-16)
### VoIP / Network Device Migration
- **Re-VLANing a wired switch port requires a link bounce to force re-DHCP.** Changing the native VLAN on a UniFi switch port does not reset the NIC link; the device holds its old DHCP lease (renewal unicast to the old DHCP server is blocked by the new VLAN's firewall rules). Fix: bounce the port (PoE power-cycle for PoE devices; disable/enable via controller API for non-PoE). A UniFi client block/unblock is a MAC-address filter only -- it does NOT bounce the link. Controller API port-bounce requires the `X-CSRF-Token` from the login response header (`x-updated-csrf-token`). Confirmed on the Vertical-Remote desktop (2026-06-17).
### pfSense Operations
- **pfSense 25.07 logs are PLAIN TEXT, not binary clog.** Read with `tail`/`grep` directly (e.g., `tail -5000 /var/log/dhcpd.log`). Using `clog` returns empty output and will cause false conclusions. All log files confirmed ASCII text (`file /var/log/*.log`).
- **pfSense OpenVPN `--inactive` idle timeout:** The Cascades OpenVPN server (`ovpns1`) has a configured `--inactive` timeout (~300s). This disconnects idle clients after ~5 min of no tunnel data. Keepalive pings do NOT reset this counter (`--inactive` measures actual tunnel data, not keepalive packets). Symptom: OpenVPN Connect auto-reconnects repeatedly; the pfSense log shows `Inactivity timeout (--inactive), exiting`. This is a config setting, not a fault. Duplicate-CN events (which would indicate a different issue) are absent. Fix: raise or disable the `--inactive` parameter on the OpenVPN server profile. Fix proposed 2026-06-18; not yet applied (requires go-ahead).
- **pfSense dirty-boot / duplicate dhcpd:** After an unclean pfSense shutdown (e.g., power loss on surge-only UPS), ZFS survives but dhcpd may start twice. Symptom: DHCP DISCOVER->OFFER loop with no REQUEST/ACK completion (clients' OFFERs are handled by the wrong daemon instance). Fix: `killall dhcpd && echo "services_dhcpd_configure();" | /usr/local/sbin/pfSsh.php`; verify one instance: `pgrep -f "dhcpd -user" | wc -l` == 1. Note: `pfSsh.php` is slow (~20-40s); use timeout 60s+.
- **Post-outage device stragglers:** Devices that booted during a DHCP-down window cache a disconnected state and do not retry once the network recovers. A DHCP-log scan cannot find them (they stop sending DISCOVER). Realistic plan: reactive power-cycle as reports come in. Cox modem must be rebooted after a pfSense configuration restore (otherwise WAN may not fully re-establish).
### Known Issues / Pending Hygiene (as of 2026-06-18)
- **[BUG] Stale exclude-group on MFA-all-users policy:** The `Require multifactor authentication for all users` policy (`7e87a1c7...`) excludes `SG-Caregivers-Pilot` (`0674f0bc...`) instead of the live `SG-Caregivers` (`8b8d9222...`). Fix: PATCH `excludeGroups` to replace `SG-Caregivers-Pilot` with `SG-Caregivers`.
- **[DESIGN] ALIS-native 2FA is not a perimeter control.** The correct permanent model: force all ALIS logins through Entra SSO (SSO-only, credential fallback disabled). Office/privileged users should be standardized onto ALIS SSO as a separate workstream; ALIS-native 2FA should then be disabled per-user then globally.
- **[INFO] Android enrollment token expiry (2027-05-08) does NOT unenroll devices.** Renewal is needed only before enrolling new devices after that date.
- **[WARN] ~25 switch ports at 100 Mbps but gig-capable.** Physical: re-terminate/replace cable or check NIC. Investigate after WiFi Phase A remediation is stable.
- **[WARN] 3 offline switches** (Switch 2nd Floor #2, Switch 4th Floor #2, USW Pro Max 16). Root cause unknown; investigate onsite.
- **[WARN] 3 offline switches** (Switch 2nd Floor #2 -- reset+re-adopted 2026-06-17 after power outage but may still show in some monitors, Switch 4th Floor #2, USW Pro Max 16). Root cause unknown for #2 and #3; investigate onsite.
- **[SECURITY] Synology Cloud Signin Portal credential exposed in vault git history (commit 1fbc0e1).** `clients/cascades-tucson/synology-signin-portal.sops.yaml` was committed plaintext; encrypted go-forward but credential must be rotated. Verify MDM service account + WiFi CSCNet entries from the same commit were never plaintext.
- **[FLEET] Leftover Datto stack (CentraStage + Infocyte/DattoAV) -- not yet cleaned up.** Confirmed on CS-SERVER (thrashing degraded disk) and DESKTOP-TRCIEJA (Lupe Sanchez, causing dual-AV slow Excel). DESKTOP-TRCIEJA will be replaced (no cleanup needed on that box). CS-SERVER cleanup still open.
- **[WARN] DESKTOP-TRCIEJA duplicate computer name on network (Event 2505).** Recurring event on Lupe Sanchez's machine. Moot if machine is replaced; note for the replacement provisioning.
### Security Incidents (historical)
@@ -376,12 +412,28 @@ Cascades' line-of-business / reporting SaaS (the systems they pull data OUT of,
- **Backup gap closed (2026-06-15):** Mike installed ACG cloud backup (MSP360/CloudBerry -> ACG-backup server) on CS-SERVER. Verify first full backup completes and set retention; confirm image-based / bare-metal + system-state for DC recoverability.
- **Restored 7 deleted mailboxes (2026-04-25)** for HIPAA SS164.316(b)(2) 7-year retention.
- **Termination policy established:** Convert to shared mailbox, hide from GAL, retain 7 years.
- **Voice VLAN 30 (HIPAA-isolated):** All voice gear (phones + Vertical desktop) being migrated to an isolated network with internet/cloud-PBX egress only; blocked from PHI/LAN/VLAN20/mgmt. 22/22 Poly done; AudioCodes pending.
---
## Active Work
Primary active project as of 2026-05-24: dept-by-dept domain migration (Syncro #110680053). Syncro live pull 2026-06-16: **0 open tickets.**
Syncro live pull 2026-06-18: **0 open tickets.** No hours drawn from the 2026-06-17/18 sessions (all advisory/diagnostic/no billable infra changes).
**Non-Syncro follow-ups open as of 2026-06-18:**
- **[URGENT] Order replacement workstation for Lupe Sanchez (DESKTOP-TRCIEJA).** Decision made 2026-06-18. EOL Gateway ZX6971 / i3-2120 / 8 GB / Win11-unsupported. On new machine: provision GuruRMM + Bitdefender only; do NOT carry over the Datto stack.
- **[URGENT] Rotate exposed Synology Cloud Signin Portal credential.** Vault commit 1fbc0e1 committed it plaintext; encrypted go-forward but credential is exposed in git history. Also verify MDM service account + WiFi CSCNet from that same commit were never plaintext.
- **[IN PROGRESS] Voice VLAN (VLAN 30) AudioCodes cutover: 0/8 remaining.** 22/22 Poly + Vertical desktop DONE. Flip USW-16-PoE ports 1-8 native VLAN to VOICE + PoE power-cycle each AudioCodes to re-DHCP. Runbook: `docs/network/voice-vlan-cutover.md`; inventory: `docs/network/voice-phone-inventory.md`.
- **[PENDING] Wireless RF Phase 0 + Phase 1 (pending go-ahead + evening window):**
- Phase 0 (safe anytime): pfSense ping-check off for 240 DHCP pools, disable 3 AM AP firmware auto-upgrade, enable full pfSense logging (DHCP/DNS/firewall/system/gateway) with rotation.
- Phase 1 (windowed, per-zone, evening): combined per-AP radio_table PUT -- ng power medium (42 at-Low radios only, not the 24 disabled), na ht 40 (76 radios), na min_rssi -82 (69 at -77). Dry-run clean. Rollback auto-saved. Validate with watch-ap before/after.
- 5 GHz Option B (same window or separate): 40MHz + non-DFS channel plan + min-RSSI -82; width + channel are coupled (width alone fixed only 7/25 co-channel pairs). Auto-channel assignment made things worse -- do NOT use UniFi auto-channel; use `channel-plan na`.
- Standing rule: no Cascades prod-infra changes without discussing + explicit per-change go (memory `feedback_cascades.md` rule #4).
- **[PENDING] pfSense OpenVPN `--inactive` timeout fix.** Raise/disable the `--inactive` idle timeout on the Cascades OpenVPN server profile (~300s -> raise or remove). Proposed, not applied. Needs go-ahead.
- **[PENDING] Enable Netgate AutoConfigBackup** on pfSense (no off-box config backup existed before 2026-06-17 manual vault; on-box auto-backup exists but only one version). Also verify UPS covers core infra + PoE switches on battery-backed outlets (pfSense rectified; other gear not confirmed).
- **[PENDING] Synology Drive Team Folder migration (department shares -> CS-SERVER).** Diagnosis complete (2026-06-18): current Drive sync covers only the Sync-user's My Drive, not the real shared folders. Plan: Admin Console Team Folder enable + low versioning; Drive Client Download-only tasks into `D:\Shares\_SynMigration\<share>`; pilot on `/volume1/Server` (1.9 G, 2,486 files) first. Pending: confirm in-scope share list, confirm ALDocs coverage in real shares, get go-ahead to execute. Runbook (optional): `clients/cascades-tucson/docs/migration/synology-team-folder-migration.md`.
- **[PENDING] Watch for post-outage device stragglers.** Devices that booted during the 2026-06-17 DHCP-down window (duplicate dhcpd) may have cached a disconnected state. Kitchen thermal printer resolved by power-cycle. Expect additional IoT/printer/POS reports; fix each by power-cycle.
**Migration phase status (as of 2026-05-26):**
@@ -396,6 +448,7 @@ Primary active project as of 2026-05-24: dept-by-dept domain migration (Syncro #
| Megan Hiatt (Marketing) | COMPLETE 2026-05-27 -- domain joined via ProfWiz, folder redirection live, data on server |
| DESKTOP-KQSL232 (Lois Lane -- CareTakers) | Blocked -- Lois Lane resistant to change; John Trozzi working with her |
| CHEF-PC, SALES4-PC, MDIRECTOR-PC | Not yet started |
| DESKTOP-TRCIEJA (Lupe Sanchez) | **EOL hardware -- replace instead of migrate.** Decision 2026-06-18. |
**Blocking issues / pending:**
- M365 relicensing: 31 Business Standard -> Business Premium (SUSPENDED -- time-critical, 31 SPB seats free)
@@ -404,22 +457,16 @@ Primary active project as of 2026-05-24: dept-by-dept domain migration (Syncro #
- RECEPTIONIST-PC GuruRMM agent (9c91d324): flaky WebSocket, lagging fleet
- Entra Connect: OU=Administrative not yet in sync scope; UPN suffix updates for that OU pending
- NURSESTATION-PC: reboot required to activate `CSC - Caregiver Device Lockdown` GPO (deployed 2026-06-05; verify lock@3min, 90s warning, sign-out@15min, never-sleep)
- #32370 -- eFax/scanner onsite (Howard); verify/likely closed (Syncro live 2026-06-16 shows 0 open)
- #32370 -- eFax/scanner onsite (Howard); verify/likely closed (Syncro live 2026-06-18 shows 0 open)
- Caregiver device allow-list: ASSISTNURSE-PC needs re-join + re-tag after Win11 reinstall; LAPTOP-8P7HDSEI Win11 upgrade + join/tag still pending; then cutover (enable allow-list policy, disable compliance-block)
- ALIS office/privileged standardization: move office/managers/nurses to ALIS SSO-only; disable ALIS-native 2FA per-user then globally
- Fix stale `SG-Caregivers-Pilot` exclude-group on `Require MFA for all users` policy
- LAPTOP-8P7HDSEI: upgrade Win 10 -> Win 11 before PHI use
- Edge UNC download bug (Chromium 149): decide fix path for Ashley Jensen + Lois Lane and fleet; no fix applied as of 2026-06-08
- ALIS app session timeout: lower from 20 to 15 min (Howard, ALIS admin) -- PENDING
- **[CRITICAL] CS-SERVER degraded RAID-1 (2026-06-15):** OS mirror (C:) running on a single 320 GB laptop spindle, no redundancy. Plan SSD rebuild-then-swap (image C: first, AFTER backup verifies). DC migration is the real fix. Cloud backup installed/started 2026-06-15 -- **verify first full completes + confirm image-based + set retention before any drive work.**
- **[CRITICAL] CS-SERVER degraded RAID-1 (2026-06-15):** OS mirror (C:) running on a single 320 GB Hitachi 5400 RPM laptop spindle, no redundancy. Recommended replacement: 2x 480 GB enterprise 2.5" SATA SSD (e.g. Solidigm D3-S4520 or Samsung PM893; no Dell drive lockout; min size 480 GB class; SAS 6/iR controller, 3 Gbps, no TRIM). Hot-swap capable. Plan rebuild-then-swap (image C: first, AFTER backup verifies; re-pull OMSA live before any physical action). DC migration is the real fix.
- **[INFO] CS-SERVER cloud backup (MSP360/CloudBerry, installed 2026-06-15):** verify first full completes + confirm image-based / bare-metal + system-state + retention before any drive work.
- **[CLEANUP] CS-SERVER agent sprawl:** remove the previous MSP's leftover Datto RMM (CentraStage) + Datto EDR (Infocyte) stack (thrashing the degraded disk).
- **[PLANNED] Voice VLAN (VLAN 30) for Vertical phones + remote desktop:** vendor email sent 2026-06-16, awaiting Richard Turner's confirm (cloud-PBX confirmed via recon, desktop static, VPN cert CN) + maintenance window, then execute. Runbook: `clients/cascades-tucson/docs/network/voice-vlan-cutover.md`.
- **[IN PROGRESS] Wireless RF remediation (2.4 GHz):**
- Phase A (power-down to Low): Floor-4 pilot APPLIED 2026-06-16 (retry 13.2->9.5%, no coverage loss). Remaining floors (1-3, 5-6 + floor-2/misc per-AP) = staged, awaiting go-ahead. Runbook: `clients/cascades-tucson/reports/2026-06-16-2.4ghz-remediation-runbook.md`.
- Phase C (disable 9 redundant 2.4 radios): staged, awaiting Phase A validation + explicit go-ahead. APs 445/428 disables held; AP 128 disabled.
- Deferred: min-data-rate, band-steering, 2.4 min-RSSI, 5 GHz 80->40 MHz + non-DFS, 6 GHz steering.
- pfSense Phase A / gated controls: pfSense SSH backend (pfsense-ssh.sh) live 2026-06-16; firewall control verbs deferred to Mike (ROADMAP SS E).
- **[VERIFY] ~25 switch ports at 100 Mbps but gig-capable** (switch-audit.sh 2026-06-16): systematic cabling/NIC issue. Investigate after WiFi Phase A stable.
- **[PROPOSED] Unified KPI dashboard (Ashley Jensen):** scoped 2026-06-17; client one-pager drafted. Parked pending Ashley's day-one KPIs, data-freshness need, and POS/Focus-HR specifics. See Business Applications & Reporting Systems section. Next: deliver one-pager; confirm ALIS analytics/data-feed availability with Medtelligent.
---
@@ -461,15 +508,31 @@ Primary active project as of 2026-05-24: dept-by-dept domain migration (Syncro #
| 2026-06-16 | **Voice VLAN plan for Vertical phones (PLANNED, not executed).** Diagnosed split voice gear: Poly phones (22, WiFi/CSCNet/VLAN 20), AudioCodes (8, wired USW-16-PoE/Default LAN), Vertical desktop (wired, static, no ACG login). CSCNet confirmed as shared PPSK SSID (not simple staff/VLAN-20). GuruRMM recon: desktop RDP-only (not a PBX); CS-QB SMB-only/no SIP; phones likely cloud PBX. Designed VLAN 30 VOICE (10.0.30.0/24, isolated, internet-only egress); wrote cutover runbook (`docs/network/voice-vlan-cutover.md`); vendor email sent. Awaiting Richard's confirm + window. |
| 2026-06-16 | **pfSense confirmed as pfSense Plus 25.07-RELEASE; health verified; home-LAN shadow resolved.** Howard-Home renumbered from 192.168.0.0/24 to 10.137.42.0/24 (removed collision with Cascades 192.168.0.0/24). pfSense now reachable from Howard-Home over the site VPN. SSH health check: DHCP not exhausted, DNS up, WAN stable, states 28-31k/790k, load 0.6 -- gateway ruled out as WiFi factor. `pfsense-ssh.sh` backend built and validated live (SSH, no RESTAPI package needed). |
| 2026-06-16 | **Floor-4 2.4 GHz power-down pilot applied (first production RF change).** 14/15 Floor-4 radios set to 6 dBm (from ~23); avg retry 13.2->9.5% (~28% fewer retransmits); clients retained, no coverage loss. AP 445 lagged (left alone, harmless). AP-hang recovery procedure learned: `device-control poe-cycle` (NOT force-provision -- took 445 offline; removed from the tool). `dfs-check.sh` confirmed ZERO real radar events fleet-wide (DFS empirically clean). `unifi-wifi` skill feature-complete (WiFi monitor/tune/apply + switch/gateway/pfSense-SSH + multi-client + channel-plan + cron health). |
| 2026-06-17 | **KPI dashboard scoping for Ashley Jensen (advisory; no infra touched).** Reframed her Power BI Gateway question (gateway is on-prem-only, not a SaaS connector). Catalogued the 9 reporting systems (ALIS/QuickBooks/Bill.com/Relias/You've Got Leads/TELS/Focus HR/Helpany/POS). Recommended Phase 1 (exportsSharePointPower BI Pro) Phase 2 (Power Automate for Bill.com/QBO), leveraging existing M365 Business Premium. Wrote internal scoping note + client-facing one-pager (with cost line) under `docs/proposals/`. Parked pending Ashley's KPIs + freshness + POS/Focus-HR specifics. |
| 2026-06-17 | **KPI dashboard scoping for Ashley Jensen (advisory; no infra touched).** Reframed her Power BI Gateway question (gateway is on-prem-only, not a SaaS connector). Catalogued the 9 reporting systems (ALIS/QuickBooks/Bill.com/Relias/You've Got Leads/TELS/Focus HR/Helpany/POS). Recommended Phase 1 (exports->SharePoint->Power BI Pro) -> Phase 2 (Power Automate for Bill.com/QBO), leveraging existing M365 Business Premium. Wrote internal scoping note + client-facing one-pager (with cost line) under `docs/proposals/`. Parked pending Ashley's KPIs + freshness + POS/Focus-HR specifics. |
| 2026-06-17 | **Voice VLAN 30 built + verified; Vertical desktop + initial Poly phones migrated.** Richard Turner confirmed window; VLAN 30 pfSense interface (igc1.30, 10.0.30.0/24) + isolation rules built (clone of GUEST VLAN, Protocol=Any; verified via `pfctl -sr`). UniFi VOICE network + CSCNet voice PPSK created (vaulted). Vertical desktop migrated (port-16 bounce via controller API with CSRF token; re-DHCP'd to 10.0.30.201). Key learnings: desktop is DHCP (not static), Vertical uses LogMeIn (not pfSense OpenVPN), re-VLAN wired port requires link bounce. |
| 2026-06-17 | **Poly phone drops root-caused and closed; whole-network smoothing plan built (dry-run only).** Phone drops = intentional pfSense reboot on 2026-06-16 22:38 MST (transient, one-time, resolved). DHCP server healthy (1241 ACK/1 NAK/0 no-free-leases, read directly from plain-text log). Per-room /28 HIPAA segmentation confirmed intentional + healthy. Produced prioritized smoothing plan (Phase 0 + Phase 1 radio_table PUT). Nothing applied. Hard rule established: no Cascades prod-infra changes without discussing + explicit per-change go. |
| 2026-06-17 | **CS-SERVER drive review (advisory).** Confirmed surviving drive: Hitachi HTS545032B9A300 (0:0:2, 320 GB SATA, 5400 RPM). Recommended replacement drives: 2x 480 GB enterprise SATA SSD (e.g. Solidigm D3-S4520 or Samsung PM893), hot-swap on R610 backplane. No server-side commands run; gated on backup verification. |
| 2026-06-17 | **Power outage -- full site down + recovery.** All 77 APs + 12 switches disconnected. Root cause: pfSense on UPS surge-only side (no battery) -> unclean shutdown -> ZFS OK, but duplicate dhcpd + 2nd-floor switch one-way L2 forwarding. Howard: killed duplicate dhcpd + clean restart remotely. Mike: moved pfSense to battery outlets, restored config from on-box auto-backup (VLAN30 intact), reset+re-adopted Switch 2nd Floor #2 (USL24PB), rebooted Cox modem. Network fully restored. Separately: 5GHz auto-channel applied (co-channel 25->30, worse). pfSense config vaulted. Pre-existing plaintext Synology signin credential found in vault history (commit 1fbc0e1) -- encrypted go-forward; needs rotation. Incident report: `clients/cascades-tucson/reports/2026-06-17-power-outage-incident.md`. |
| 2026-06-18 | **Power outage follow-ups: OpenVPN flapping root-caused; kitchen printer casualty resolved.** OpenVPN disconnect/reconnect cycle = configured `--inactive` idle timeout (~300s) on the pfSense server, not a fault. Fix proposed (raise/disable); not applied. Kitchen thermal printer (iPad POS) would not print post-outage -- booted during DHCP-down window, cached disconnected state; fixed by power-cycle. DHCP straggler sweep: 13/13 active senders completing, 0 stuck. |
| 2026-06-18 | **Synology Drive sync architecture diagnosed; Team Folder migration plan produced.** Current Drive sync is Sync-user My Drive only (not the real shared folders). Real NAS shares (Server 1.9 G, Management 5.5 G, Public ~50 G, SalesDept ~23 G) are not mirrored. Plan: Team Folder Download-only tasks into `D:\Shares\_SynMigration\<share>` staging; pilot on `/volume1/Server`. No changes made. |
| 2026-06-18 | **DESKTOP-TRCIEJA (Lupe Sanchez) performance diagnosed; replace-not-remediate decision.** Root causes: (a) EOL hardware -- Gateway ZX6971 AIO, Intel i3-2120 (2011, 2C/4T), 8 GB RAM, Win11 unsupported; (b) dual real-time AV -- ACG Bitdefender (keep) + leftover Datto stack (Datto RMM/CentraStage + Datto EDR/Infocyte + bundled DattoAV) both scanning every file on a 2-core CPU under memory pressure. OneDrive ruled out (desktop is local). Howard decided: no remediation; order replacement. Another instance of the fleet-wide leftover-Datto-stack cleanup. |
---
## Compilation Notes
**Session logs read:** all prior sessions + 2026-06-15/16 logs (wireless RF audit, CS-SERVER RAID + VPN reset, voice VLAN plan) + 2026-06-17 KPI-dashboard-scoping log + 2 proposal docs (kpi-dashboard, kpi-dashboard-onepager) + 2 reports (unifi-full-audit, 2.4ghz-remediation-runbook) + 9 memory files. Date range: 2026-03-06 through 2026-06-17.
**Session logs read:** all prior sessions + 2026-06-17/18 logs: voice VLAN 30 build + Poly cutover, Poly phone-drop root cause + wireless smoothing plan, power-outage recovery + 5GHz option analysis, CS-SERVER drive review, KPI dashboard scoping, power-outage follow-up (OpenVPN + printer), Synology Drive sync diagnosis, DESKTOP-TRCIEJA (Lupe Sanchez) perf diagnosis. Date range: 2026-03-06 through 2026-06-18.
**New this compile (2026-06-17):** added Business Applications & Reporting Systems section (9 LOB/reporting SaaS catalogued) + the proposed unified KPI dashboard (Ashley Jensen). Advisory-only session; no infrastructure changed. All RF / migration / HIPAA state unchanged from the 2026-06-16 compile.
**New this compile (2026-06-18):**
- Voice VLAN 30: status updated to 22/22 Poly + desktop DONE; AudioCodes 0/8 still pending. PPSK vaulted. Wired-port link-bounce pattern documented.
- Power outage (2026-06-17): full incident documented. pfSense UPS placement rectified. Duplicate dhcpd, 2nd-floor switch L2 failure, Cox modem reboot step. Post-outage straggler pattern (power-cycle) documented. pfSense config vaulted. Synology signin credential exposure flagged (vault commit 1fbc0e1).
- Wireless: Phase A extended overnight 2026-06-17 and over-thinned (retry 17->23.4%, satisfaction 39->30). 5GHz auto-channel made co-channel overlap worse. Both corrective plans staged (Low->Medium, Option B) but not applied. Phone drop mystery closed (intentional pfSense reboot).
- DESKTOP-TRCIEJA (Lupe Sanchez): added to key contacts and migration table; EOL hardware + dual-AV root cause; replace decision.
- pfSense patterns: plain-text logs (not clog), --inactive OpenVPN timeout, dirty-boot/duplicate-dhcpd recovery, post-outage stragglers.
- Synology Drive sync architecture documented (current scope is Sync-user My Drive only; Team Folder migration plan for department shares).
- Active Work: updated with all new non-Syncro follow-ups (Lupe replacement, Synology credential rotation, AudioCodes cutover, Phase 0+1 wireless, OpenVPN fix, AutoConfigBackup, Team Folder migration, outage stragglers).
- Sources: 7 new session-log paths appended (2026-06-17-poly-phone-drops, 2026-06-17-power-outage, 2026-06-17-cs-server-drive-review, 2026-06-17-voice-vlan30-build, 2026-06-18-outage-followup-openvpn-printer, 2026-06-18-synology-drive-sync, 2026-06-18-lupesanchez-perf-diag); voice-phone-inventory.md added.
- Billing: hours 55.75 (unchanged; no draws from 2026-06-17/18 sessions); date updated to 2026-06-18.
**Client folder:** `clients/cascades-tucson/` (NOT `clients/cascades/` -- that directory does not exist).
@@ -478,29 +541,28 @@ Primary active project as of 2026-05-24: dept-by-dept domain migration (Syncro #
- Audit retention infra -- approved 2026-04-29, not yet built
- dunedolly21@gmail.com guest invite -- confirm with Lauren
- Windows MDM auto-enroll scope -- confirm in portal (Entra -> Devices -> Mobility -> Microsoft Intune -> MDM user scope)
- #32370 -- verify/likely closed; Syncro live 2026-06-16 shows 0 open tickets
- #32370 -- verify/likely closed; Syncro live 2026-06-18 shows 0 open tickets
- Edge UNC download bug fix path -- no fix applied as of 2026-06-08; decision pending Howard
- ALIS BAA with Medtelligent -- not yet verified; confirm with Meredith (also: does ALIS offer a built-in analytics / data feed? relevant to the KPI dashboard)
- KPI dashboard (Ashley Jensen) -- parked; need day-one KPIs, data-freshness need, POS product + Focus HR plan before scoping a build
- JD Martin (jd.martin@cascadestucson.com) -- confirmed Syncro contact; role not yet documented
- CS-SERVER cloud backup: verify first full completes, confirm image-based / bare-metal + system-state, set retention; only then proceed with RAID remediation
- NURSESTATION-PC: verify `CSC - Caregiver Device Lockdown` GPO activated (requires reboot; verify lock@3min, 90s warning, sign-out@15min, never-sleep)
- Wireless RF: Floors 1-3, 5-6 power-down + Phase C disables pending scope go-ahead from Howard
- Wireless RF: Phase 0 + Phase 1 (Low->Medium + Option B) pending scope go-ahead from Howard; windowed evening session needed
- Christine (room 515, Poly phone 10.0.30.220) -- last name noted as "~Nyuda -- VERIFY"
- UPS coverage: confirm all core infra + PoE switches are battery-backed (pfSense rectified; others not confirmed)
- Netgate AutoConfigBackup: not yet enabled
- Synology signin credential rotation: exposed in vault history commit 1fbc0e1; encrypted go-forward but must rotate
**Resolved since last compile (2026-06-15 -> 2026-06-16):**
- Howard-Home LAN shadow: resolved 2026-06-16 (renumbered to 10.137.42.0/24; Cascades 192.168.0.x now reachable over VPN)
- pfSense version: confirmed pfSense Plus 25.07-RELEASE (was listed as "pfSense 24.0")
- pfSense gateway: ruled out as WiFi factor (health check 2026-06-16)
- DFS empirically clean: dfs-check.sh confirmed ZERO radar events fleet-wide (was theoretical concern)
- Floor-4 2.4 GHz power-down: applied (first production RF change; retry 13.2->9.5%)
- unifi-wifi skill: feature-complete as of 2026-06-16 (WiFi/switch/gateway/pfSense-SSH, all gated writes validated)
**Carried forward from prior compile:**
- Wireless controller access unblocked (2026-06-15): SSH/Mongo + RW API + AP creds all vaulted; live RF audit completed; tuning plan staged
- CS-SERVER RAID degraded + cloud backup installed (2026-06-15)
- Voice VLAN VLAN 30 plan + runbook (2026-06-16); vendor email sent; awaiting confirm
- CSCNet SSID correction: shared PPSK SSID (~230 per-key->network mappings), not "staff/VLAN-20"
- Shared mailboxes grievances@ + Surveys@ created and delegated (2026-06-12): ticket #32417 Invoiced; prepay block 55.75h
**Resolved since last compile (2026-06-17 -> 2026-06-18):**
- Poly phone drops: closed (intentional 2026-06-16 pfSense reboot; transient)
- Voice VLAN 30: built, verified, Poly cutover complete (22/22); Vertical desktop done
- pfSense 25.07 log format: documented (plain text, not clog)
- pfSense config backup: vaulted post-restore (2026-06-17)
- pfSense on battery-backed UPS: rectified (Mike, 2026-06-17)
- Kitchen printer / post-outage straggler: resolved by power-cycle (2026-06-18)
- Synology Drive sync architecture: diagnosed (2026-06-18; Team Folder plan produced)
- DESKTOP-TRCIEJA root cause: identified (dual AV + EOL hardware); decision made (replace)
## Backlinks

View File

@@ -0,0 +1,55 @@
---
type: client
name: darrell-delphen
display_name: Darrell Delphen
last_compiled: 2026-06-18
compiled_by: GURU-5070/claude-main
sources:
- clients/darrell-delphen/session-logs/2026-06/2026-06-18-mike-email-link-sni-block.md
backlinks: []
---
# Darrell Delphen
## Profile
- **Contract type:** Break-fix (no prepaid block)
- **Key contacts:** Darrell Delphen
- **Billing rate:** $150/hr remote (most recent labor)
- **Location:** Yantis, TX
- **Syncro customer ID:** 35996725
- **Managed devices:** GuruRMM agent on DDDOffice072023 (0 assets recorded in Syncro)
## Infrastructure
### Servers & Services
| Host | IP | Role | OS | Notes |
|---|---|---|---|---|
| DDDOffice072023 | LAN behind 192.168.1.1 | Workstation | Windows | GuruRMM agent `000ed57d-fd05-4001-871c-244f43155c16` (v0.6.66); ISP egress 167.89.210.225 |
### Email & Identity
- **Mail security:** Intermedia Email Protection (rewrites links to `url.emailprotection.link`). Mailbox accessed via Outlook. (verify tenant/host details)
### Network
- **ISP / WAN:** ISP-provided/managed **Extreme EXOS** gateway (LAN gateway 192.168.1.1, egress 167.89.210.225). Client has no login to this device — changes require ISP escalation.
- **Firewall:** the EXOS gateway runs ISP-side web/URL filtering (the "NetIQ" feature) capable of SNI-based TLS inspection.
- **VPN:** (verify — none known)
## Access
- Remote management: GuruRMM (agent on DDDOffice072023)
- Vault path: (verify — no client credentials vaulted)
## Patterns & Known Issues
- **ISP gateway SNI filtering breaks Intermedia/Outlook email links.** The ISP-managed Extreme EXOS gateway's "NetIQ" URL-filtering feature has SNI-intercepted and reset TLS to `url.emailprotection.link` (Intermedia's link-rewriter), producing `ERR_CONNECTION_ABORTED` / SChannel `0x80090326` (SEC_E_ILLEGAL_MESSAGE) on Outlook links while Gmail links (Google redirector) worked. The client cannot change this device — resolution requires an ISP ticket. If it recurs, suspect the "NetIQ" feature being re-enabled and confirm with an SNI-varied handshake test against `199.193.205.140`.
## Active Work
*No open tickets in Syncro as of 2026-06-18.*
## History Highlights
- **2026-06-18** — Diagnosed Outlook email links failing (`ERR_CONNECTION_ABORTED`). Root cause: ISP EXOS gateway "NetIQ" feature SNI-blocking Intermedia's `url.emailprotection.link` rewriter. Deployed Cloudflare WARP as interim bypass; ISP disabled the feature for the permanent fix; verified native path and removed WARP. Billed 1.0h remote ($150, Syncro #32437 / invoice #67853).
## Backlinks
*(none yet)*

View File

@@ -0,0 +1,91 @@
---
type: client
name: patriot-internal-medicine
display_name: Patriot Internal Medicine
last_compiled: 2026-06-18
compiled_by: Mikes-MacBook-Air/claude-main
sources:
- session-logs/2026-06/2026-06-18-mike-acg-website-phase3b-pim-onboarding.md
- clients/patriot-internal-medicine/gururmm-site-tucson.sops.yaml (vault)
- clients/patriot-internal-medicine/gururmm-site-sonoita.sops.yaml (vault)
backlinks:
- projects/gururmm
---
# Patriot Internal Medicine
Medical practice with two locations: Tucson (primary) and Sonoita. Onboarded to GuruRMM on 2026-06-18. No agents deployed yet — enrollment pending.
---
## Profile
- **Contract type:** [unverified — not documented]
- **Key contacts:** [not yet captured]
- **Billing rate:** [unverified — not documented]
- **Syncro customer ID:** [not yet documented]
- **Active tickets:** None
---
## Infrastructure
### Servers & Services
No servers or infrastructure documented yet. Medical practice with two physical locations.
### Network
No network details captured yet.
---
## GuruRMM Enrollment
- **Client name in RMM:** Patriot Internal Medicine
- **Client ID:** `9c69dc3c-5fa1-4878-8b9d-f69c36df2a67`
**Sites:**
| Site Name | Site ID | Site Code | Enrollment URL | Vault Path |
|---|---|---|---|---|
| Tucson | `8c4fcf01-5fca-4a40-83e7-75b87f658b72` | `NORTH-WOLF-6270` | https://rmm.azcomputerguru.com/install/NORTH-WOLF-6270 | `clients/patriot-internal-medicine/gururmm-site-tucson.sops.yaml` |
| Sonoita | `2ec7dcc5-91c3-4182-bfba-cd2cf9a43349` | `LIGHT-HARBOR-9617` | https://rmm.azcomputerguru.com/install/LIGHT-HARBOR-9617 | `clients/patriot-internal-medicine/gururmm-site-sonoita.sops.yaml` |
**Enrolled agents:** None — no agents deployed yet as of 2026-06-18.
---
## Access
- **GuruRMM (external):** https://rmm.azcomputerguru.com
- **Vault paths:**
- `clients/patriot-internal-medicine/gururmm-site-tucson.sops.yaml` — Tucson site enrollment key and API credentials
- `clients/patriot-internal-medicine/gururmm-site-sonoita.sops.yaml` — Sonoita site enrollment key and API credentials
---
## Active Work
As of 2026-06-18:
- **[OPEN] Deploy agents:** Install GuruRMM agents at Tucson and Sonoita endpoints using the enrollment URLs above
- **[OPEN] Run onboarding diagnostics:** After first agents enroll, run baseline security and infrastructure scans
- **[OPEN] Capture contact information:** Document primary contacts, phone numbers, and escalation paths
- **[OPEN] Determine contract type:** Verify prepaid block, managed services, or break-fix arrangement
- **[OPEN] Infrastructure discovery:** Document servers, network topology, line-of-business applications, backup systems
- **[OPEN] M365 tenant verification:** Check if practice has Microsoft 365 tenant and onboard remediation tools if present
---
## History Highlights
| Date | Event |
|---|---|
| 2026-06-18 | Client "Patriot Internal Medicine" created in GuruRMM (ID: 9c69dc3c). Two sites provisioned: Tucson (NORTH-WOLF-6270) and Sonoita (LIGHT-HARBOR-9617). Enrollment keys vaulted. Discord alerts posted to #dev-alerts. |
---
## Backlinks
- [[projects/gururmm]] — Client and sites provisioned; no agents enrolled yet

View File

@@ -1,7 +1,7 @@
# Wiki Index
Last updated: 2026-06-17
Compiled by: HOWARD-HOME/claude-main
Last updated: 2026-06-18
Compiled by: GURU-5070/claude-main
This wiki is LLM-maintained. Do not edit articles manually — run `/wiki-compile` to update.
Run `/wiki-lint` to check for stale entries and broken backlinks.
@@ -18,19 +18,21 @@ Run `/wiki-lint` to check for stale entries and broken backlinks.
| Article | Summary | Last Compiled |
|---|---|---|
| [Cascades of Tucson](clients/cascades-tucson.md) | Prepaid block $175/hr, **55.75 hrs remaining** (live 2026-06-16); senior living; active domain migration + HIPAA compliance project; single DC on aging R610 hardware; caregiver restricted-access model PROVEN 2026-06-05: Hybrid Entra Join + CA allow-list + ALIS SSO validated on NURSESTATION-PC/pilot.test; GPO `CSC - Caregiver Workstation` (shortcuts + printers) built + validated; GPO `CSC - Caregiver Device Lockdown` deployed (HIPAA auto-logoff, activates on reboot); INTUNE_A PendingInput tenant-wide (MS case open; GPO path used instead); folder-redirection root cause fixed 2026-06-08 (fdeploy.ini); shared mailboxes grievances@/Surveys@ created + delegated 2026-06-12 (#32417); Monday cutover to real caregivers pending; #32383 (bill.com/BOK chris.knight) Resolved; UniFi wifi RF (77 U7-Pro APs/~587 clients via UOS controller): 2.4GHz over-coverage = primary pain; pfSense ruled out as cause; Floor-4 power-down pilot applied 2026-06-16 (retry 13.2->9.5%); coverage-thin disable plan + 2.4 remediation runbook staged; DFS empirically clean; 6GHz untapped; CS-SERVER OS RAID-1 degraded 2026-06-15 (data-loss risk; cloud backup now started); Voice VLAN (VLAN 30) consolidation planned 2026-06-16 for Vertical phones + remote desktop (CSCNet confirmed a shared PPSK SSID); KPI dashboard for Ashley Jensen scoped 2026-06-17 (Power BI + SharePoint phased plan, parked); Syncro 0 open tickets | 2026-06-17 |
| [Cascades of Tucson](clients/cascades-tucson.md) | Prepaid block $175/hr, **55.75 hrs remaining** (live 2026-06-18); senior living; active domain migration + HIPAA compliance project; single DC on aging R610 hardware; caregiver restricted-access model PROVEN 2026-06-05: Hybrid Entra Join + CA allow-list + ALIS SSO validated on NURSESTATION-PC/pilot.test; GPO `CSC - Caregiver Workstation` (shortcuts + printers) built + validated; GPO `CSC - Caregiver Device Lockdown` deployed (HIPAA auto-logoff, activates on reboot); INTUNE_A PendingInput tenant-wide (MS case open; GPO path used instead); folder-redirection root cause fixed 2026-06-08 (fdeploy.ini); shared mailboxes grievances@/Surveys@ created + delegated 2026-06-12 (#32417); Monday cutover to real caregivers pending; #32383 (bill.com/BOK chris.knight) Resolved; UniFi wifi RF (77 U7-Pro APs/~587 clients via UOS controller): 2.4GHz over-coverage = primary pain; pfSense ruled out as cause; Floor-4 power-down pilot applied 2026-06-16 (retry 13.2->9.5%); coverage-thin disable plan + 2.4 remediation runbook staged; DFS empirically clean; 6GHz untapped; CS-SERVER OS RAID-1 degraded 2026-06-15 (data-loss risk; cloud backup now started); Voice VLAN (VLAN 30) consolidation planned 2026-06-16 for Vertical phones + remote desktop (CSCNet confirmed a shared PPSK SSID); KPI dashboard for Ashley Jensen scoped 2026-06-17 (Power BI + SharePoint phased plan, parked); Voice VLAN 30 built + 22/22 Poly cut over 2026-06-17 (AudioCodes 0/8 pending); building power outage 2026-06-17 (pfSense on UPS surge-only side) full site down + recovered; DESKTOP-TRCIEJA (Lupe Sanchez) slow Excel diagnosed 2026-06-18 = EOL i3-2120 hardware + dual real-time AV (leftover Datto stack) -> replace machine; Syncro 0 open tickets | 2026-06-18 |
| [Dataforth Corporation](clients/dataforth.md) | Prepaid block ~$2,099/mo, 34.5 hrs remaining; signal conditioning manufacturer; 64 DOS test stations; 2025 crypto attack recovery + incomplete restore (files dropped across shares — migration-gap audit in progress); 2026-03-27 phishing incident + MFA rollout; active test datasheet pipeline project; Neptune Exchange colocated at D2; 2026-06-04 SP1366 file recovery (19/20 PDFs restored from HGHAUBNER pre-attack backup); GuruRMM fleet 13→45 agents; 2026-06-02 Syncro asset reconciliation (78→20 keep/21 flag/28 remove/9 verify); fleet-wide Syncro agent break ~2025-10-06; Bitdefender phase-off in progress | 2026-06-04 |
| [Instrumental Music Center](clients/instrumental-music-center.md) | Prepaid block $175/hr, 12.5 hrs remaining; music retail/repair; AIMsi POS on SQL Server 2019; phantom DC causing slow logons; GuruRMM enrolled (IMC1) | 2026-05-24 |
| [Valley Wide Plastering](clients/valleywide.md) | Prepaid block, 10 hrs remaining; plastering/stucco contractor; HP DL360 Gen10 + XenServer; VB6 app modernization project; RDWeb brute-force incident; 11 Yealink phones pending | 2026-06-14 |
| [ACG Internal Infrastructure](clients/internal-infrastructure.md) | ACG's own hosting infra — Neptune Exchange (cert expires 2026-05-31, DkimSigner disabled), IX server, Cloudflare tunnel workaround, ACG M365 tenant gaps | 2026-05-24 |
| [BirthBiologic](clients/birth-biologic.md) | Bio/healthcare; BB-SERVER (WS2016) GuruRMM enrolled; Datto→SharePoint migration incomplete; M365 apps partially consented | 2026-05-24 |
| [CryoWeave](clients/cryoweave.md) | Custom cryogenic cable assemblies; cPanel on IX; website redesign + SEO project in progress; Syncro ID not documented | 2026-05-24 |
| [Darrell Delphen](clients/darrell-delphen.md) | Break-fix residential (Yantis, TX); single Windows workstation DDDOffice072023 (GuruRMM); 2026-06-18 Outlook email links failing = ISP-managed Extreme EXOS gateway "NetIQ" SNI-filtering of Intermedia's url.emailprotection.link rewriter (WARP interim bypass, ISP disabled the feature for permanent fix); Syncro #35996725 | 2026-06-18 |
| [Glaz-Tech Industries](clients/glaztech.md) | ~200 users, 9 locations; prepaid ~22.25 hrs; web server WWW (192.168.8.72 / 65.113.52.88) — IIS 10/VB.NET e-commerce; CRITICAL security posture: website connects to GTI-INV-SQL as sysadmin (login `tom`, named SQL login, C0 top finding) + plaintext PANs+CVV (stored by GTIware PSA, not website) + plaintext passwords + SQLi via `quo()` + XSS; apex 404 fixed + payment TLS fixed 2026-06-03; intrusion/brute-force log review 2026-06-04 (no attacker found; H5 detection blind spot confirmed — HTTP 200 on both success/failure + no failed-login logging); #32378 Waiting on Customer (assessment + reports + Appendix A delivered); M365 no MFA; SCL bypass rules for vendor DMARC + MailProtector digests | 2026-06-04 |
| [Grabb & Durando Law Office](clients/grabb-durando.md) | Personal injury law firm; GND-SERVER GuruRMM enrolled; AI demand review app scoped ($4K$7K); website migration pending; plaintext DB password in README needs vaulting | 2026-05-24 |
| [Pavon](clients/pavon.md) | Former/archive client; GeoVision NVR surveillance; OwnCloud at 172.16.3.22 backed by Uranus; cron stacking fixed; Nextcloud migration deferred 36 months | 2026-05-24 |
| [Rieusset Corp (Tom Sorensen)](clients/rieusset-corp.md) | Small business; email hosted on Neptune Exchange (4 mailboxes: tsorensen, tomrc, ojodeagua, csorensen @rieussetcorp.com); Mailprotector domain ID 57833; outbound via SBR Outbound.Sorensen connector; clipto.com allow rule added 2026-06-08 | 2026-06-08 |
| [Rednour Law Offices](clients/rednour.md) | Law firm; M365 rednourlaw.com (tenant 4a4ca18a) fully onboarded 2026-05-31; all 5 ComputerGuru SPs consented; no MDE license; 3 workstations GuruRMM enrolled (FRONTDESKRECEPT/LEGALASST/REDNOURCARRIEVI); Carla Skinner renamed from Emma; prior MSP agents (ScreenConnect/Splashtop/Datto) still present; shared-drive access for Nick Pafford deferred | 2026-06-02 |
| [Peaceful Spirit Therapeutic Massage](clients/peaceful-spirit.md) | Massage therapy practice; PST-SERVER (192.168.0.2) + 5 GuruRMM agents; L2TP/IPsec RRAS VPN complete; 2026-06-04 site-wide outage resolved (UDR Ultra reboot dropped VPN port-forward, re-added in controller); BridgettePSHomeComputer re-enrolled (new UUID 01160fc8); vault drift open (pst-admin password); Syncro 278525 (Peaceful Spirit Massage) | 2026-06-04 |
| [Patriot Internal Medicine](clients/patriot-internal-medicine.md) | Medical practice, two locations (Tucson + Sonoita); GuruRMM client+sites provisioned 2026-06-18 (Tucson: NORTH-WOLF-6270, Sonoita: LIGHT-HARBOR-9617); no agents deployed yet; enrollment keys vaulted; infrastructure discovery pending | 2026-06-18 |
| [Sombra Residential LLC](clients/sombra-residential.md) | Property management; Server2013 (actually WS2012 EOL, unpatched) + DESKTOP-UQRN4K3 GuruRMM enrolled; Transwiz migration artifacts cause Office credential prompts | 2026-05-24 |
| [Stamback Septic](clients/stamback-septic.md) | Septic services; prepaid block ~3.5 hrs remaining; DESKTOP-BTR2AM3 + StambackLaptopNew GuruRMM enrolled; OneDrive identity wipe pattern documented | 2026-05-24 |
| [BG Builders LLC](clients/bg-builders.md) | Construction; M365 bgbuildersllc.com (CIPP: sonorangreenllc.com); terminated employee (Lesley Roth) — account disabled, litigation hold, device wipes pending; no Intune | 2026-05-24 |
@@ -107,6 +109,7 @@ Run `/wiki-lint` to check for stale entries and broken backlinks.
| Grabb & Durando Law Office | GND-SERVER (WS2019, GuruRMM enrolled) | GuruRMM; AI demand review app (scoped) |
| Pavon | OwnCloud VM (172.16.3.22), Uranus /Archive storage | — |
| Peaceful Spirit | PST-SERVER (192.168.0.2, agent 87293069), UCG-PST-CC UDR Ultra (192.168.0.10 / 98.190.129.150), 4 workstations | GuruRMM |
| Patriot Internal Medicine | Two locations (Tucson, Sonoita); no systems documented yet | GuruRMM (client+sites provisioned, no agents deployed yet) |
| Sombra Residential LLC | Server2013 (WS2012 EOL) + DESKTOP-UQRN4K3, GuruRMM enrolled | GuruRMM |
| Stamback Septic | DESKTOP-BTR2AM3 + StambackLaptopNew, GuruRMM enrolled | GuruRMM |
| BG Builders LLC | M365 bgbuildersllc.com; no on-prem infra documented | — |