Skip to content

Commit 2cf6f52

Browse files
authored
Merge pull request #157 from HanSur94/claude/upbeat-jackson-9400d5
v3.1 Plant Log Integration (5 phases + demo wiring)
2 parents d8f740e + e2ded77 commit 2cf6f52

71 files changed

Lines changed: 16482 additions & 62 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.planning/ROADMAP.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
-**v2.0 Tag-Based Domain Model** — Phases 1004-1011 (shipped 2026-04-17)
1010
- 📋 **v2.1 Tag-API Tech Debt Cleanup** — Phases 1012-1017 (carry-forward, parallel — not active)
1111
-**v3.0 FastSense Companion** — Phases 1018-1023 + 1023.1 gap closure (shipped 2026-04-30)
12+
-**v3.1 Plant Log Integration** — Phases 1034-1038 (shipped 2026-05-19; phases renumbered from 1029-1033 on merge to resolve collision with parallel v4.0 development)
1213
- 🚧 **Pending milestone** — Phases 1025-1028 (promoted from backlog 2026-05-08, awaiting milestone scoping; 1024 closed via quick task 260508-d7k; 1025/1026 substantially addressed via quick tasks 260508-d8y/260508-das)
1314
- 🚧 **v4.0 Multi-User LAN Concurrency** — Phases 1029-1033 (active, started 2026-05-13)
1415

@@ -25,6 +26,21 @@
2526

2627
</details>
2728

29+
<details>
30+
<summary>✅ v3.1 Plant Log Integration (Phases 1034-1038) — SHIPPED 2026-05-19</summary>
31+
32+
- [x] Phase 1034: Plant Log Storage Foundation (3/3 plans) — completed 2026-05-13 (originally Phase 1029)
33+
- [x] Phase 1035: CSV/XLSX Import + Mapping Dialog (3/3 plans) — completed 2026-05-13 (originally Phase 1030)
34+
- [x] Phase 1036: Live Tail + Slider Preview Overlay (3/3 plans) — completed 2026-05-14 (originally Phase 1031)
35+
- [x] Phase 1037: Per-Widget Plant Log Overlay (3/3 plans) — completed 2026-05-19 (originally Phase 1032)
36+
- [x] Phase 1038: Dashboard + Companion Integration & Serialization (3/3 plans) — completed 2026-05-19 (originally Phase 1033)
37+
38+
Note: v3.1 was developed in parallel with v4.0 in a separate worktree and chose phase numbers 1029-1033 before learning v4.0 had already claimed them on main. The phases were renumbered to 1034-1038 on merge. Original phase numbers are preserved in commit messages (`feat(1029-01): ...`, etc.) and in the milestone archive (`milestones/v3.1-ROADMAP.md`).
39+
40+
Full details: [milestones/v3.1-ROADMAP.md](milestones/v3.1-ROADMAP.md)
41+
42+
</details>
43+
2844
<details>
2945
<summary>🚧 Pending milestone (Phases 1025-1028) — promoted from backlog 2026-05-08</summary>
3046

@@ -133,6 +149,11 @@ Full details: [milestones/v3.0-ROADMAP.md](milestones/v3.0-ROADMAP.md)
133149
| 1031. EventLog + EventStore rollback-mode migration | v4.0 | 4/4 | Complete | 2026-05-14 |
134150
| 1032. Single-Source MonitorTag Events + ack workflow | v4.0 | 5/5 | Complete | 2026-05-14 |
135151
| 1033. Companion Integration + Acceptance Test | v4.0 | 4/4 | Complete | 2026-05-14 |
152+
| 1034. Plant Log Storage Foundation | v3.1 | 3/3 | Complete | 2026-05-13 |
153+
| 1035. CSV/XLSX Import + Mapping Dialog | v3.1 | 3/3 | Complete | 2026-05-13 |
154+
| 1036. Live Tail + Slider Preview Overlay | v3.1 | 3/3 | Complete | 2026-05-14 |
155+
| 1037. Per-Widget Plant Log Overlay | v3.1 | 3/3 | Complete | 2026-05-19 |
156+
| 1038. Dashboard + Companion Integration & Serialization | v3.1 | 3/3 | Complete | 2026-05-19 |
136157

137158
## Phase Details (v4.0 Multi-User LAN Concurrency)
138159

demo/industrial_plant/run_demo.m

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616
% ctx - struct with fields:
1717
% writerTimer - IndustrialPlantDataGen MATLAB timer (running)
1818
% pipeline - LiveTagPipeline (running)
19-
% engine - [] (plan 02 populates this with a DashboardEngine)
19+
% engine - DashboardEngine handle (live, populated by buildDashboard)
2020
% companion - FastSenseCompanion handle (or [] when 'Companion'=false)
2121
% store - EventStore wired into every MonitorTag
2222
% plantHealthKey - 'plant.health' (top-level rollup)
2323
% rawDir - absolute path to demo/industrial_plant/data/raw
2424
% tagsDir - absolute path to demo/industrial_plant/data/tags
25+
% plantLogPath - absolute path to the generated plant_log.csv (or
26+
% '' if seeding/attaching failed; see warning
27+
% run_demo:plantLogAttachFailed)
2528
%
2629
% Teardown:
2730
% teardownDemo(ctx);
@@ -40,7 +43,8 @@
4043
% teardownDemo(ctx);
4144
%
4245
% See also: plantConfig, registerPlantTags, makeDataGenerator,
43-
% startLivePipeline, teardownDemo, TagRegistry, LiveTagPipeline.
46+
% startLivePipeline, seedPlantLog, teardownDemo,
47+
% TagRegistry, LiveTagPipeline.
4448

4549
here = fileparts(mfilename('fullpath'));
4650

@@ -107,6 +111,7 @@
107111
'plantHealthKey', plantHealthKey, ...
108112
'rawDir', rawDir, ...
109113
'tagsDir', tagsDir, ...
114+
'plantLogPath', '', ...
110115
'stressMode', stressMode, ...
111116
'fleetTagKeys', {fleetTagKeys});
112117

@@ -124,6 +129,23 @@
124129
ctx.companion = [];
125130
end
126131

132+
% Phase 1033 / milestone v3.1 -- seed a synthetic plant log and
133+
% attach it to the dashboard so the slider preview + per-widget
134+
% overlay are exercised end-to-end. Best-effort: a failure here
135+
% must not crash the demo bootstrap (dashboard + writer + pipeline
136+
% keep running so the rest of the demo stays usable).
137+
try
138+
ctx.plantLogPath = seedPlantLog(rawDir, plantConfig());
139+
if ~isempty(ctx.engine) && isvalid(ctx.engine)
140+
ctx.engine.attachPlantLog(ctx.plantLogPath);
141+
end
142+
catch err
143+
warning('run_demo:plantLogAttachFailed', ...
144+
'Seed/attach plant log failed: %s (demo continues without plant log)', ...
145+
err.message);
146+
ctx.plantLogPath = '';
147+
end
148+
127149
% Phase 1023.1 cross-phase fix: re-bind the dashboard figure's
128150
% CloseRequestFcn with the now-complete ctx. buildDashboard set the
129151
% callback when ctx.companion was still [], so the closure captured
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
function plantLogPath = seedPlantLog(rawDir, cfg)
2+
%SEEDPLANTLOG Generate a synthetic plant log CSV for the industrial plant demo.
3+
% plantLogPath = seedPlantLog(rawDir, cfg) writes ~30 deterministic
4+
% operator-log entries to data/raw/plant_log.csv with timestamps spread
5+
% across the last 7 days plus 3 entries in the recent past (now-30s,
6+
% now-15s, now+0s) so the live-tail demo immediately has fresh-looking
7+
% entries to display.
8+
%
9+
% The CSV columns are:
10+
% Timestamp,Message,Unit,Shift,Operator
11+
%
12+
% - Timestamp uses 'yyyy-MM-dd HH:mm:ss' (PlantLogReader auto-detect format).
13+
% - Message is the free-text operator note.
14+
% - Unit values are drawn from cfg.Subsystems ({'FeedLine','Reactor','Cooling'})
15+
% plus 'ALL' for plant-wide entries.
16+
% - Shift is 'A' | 'B' | 'C'.
17+
% - Operator is a small name pool.
18+
%
19+
% The function reseeds RNG to 1015 at entry and restores the previous RNG
20+
% state at exit (matching seedHistory.m's idiom). Determinism + state
21+
% restore lets repeated run_demo() calls produce byte-identical CSVs
22+
% (modulo the now-relative anchor timestamp).
23+
%
24+
% Inputs:
25+
% rawDir - char, absolute path to demo/industrial_plant/data/raw (must exist).
26+
% cfg - struct returned by plantConfig() -- uses cfg.Subsystems.
27+
%
28+
% Output:
29+
% plantLogPath - char, absolute path to the generated CSV.
30+
%
31+
% See also: seedHistory, plantConfig, run_demo, PlantLogReader.
32+
33+
% --- Input validation -----------------------------------------------
34+
if ~ischar(rawDir) && ~(isstring(rawDir) && isscalar(rawDir))
35+
error('IndustrialPlant:invalidRawDir', ...
36+
'rawDir must be a char or scalar string.');
37+
end
38+
rawDir = char(rawDir);
39+
if ~exist(rawDir, 'dir')
40+
error('IndustrialPlant:rawDirMissing', ...
41+
'rawDir does not exist: %s', rawDir);
42+
end
43+
if ~isstruct(cfg)
44+
error('IndustrialPlant:invalidCfg', ...
45+
'cfg must be a struct (plantConfig() output).');
46+
end
47+
48+
% --- Seed RNG, restore on exit (matches seedHistory.m idiom) --------
49+
prevRng = rng(1015, 'twister');
50+
cleanup = onCleanup(@() rng(prevRng));
51+
52+
% --- Build entry pool ------------------------------------------------
53+
% Each entry: offsetSeconds (relative to now()) + message + unit + shift + operator.
54+
% First 30 entries spread across the last 7 days at shift-start times
55+
% (06:00, 14:00, 22:00) and an early-morning maintenance window (02:30);
56+
% final 3 entries land in the recent past so live-tail picks them up.
57+
entries = buildEntries_(cfg);
58+
59+
% --- Write CSV via fprintf (cross-runtime, MATLAB + Octave 7+) ------
60+
% writetable's 'Size'+'VariableTypes' form is MATLAB-only on some
61+
% Octave builds; fprintf is the safe lowest-common-denominator.
62+
plantLogPath = fullfile(rawDir, 'plant_log.csv');
63+
nowRef = now();
64+
65+
% Sort entries by offsetSeconds ASC so timestamps land chronologically
66+
% in the CSV (PlantLogStore dedup tolerates out-of-order but ordered
67+
% is the canonical state we want the live-tail tail to read).
68+
[~, order] = sort([entries.offsetSeconds]);
69+
entries = entries(order);
70+
71+
fid = fopen(plantLogPath, 'w');
72+
if fid == -1
73+
error('IndustrialPlant:writeFailed', ...
74+
'Could not open %s for writing.', plantLogPath);
75+
end
76+
closer = onCleanup(@() fclose(fid));
77+
fprintf(fid, 'Timestamp,Message,Unit,Shift,Operator\n');
78+
for k = 1:numel(entries)
79+
e = entries(k);
80+
ts = datestr(nowRef + e.offsetSeconds/86400, 'yyyy-mm-dd HH:MM:SS'); %#ok<DATST>
81+
% Quote message field (may contain commas/colons); other fields
82+
% are short alphanum so unquoted is fine.
83+
fprintf(fid, '%s,"%s",%s,%s,%s\n', ts, e.message, e.unit, e.shift, e.operator);
84+
end
85+
end
86+
87+
function entries = buildEntries_(cfg)
88+
%BUILDENTRIES_ Construct the 33-entry plant-log pool.
89+
% First 30 entries: shift-pattern times spread over 7 days. Final 3
90+
% entries: near-now (-30s, -15s, 0s) so the live-tail demo has fresh
91+
% content as soon as the dashboard renders.
92+
%
93+
% Unit values use the 4-element set [{'ALL'}, cfg.Subsystems(:)']
94+
% directly so the demo's subsystem nomenclature is the single source
95+
% of truth (changing cfg.Subsystems propagates to seedPlantLog).
96+
97+
units = [{'ALL'}, cfg.Subsystems(:)']; %#ok<NASGU> referenced via literals below
98+
99+
% Shift-start anchor times within a day (HH * 3600 + MM * 60 + SS):
100+
% 06:00 -> 21600s
101+
% 14:00 -> 50400s
102+
% 22:00 -> 79200s
103+
% 02:30 -> 9000s (overnight maintenance)
104+
shiftA = 21600;
105+
shiftB = 50400;
106+
shiftC = 79200;
107+
maint = 9000;
108+
109+
% Helper to compute an offsetSeconds: secondsIntoDay - daysAgo * 86400.
110+
% Day 0 is "today"; negative offsetSeconds = past.
111+
secOf = @(daysAgo, secondsIntoDay) -(daysAgo * 86400) + (secondsIntoDay - 86400);
112+
% Explanation: relative to now (= 86400s offset within today), an event
113+
% at secondsIntoDay of (today - daysAgo) sits at:
114+
% (secondsIntoDay) + (-daysAgo - 0) * 86400 - 86400
115+
% which simplifies above. The result is strictly <= 0 for any
116+
% daysAgo >= 0 and secondsIntoDay <= 86400.
117+
118+
% Build the 30-entry historical pool (shift-pattern times across days 0..6).
119+
% Mix shift-starts with overnight maintenance entries for variety.
120+
rows = { ...
121+
% daysAgo secondsIntoDay message unit shift operator
122+
6, shiftA, 'Operator Mehta starting morning shift, all systems nominal', 'ALL', 'A', 'Mehta'; ...
123+
6, shiftB, 'Routine maintenance: cooling pump filter changed', 'Cooling', 'B', 'Yamamoto'; ...
124+
6, shiftC, 'Reactor heated to 160C setpoint', 'Reactor', 'A', 'Patel'; ...
125+
5, maint, 'Feedline pressure alarm cleared', 'FeedLine', 'C', 'Davis'; ...
126+
5, shiftA, 'Batch B-2381 started', 'Reactor', 'B', 'Patel'; ...
127+
5, shiftB, 'Batch B-2381 complete, 1843 L yield', 'Reactor', 'B', 'Patel'; ...
128+
5, shiftC, 'Shift handover: Davis -> Patel, no anomalies reported', 'ALL', 'A', 'Patel'; ...
129+
4, maint, 'Heat exchanger fouling suspected, cleaning scheduled', 'Cooling', 'A', 'Yamamoto'; ...
130+
4, shiftB, 'Reactor pressure spike at 14:32 acknowledged by operator Chen', 'Reactor', 'B', 'Chen'; ...
131+
4, shiftC, 'Feedline valve V-117 replaced with conditioning unit', 'FeedLine', 'C', 'Davis'; ...
132+
3, shiftA, 'Cooling loop flow rate adjusted to 95 L/min', 'Cooling', 'A', 'Yamamoto'; ...
133+
3, shiftA + 1800, 'Pre-shift safety briefing complete', 'ALL', 'A', 'Mehta'; ...
134+
3, shiftB, 'Reactor mode transition: heating -> running', 'Reactor', 'B', 'Chen'; ...
135+
3, shiftC, 'Inlet temperature sensor calibration verified', 'Cooling', 'A', 'Yamamoto'; ...
136+
2, shiftA, 'Feedline pressure transient observed during startup', 'FeedLine', 'A', 'Patel'; ...
137+
2, shiftB, 'Emergency stop test (drill) completed successfully', 'ALL', 'B', 'Mehta'; ...
138+
2, shiftC, 'Reactor RPM trending nominal, no action required', 'Reactor', 'C', 'Davis'; ...
139+
2, maint, 'Cooling tower fan cycled per maintenance schedule', 'Cooling', 'C', 'Yamamoto'; ...
140+
1, shiftA, 'Batch B-2382 started', 'Reactor', 'A', 'Patel'; ...
141+
1, shiftA + 600, 'Feedline strainer inspection: clean', 'FeedLine', 'A', 'Davis'; ...
142+
1, shiftB, 'Reactor temperature setpoint changed to 165C per recipe revision','Reactor', 'B', 'Chen'; ...
143+
1, shiftC, 'Night shift quiet period, monitoring only', 'ALL', 'C', 'Davis'; ...
144+
0, maint, 'Cooling water pH within spec (7.4)', 'Cooling', 'A', 'Yamamoto'; ...
145+
0, shiftA, 'Feedline flow stable at 122 L/min', 'FeedLine', 'B', 'Patel'; ...
146+
0, shiftA + 1200, 'Reactor agitator vibration spike investigated, within tolerance','Reactor', 'A', 'Chen'; ...
147+
0, shiftA + 2400, 'Batch B-2382 complete, 1798 L yield', 'Reactor', 'B', 'Patel'; ...
148+
0, shiftA + 3000, 'Shift handover: Patel -> Mehta, batch B-2383 queued', 'ALL', 'A', 'Mehta'; ...
149+
0, shiftA + 3600, 'Cooling out-temp briefly exceeded 50C, alarm cleared after 12s', 'Cooling', 'B', 'Yamamoto'; ...
150+
0, shiftA + 4200, 'Feedline valve V-118 actuator stroke time verified', 'FeedLine', 'A', 'Davis'; ...
151+
0, shiftA + 4800, 'Reactor pressure trending up -- operator confirms expected', 'Reactor', 'B', 'Chen' ...
152+
};
153+
154+
nHist = size(rows, 1);
155+
entries = repmat(struct( ...
156+
'offsetSeconds', 0, ...
157+
'message', '', ...
158+
'unit', '', ...
159+
'shift', '', ...
160+
'operator', ''), 1, nHist + 3);
161+
162+
for k = 1:nHist
163+
daysAgo = rows{k, 1};
164+
secondsIntoDay = rows{k, 2};
165+
entries(k).offsetSeconds = secOf(daysAgo, secondsIntoDay);
166+
entries(k).message = rows{k, 3};
167+
entries(k).unit = rows{k, 4};
168+
entries(k).shift = rows{k, 5};
169+
entries(k).operator = rows{k, 6};
170+
end
171+
172+
% --- 3 entries near now() so the live-tail demo shows fresh content --
173+
entries(nHist + 1).offsetSeconds = -30;
174+
entries(nHist + 1).message = 'Live-tail entry: 30s ago -- routine check, all green';
175+
entries(nHist + 1).unit = 'ALL';
176+
entries(nHist + 1).shift = 'A';
177+
entries(nHist + 1).operator = 'Mehta';
178+
179+
entries(nHist + 2).offsetSeconds = -15;
180+
entries(nHist + 2).message = 'Live-tail entry: 15s ago -- feedline pressure 5.1 bar nominal';
181+
entries(nHist + 2).unit = 'FeedLine';
182+
entries(nHist + 2).shift = 'A';
183+
entries(nHist + 2).operator = 'Davis';
184+
185+
entries(nHist + 3).offsetSeconds = 0;
186+
entries(nHist + 3).message = 'Live-tail entry: now -- beginning fresh observation window';
187+
entries(nHist + 3).unit = 'ALL';
188+
entries(nHist + 3).shift = 'A';
189+
entries(nHist + 3).operator = 'Mehta';
190+
end

install.m

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
% libs/Dashboard — widget-based dashboard engine
2323
% libs/WebBridge — browser-based visualization bridge
2424
% libs/FastSenseCompanion — companion navigator app
25-
% libs/Help — Wiki Browser + WikiPageIndex (Phase 1034)
25+
% libs/PlantLog — plant-log entry storage (CSV/XLSX import target; v3.1)
26+
% libs/Help — Wiki Browser + WikiPageIndex (v4.0 Phase 1034)
2627
% examples/ — runnable example scripts
2728
% benchmarks/ — performance benchmarks
2829
% tests/ — test suites
@@ -56,6 +57,7 @@
5657
addpath(fullfile(root, 'libs', 'Dashboard'));
5758
addpath(fullfile(root, 'libs', 'WebBridge'));
5859
addpath(fullfile(root, 'libs', 'FastSenseCompanion'));
60+
addpath(fullfile(root, 'libs', 'PlantLog'));
5961
addpath(fullfile(root, 'libs', 'Concurrency'));
6062
addpath(fullfile(root, 'libs', 'Help'));
6163

0 commit comments

Comments
 (0)