The three open Phase 5 findings were one defect: the stand-in was keyed on
CI Server POINT names (PS_STN_WET_WELL_LEVEL) when the historian is keyed on
CI Server ITEM names (AID.WRPS.STN.LEVEL). Modbus carries register numbers,
not names, so those two layers are free to differ - and do. Reconciling
against the register map, as planned, would only have proved the first three
namespaces agreed with each other.
Rebuilt from WRPS/05-scada/modbus, so item names, sample rates, retention and
timestamp semantics come from the machine rather than from a guess.
(a) Level tag does not join. PS_STN_WET_WELL_LEVEL becomes a tag row in its
own right; LIT-101 is marked NOT HISTORISED - a field input on %IW0 that
never reaches SCADA. It was the only seed row carrying two addresses.
public.historian_items holds the item-to-tag mapping, generated by
scripts/gen_historian_items.py and enforced non-empty at generate, at
deploy and at verify.
(b) first_alarm/last_alarm returned UTC. Converted inside the measure, so it
stays in Cube and happens once. Aggregate first, convert after - the other
order picks the wrong row across a DST fall-back. Returned as a formatted
string with a companion site_timezone measure. Storage being UTC is now
confirmed, not assumed: all 49 points carry TIME_ZONE "Date+time GMT" and
every history group CORRECT_DAYLIGHT=0. This answers Phase 4 task 4.
(c) High level alarm filed against the wrong equipment. Both sides were right
about different things; the defect was asserting equipment twice. The
history now carries no equipment column at all - faithful, since CI
Server's section tree stops at the station and three pumps. Equipment is
reached bit -> tag -> equipment via public.alarm_bits.
Alarms are derived, not stored: CI Server's ALARM_HISTORY group is empty
because every item imports with alarming off. Decomposing the alarm word needs
no configuration that does not exist.
Three things the SCADA config changed that were never filed as faults:
- retention is 7 days, not 30. The advisory path was reporting a month of
evidence drawn from a week of data
- the analogue rate is 5 s, not 60. Two measures multiplied sample counts by
a hardcoded 60 - a twelvefold overstatement that read as plausible
- the deadband warning in process_values.yml was wrong and was steering
people away from the correct measure
db/002_fixtures.sql now asserts its own counts at load and cross-checks the
alarm derivation against two independent signals. Those prove the pipeline,
not the plant.
db/README-standin-historian.md documents removal: the seam between generation
and contract, and twelve assumptions about imh that are NOT confirmed. Two of
them fail silently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1042 lines
68 KiB
HTML
1042 lines
68 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>WRPS Assistant Status Board</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Barlow+Semi+Condensed:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=Source+Serif+4:opsz,wght@8..60,400;8..60,600&display=swap">
|
|
<style>
|
|
/* ---------------------------------------------------------------- tokens ---
|
|
Palette inherited from yau-poc-architecture-and-build.pptx so the deck, the
|
|
workflow map and this page read as one set. Neutrals are slate-biased toward
|
|
the project blue rather than a flat grey. */
|
|
:root {
|
|
--ground: #EEF1F5;
|
|
--surface: #FFFFFF;
|
|
--surface-2: #F7F9FB;
|
|
--ink: #0F172A;
|
|
--ink-2: #334155;
|
|
--muted: #64748B;
|
|
--hair: #D3DAE4;
|
|
--hair-soft: #E6EBF1;
|
|
|
|
--blue: #1D4ED8; --blue-tint: #EEF3FE;
|
|
--purple: #7C3AED; --purple-tint: #F4EEFE;
|
|
--amber: #B45309; --amber-tint: #FDF3E7;
|
|
--red: #DC2626; --red-tint: #FDEEEE;
|
|
--teal: #0F766E; --teal-tint: #E9F5F3;
|
|
--green: #059669; --green-tint: #E8F7F1;
|
|
|
|
--shadow: 0 1px 2px rgba(15,23,42,.05), 0 8px 24px -16px rgba(15,23,42,.28);
|
|
|
|
--measure: 68ch;
|
|
--step--1: .8125rem;
|
|
--step-0: 1rem;
|
|
--step-1: 1.1875rem;
|
|
--step-2: 1.5rem;
|
|
--step-3: 2rem;
|
|
--step-4: 2.75rem;
|
|
|
|
--sans: "Barlow Semi Condensed", "Segoe UI", system-ui, sans-serif;
|
|
--serif: "Source Serif 4", Georgia, "Times New Roman", serif;
|
|
--mono: "IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, monospace;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme="light"]) {
|
|
--ground: #0B1018;
|
|
--surface: #141C27;
|
|
--surface-2: #101822;
|
|
--ink: #E7EDF5;
|
|
--ink-2: #BCC8D8;
|
|
--muted: #8A9AAE;
|
|
--hair: #2A3646;
|
|
--hair-soft: #1F2A38;
|
|
|
|
--blue: #7DA6FF; --blue-tint: #16233A;
|
|
--purple: #BFA0FF; --purple-tint: #221A38;
|
|
--amber: #E0A052; --amber-tint: #2B2013;
|
|
--red: #F98080; --red-tint: #2E1618;
|
|
--teal: #5FC7BB; --teal-tint: #10262A;
|
|
--green: #4ED2A0; --green-tint: #0E2620;
|
|
|
|
--shadow: 0 1px 2px rgba(0,0,0,.4), 0 10px 28px -18px rgba(0,0,0,.9);
|
|
}
|
|
}
|
|
:root[data-theme="dark"] {
|
|
--ground: #0B1018;
|
|
--surface: #141C27;
|
|
--surface-2: #101822;
|
|
--ink: #E7EDF5;
|
|
--ink-2: #BCC8D8;
|
|
--muted: #8A9AAE;
|
|
--hair: #2A3646;
|
|
--hair-soft: #1F2A38;
|
|
|
|
--blue: #7DA6FF; --blue-tint: #16233A;
|
|
--purple: #BFA0FF; --purple-tint: #221A38;
|
|
--amber: #E0A052; --amber-tint: #2B2013;
|
|
--red: #F98080; --red-tint: #2E1618;
|
|
--teal: #5FC7BB; --teal-tint: #10262A;
|
|
--green: #4ED2A0; --green-tint: #0E2620;
|
|
|
|
--shadow: 0 1px 2px rgba(0,0,0,.4), 0 10px 28px -18px rgba(0,0,0,.9);
|
|
}
|
|
|
|
/* ----------------------------------------------------------------- base --- */
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0;
|
|
background: var(--ground);
|
|
color: var(--ink-2);
|
|
font-family: var(--serif);
|
|
font-size: var(--step-0);
|
|
line-height: 1.62;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
.wrap { max-width: 1180px; margin: 0 auto; padding: 0 clamp(16px, 4vw, 40px) 96px; }
|
|
|
|
h1, h2, h3, h4, .lbl, th, .chip, .lamp-key, .kpi-n, .kpi-l { font-family: var(--sans); }
|
|
h1 { font-size: var(--step-4); line-height: 1.04; margin: 0; color: var(--ink);
|
|
font-weight: 700; letter-spacing: -.015em; text-wrap: balance; }
|
|
h2 { font-size: var(--step-3); line-height: 1.12; margin: 0; color: var(--ink);
|
|
font-weight: 700; letter-spacing: -.01em; text-wrap: balance; }
|
|
h3 { font-size: var(--step-1); line-height: 1.24; margin: 0; color: var(--ink);
|
|
font-weight: 600; text-wrap: balance; }
|
|
h4 { font-size: var(--step-0); margin: 0; color: var(--ink); font-weight: 600; }
|
|
p { margin: 0; max-width: var(--measure); }
|
|
a { color: var(--blue); }
|
|
b, strong { color: var(--ink); font-weight: 600; }
|
|
code, .mono { font-family: var(--mono); font-size: .86em; }
|
|
:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; border-radius: 2px; }
|
|
|
|
.lbl {
|
|
font-size: .70rem; font-weight: 600; letter-spacing: .13em;
|
|
text-transform: uppercase; color: var(--muted);
|
|
}
|
|
|
|
/* --------------------------------------------------------------- header --- */
|
|
header.top { padding: clamp(40px, 7vw, 76px) 0 34px; }
|
|
.eyebrow { display: flex; flex-wrap: wrap; gap: 10px 18px; align-items: baseline;
|
|
margin-bottom: 20px; }
|
|
.rule { height: 1px; background: var(--hair); margin: 26px 0; }
|
|
.lede { font-size: var(--step-1); color: var(--ink-2); margin-top: 20px; }
|
|
|
|
/* flex, not grid: flex items grow to fill the last row, so a count that does not
|
|
divide by the column count never leaves an empty tinted cell */
|
|
.kpis { display: flex; flex-wrap: wrap; gap: 1px; background: var(--hair);
|
|
border: 1px solid var(--hair); border-radius: 4px; overflow: hidden;
|
|
margin-top: 30px; }
|
|
.kpi { background: var(--surface); padding: 15px 17px 14px; flex: 1 1 158px; }
|
|
.kpi-n { font-size: 1.95rem; font-weight: 700; line-height: 1; color: var(--ink);
|
|
font-variant-numeric: tabular-nums; }
|
|
.kpi-n.is-red { color: var(--red); } .kpi-n.is-green { color: var(--green); }
|
|
.kpi-n.is-amber { color: var(--amber); } .kpi-n.is-purple { color: var(--purple); }
|
|
.kpi-l { display: block; margin-top: 7px; font-size: .74rem; font-weight: 500;
|
|
letter-spacing: .04em; color: var(--muted); line-height: 1.35; }
|
|
|
|
/* -------------------------------------------------------------- sections --- */
|
|
section { margin-top: clamp(52px, 7vw, 84px); }
|
|
.sec-head { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap;
|
|
border-bottom: 2px solid var(--ink); padding-bottom: 10px; margin-bottom: 8px; }
|
|
.sec-head .num { font-family: var(--mono); font-size: .8rem; font-weight: 600;
|
|
color: var(--muted); }
|
|
.sec-sub { margin-top: 14px; color: var(--muted); }
|
|
|
|
/* ---------------------------------------------------------------- lamps --- */
|
|
.lamp { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto;
|
|
display: inline-block; box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 16%, transparent); }
|
|
.l-green { background: var(--green); color: var(--green); }
|
|
.l-amber { background: var(--amber); color: var(--amber); }
|
|
.l-red { background: var(--red); color: var(--red); }
|
|
.l-purple { background: var(--purple); color: var(--purple); }
|
|
.l-muted { background: var(--muted); color: var(--muted); }
|
|
|
|
.legend { display: flex; flex-wrap: wrap; gap: 8px 26px; margin-top: 22px; }
|
|
.lamp-key { display: flex; align-items: center; gap: 9px; font-size: .78rem;
|
|
color: var(--muted); font-weight: 500; }
|
|
|
|
.chip { display: inline-flex; align-items: center; gap: 7px; font-size: .715rem;
|
|
font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
|
|
padding: 3px 9px 2px; border-radius: 3px; white-space: nowrap;
|
|
border: 1px solid currentColor; }
|
|
.c-green { color: var(--green); background: var(--green-tint); }
|
|
.c-amber { color: var(--amber); background: var(--amber-tint); }
|
|
.c-red { color: var(--red); background: var(--red-tint); }
|
|
.c-purple { color: var(--purple); background: var(--purple-tint); }
|
|
.c-muted { color: var(--muted); background: var(--surface-2); }
|
|
|
|
/* --------------------------------------------------------------- diagram --- */
|
|
.figure { margin-top: 26px; border: 1px solid var(--hair); border-radius: 5px;
|
|
background: var(--surface); box-shadow: var(--shadow); overflow: hidden; }
|
|
.figure .scroller { overflow-x: auto; padding: 18px; }
|
|
.figure svg { display: block; min-width: 940px; width: 100%; height: auto; }
|
|
.figcap { border-top: 1px solid var(--hair-soft); background: var(--surface-2);
|
|
padding: 13px 18px; font-size: var(--step--1); color: var(--muted); }
|
|
.figcap b { color: var(--ink); }
|
|
|
|
svg text { font-family: var(--sans); fill: var(--muted); }
|
|
svg .t-name { font-size: 15px; font-weight: 700; }
|
|
svg .t-meta { font-size: 10.5px; font-weight: 500; }
|
|
svg .t-box { font-size: 12px; font-weight: 600; }
|
|
svg .t-sub { font-size: 10px; font-weight: 400; }
|
|
svg .t-tiny { font-size: 9.5px; font-weight: 600; letter-spacing: .04em; }
|
|
svg .f-amber { fill: var(--amber); } svg .s-amber { stroke: var(--amber); }
|
|
svg .f-red { fill: var(--red); } svg .s-red { stroke: var(--red); }
|
|
svg .f-blue { fill: var(--blue); } svg .s-blue { stroke: var(--blue); }
|
|
svg .f-purple { fill: var(--purple); } svg .s-purple { stroke: var(--purple); }
|
|
svg .f-teal { fill: var(--teal); } svg .s-teal { stroke: var(--teal); }
|
|
svg .f-green { fill: var(--green); } svg .s-green { stroke: var(--green); }
|
|
svg .f-ink { fill: var(--ink); } svg .s-hair { stroke: var(--hair); }
|
|
svg .f-muted { fill: var(--muted); } svg .s-muted { stroke: var(--muted); }
|
|
svg .panel { fill: var(--surface); }
|
|
svg .bg-amber { fill: var(--amber-tint); } svg .bg-red { fill: var(--red-tint); }
|
|
svg .bg-blue { fill: var(--blue-tint); } svg .bg-purple { fill: var(--purple-tint); }
|
|
svg .bg-teal { fill: var(--teal-tint); } svg .bg-green { fill: var(--green-tint); }
|
|
svg .bg-soft { fill: var(--surface-2); }
|
|
svg [class*="s-"] { fill: none; stroke-width: 1.25; }
|
|
|
|
/* ---------------------------------------------------------------- phases --- */
|
|
.phases { display: grid; gap: 1px; background: var(--hair); border: 1px solid var(--hair);
|
|
border-radius: 5px; overflow: hidden; margin-top: 26px; }
|
|
.phase { --stripe: var(--muted);
|
|
background: var(--surface); display: grid; gap: 4px 20px; padding: 17px 20px 17px 22px;
|
|
grid-template-columns: 46px minmax(0,1fr) auto; align-items: baseline;
|
|
position: relative; }
|
|
.phase::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
|
|
background: var(--stripe); }
|
|
.phase.s-done { --stripe: var(--green); }
|
|
.phase.s-part { --stripe: var(--amber); }
|
|
.phase.s-blocked { --stripe: var(--red); }
|
|
.phase.s-odd { --stripe: var(--purple); }
|
|
.phase .pn { font-family: var(--mono); font-size: 1.05rem; font-weight: 600; color: var(--muted); }
|
|
.phase .pbody { grid-column: 2; }
|
|
.phase .pnote { grid-column: 2 / -1; margin-top: 7px; font-size: var(--step--1);
|
|
color: var(--muted); }
|
|
.phase .pnote b { color: var(--ink); }
|
|
.evi { grid-column: 2 / -1; margin: 9px 0 0; padding: 0; list-style: none;
|
|
display: flex; flex-wrap: wrap; gap: 5px 8px; }
|
|
.evi li { font-family: var(--mono); font-size: .72rem; color: var(--muted);
|
|
background: var(--surface-2); border: 1px solid var(--hair-soft);
|
|
border-radius: 3px; padding: 2px 7px; }
|
|
@media (max-width: 640px) {
|
|
.phase { grid-template-columns: 34px 1fr; }
|
|
.phase > .chip { grid-column: 2; justify-self: start; margin-top: 6px; }
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- tables --- */
|
|
.tblwrap { margin-top: 26px; border: 1px solid var(--hair); border-radius: 5px;
|
|
background: var(--surface); overflow-x: auto; box-shadow: var(--shadow); }
|
|
table { border-collapse: collapse; width: 100%; min-width: 860px; font-size: var(--step--1); }
|
|
th { text-align: left; font-size: .70rem; font-weight: 600; letter-spacing: .1em;
|
|
text-transform: uppercase; color: var(--muted); padding: 13px 16px;
|
|
border-bottom: 1px solid var(--hair); background: var(--surface-2);
|
|
position: sticky; top: 0; }
|
|
td { padding: 12px 16px; border-bottom: 1px solid var(--hair-soft);
|
|
vertical-align: top; color: var(--muted); }
|
|
tr:last-child td { border-bottom: 0; }
|
|
td.name { color: var(--ink); font-family: var(--mono); font-weight: 600; font-size: .8rem;
|
|
white-space: nowrap; }
|
|
td.mono, .mono-cell { font-family: var(--mono); font-size: .76rem; }
|
|
tr.row-red td { background: var(--red-tint); }
|
|
tr.row-amber td { background: var(--amber-tint); }
|
|
tr.row-red td, tr.row-amber td { color: var(--ink-2); }
|
|
|
|
/* -------------------------------------------------------------- catalogue --- */
|
|
.cat { display: grid; gap: 14px; margin-top: 26px;
|
|
grid-template-columns: repeat(auto-fill, minmax(310px, 1fr)); }
|
|
.comp { --edge: var(--hair);
|
|
background: var(--surface); border: 1px solid var(--hair); border-radius: 5px;
|
|
padding: 15px 17px 16px; display: flex; flex-direction: column; gap: 8px;
|
|
border-left: 4px solid var(--edge); }
|
|
.comp.e-purple { --edge: var(--purple); } .comp.e-blue { --edge: var(--blue); }
|
|
.comp.e-amber { --edge: var(--amber); } .comp.e-red { --edge: var(--red); }
|
|
.comp.e-muted { --edge: var(--hair); }
|
|
.comp-top { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
|
|
.comp-name { font-family: var(--mono); font-size: .875rem; font-weight: 600; color: var(--ink); }
|
|
.comp-img { font-family: var(--mono); font-size: .71rem; color: var(--muted);
|
|
word-break: break-all; }
|
|
.comp p { font-size: var(--step--1); margin: 0; color: var(--muted); max-width: none; }
|
|
.comp .job { color: var(--ink-2); }
|
|
.comp-foot { margin-top: auto; padding-top: 8px; border-top: 1px solid var(--hair-soft);
|
|
font-size: .73rem; color: var(--muted); font-family: var(--sans);
|
|
display: flex; gap: 7px; align-items: baseline; }
|
|
.comp-foot .k { font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
|
|
font-size: .66rem; color: var(--muted); flex: 0 0 auto; }
|
|
|
|
/* --------------------------------------------------------------- notices --- */
|
|
.notes { display: grid; gap: 16px; margin-top: 26px;
|
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
|
|
.note { --edge: var(--muted);
|
|
background: var(--surface); border: 1px solid var(--hair); border-radius: 5px;
|
|
padding: 18px 20px 20px; display: flex; flex-direction: column; gap: 10px;
|
|
border-top: 3px solid var(--edge); }
|
|
.note.e-red { --edge: var(--red); } .note.e-amber { --edge: var(--amber); }
|
|
.note.e-purple { --edge: var(--purple); } .note.e-blue { --edge: var(--blue); }
|
|
.note p { font-size: var(--step--1); max-width: none; }
|
|
.note .why { border-top: 1px solid var(--hair-soft); padding-top: 10px; }
|
|
.note .why b { color: var(--edge); }
|
|
|
|
.callout { margin-top: 26px; background: var(--surface); border: 1px solid var(--hair);
|
|
border-left: 4px solid var(--red); border-radius: 5px; padding: 18px 22px;
|
|
display: flex; flex-direction: column; gap: 10px; }
|
|
.callout p { max-width: none; font-size: var(--step--1); }
|
|
|
|
.shortcuts { margin: 22px 0 0; padding: 0; list-style: none; display: grid; gap: 1px;
|
|
background: var(--hair); border: 1px solid var(--hair); border-radius: 5px;
|
|
overflow: hidden; }
|
|
.shortcuts li { background: var(--surface); padding: 12px 18px; font-size: var(--step--1);
|
|
display: grid; grid-template-columns: 118px 1fr; gap: 4px 18px; }
|
|
.shortcuts .k { font-family: var(--sans); font-weight: 600; font-size: .70rem;
|
|
letter-spacing: .07em; text-transform: uppercase; color: var(--muted);
|
|
padding-top: 3px; }
|
|
@media (max-width: 560px) { .shortcuts li { grid-template-columns: 1fr; } }
|
|
|
|
footer { margin-top: 72px; padding-top: 22px; border-top: 1px solid var(--hair);
|
|
font-size: var(--step--1); color: var(--muted); display: grid; gap: 8px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
|
|
<header class="top">
|
|
<div class="eyebrow">
|
|
<span class="lbl">Waterloo Road Pump Station</span>
|
|
<span class="lbl" style="color:var(--blue)">Plant Operations Assistant · Proof of Concept</span>
|
|
</div>
|
|
<h1>Where the build actually stands</h1>
|
|
<p class="lede">Most of this page was read off the running host on <b>28 August 2026</b>
|
|
— containers listed, endpoints probed, the database queried, and two questions put through the
|
|
live assistant end to end. Nothing here is taken from the plan. Where this page and the build
|
|
documents disagree, this page is the later reading.</p>
|
|
<p class="lede" style="margin-top:14px"><b>Updated 31 August.</b> The three open findings are
|
|
closed. They turned out to be one defect wearing three faces, and it was settled not by choosing
|
|
between the two disagreeing sides but by going to the SCADA configuration and finding that
|
|
<b>both were describing something the historian does not use.</b> The section that used to list
|
|
them now records what was actually wrong.</p>
|
|
|
|
<div class="kpis">
|
|
<div class="kpi"><span class="kpi-n">28</span><span class="kpi-l">containers on lin001<br>7 of them ours</span></div>
|
|
<div class="kpi"><span class="kpi-n is-green">6</span><span class="kpi-l">phases done<br>of nine</span></div>
|
|
<div class="kpi"><span class="kpi-n is-red">1</span><span class="kpi-l">request still outstanding<br>the historian login</span></div>
|
|
<div class="kpi"><span class="kpi-n is-green">0</span><span class="kpi-l">open findings<br>all three closed 31 Aug</span></div>
|
|
<div class="kpi"><span class="kpi-n is-amber">78</span><span class="kpi-l">exam questions<br>never yet run as a gate</span></div>
|
|
<div class="kpi"><span class="kpi-n is-purple">3.96<span style="font-size:.55em">s</span></span><span class="kpi-l">live answer, 28 Aug<br>contract-valid</span></div>
|
|
</div>
|
|
|
|
<div class="legend">
|
|
<span class="lamp-key"><i class="lamp l-green"></i> Done and proven on the host</span>
|
|
<span class="lamp-key"><i class="lamp l-amber"></i> Running, but its gate is not met</span>
|
|
<span class="lamp-key"><i class="lamp l-red"></i> Blocked, or never run</span>
|
|
<span class="lamp-key"><i class="lamp l-purple"></i> Built out of the planned order</span>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- ================================================================ 1 ==== -->
|
|
<section id="topology">
|
|
<div class="sec-head"><span class="num">01</span><h2>The setup as built</h2></div>
|
|
<p class="sec-sub">Three servers and one cloud service. Two of the three servers exist; the third
|
|
is the whole of what this project is still waiting for.</p>
|
|
|
|
<div class="figure">
|
|
<div class="scroller">
|
|
<svg viewBox="0 0 1200 600" role="img" aria-labelledby="topo-title topo-desc">
|
|
<title id="topo-title">WRPS plant assistant topology as built, 28 August 2026</title>
|
|
<desc id="topo-desc">The SCADA server cicore1 reaches the assistant on the Docker host
|
|
lin001 without signing in. The SQL host imh, which would supply real plant history, does
|
|
not yet exist. lin001 runs seven containers built by this project alongside twenty-one that
|
|
were already there, and calls out to Azure OpenAI.</desc>
|
|
|
|
<!-- ============================================ cicore1 -->
|
|
<rect class="bg-amber" x="12" y="46" width="236" height="300" rx="6"/>
|
|
<rect class="s-amber" x="12" y="46" width="236" height="300" rx="6"/>
|
|
<text class="t-name f-amber" x="28" y="72">yau-poc-cicore1</text>
|
|
<text class="t-meta" x="28" y="90">SCADA server · 10.0.0.21 · built</text>
|
|
|
|
<rect class="panel" x="28" y="104" width="204" height="52" rx="4"/>
|
|
<rect class="s-amber" x="28" y="104" width="204" height="52" rx="4"/>
|
|
<text class="t-box f-amber" x="42" y="124">CI Server (SCADA)</text>
|
|
<text class="t-sub" x="42" y="140">HMI, alarms, the operator’s desk</text>
|
|
|
|
<rect class="panel" x="28" y="164" width="204" height="52" rx="4"/>
|
|
<rect class="s-amber" x="28" y="164" width="204" height="52" rx="4"/>
|
|
<text class="t-box f-amber" x="42" y="184">Raw historian</text>
|
|
<text class="t-sub" x="42" y="200">never queried by this system</text>
|
|
|
|
<rect class="panel" x="28" y="224" width="204" height="52" rx="4"/>
|
|
<rect class="s-amber" x="28" y="224" width="204" height="52" rx="4"/>
|
|
<text class="t-box f-amber" x="42" y="244">Modbus master</text>
|
|
<text class="t-sub" x="42" y="260">polls the PLC on lin001 :502</text>
|
|
|
|
<rect class="bg-green" x="28" y="290" width="204" height="40" rx="4"/>
|
|
<rect class="s-green" x="28" y="290" width="204" height="40" rx="4"/>
|
|
<text class="t-tiny f-green" x="130" y="308" text-anchor="middle">Chromium here reaches</text>
|
|
<text class="t-tiny f-green" x="130" y="322" text-anchor="middle">the assistant with NO sign-in</text>
|
|
|
|
<!-- ============================================ imh -->
|
|
<rect class="bg-red" x="264" y="46" width="204" height="300" rx="6"/>
|
|
<rect class="s-red" x="264" y="46" width="204" height="300" rx="6"/>
|
|
<text class="t-name f-red" x="280" y="72">yau-sls-poc-imh</text>
|
|
<text class="t-meta f-red" x="280" y="90">SQL host · STILL PENDING</text>
|
|
|
|
<rect class="panel" x="280" y="104" width="172" height="96" rx="4"/>
|
|
<rect class="s-red" x="280" y="104" width="172" height="96" rx="4" stroke-dasharray="4 3"/>
|
|
<text class="t-box f-red" x="294" y="124">SQL Server</text>
|
|
<text class="t-sub" x="294" y="141">already an isolated copy of</text>
|
|
<text class="t-sub" x="294" y="155">the raw SCADA historian, so</text>
|
|
<text class="t-sub" x="294" y="169">it can be queried directly</text>
|
|
<text class="t-sub" x="294" y="183">with no impact on the plant</text>
|
|
|
|
<rect class="panel" x="280" y="212" width="172" height="72" rx="4"/>
|
|
<rect class="s-red" x="280" y="212" width="172" height="72" rx="4" stroke-dasharray="4 3"/>
|
|
<text class="t-box f-red" x="294" y="232">svc_agent_ro</text>
|
|
<text class="t-sub" x="294" y="249">read-only login, agreed tables</text>
|
|
<text class="t-tiny f-red" x="294" y="268">DOES NOT EXIST YET</text>
|
|
|
|
<text class="t-tiny f-red" x="366" y="308" text-anchor="middle">The one true blocker.</text>
|
|
<text class="t-tiny f-red" x="366" y="322" text-anchor="middle">Everything else is built.</text>
|
|
|
|
<!-- ============================================ lin001 -->
|
|
<rect class="bg-blue" x="484" y="46" width="472" height="500" rx="6"/>
|
|
<rect class="s-blue" x="484" y="46" width="472" height="500" rx="6"/>
|
|
<text class="t-name f-blue" x="500" y="72">yau-sls-poc-lin001</text>
|
|
<text class="t-meta" x="500" y="90">Ubuntu 22.04 · Docker host · 10.0.0.17 · 28 containers</text>
|
|
|
|
<rect class="panel" x="500" y="104" width="440" height="118" rx="4"/>
|
|
<rect class="s-hair" x="500" y="104" width="440" height="118" rx="4"/>
|
|
<text class="t-tiny f-muted" x="514" y="123">EXISTING — DO NOT DISTURB · 21 CONTAINERS</text>
|
|
<text class="t-sub" x="514" y="142">Caddy · Authelia · authelia-portal · InfluxDB 2.7 (55 GB) · Grafana</text>
|
|
<text class="t-sub" x="514" y="158">Node-RED · Mosquitto · Telegraf · Forgejo · Portainer · Dozzle</text>
|
|
<text class="t-sub" x="514" y="174">Watchtower · WireGuard · Showroom · EQP licence · ChirpStack ×4</text>
|
|
<text class="t-sub f-amber" x="514" y="196" font-weight="600">openplc-runtime — the PLC for this demo, Modbus TCP on host port 502</text>
|
|
<text class="t-sub f-amber" x="514" y="211">Live control. Never restarted as a side effect of AI work.</text>
|
|
|
|
<rect class="panel" x="500" y="234" width="440" height="252" rx="4"/>
|
|
<rect class="s-purple" x="500" y="234" width="440" height="252" rx="4"/>
|
|
<text class="t-tiny f-purple" x="514" y="253">BUILT BY THIS PROJECT — 7 CONTAINERS, ALL RUNNING</text>
|
|
|
|
<rect class="bg-purple" x="514" y="264" width="204" height="50" rx="4"/>
|
|
<rect class="s-purple" x="514" y="264" width="204" height="50" rx="4"/>
|
|
<text class="t-box f-purple" x="526" y="282">pg-ai</text>
|
|
<text class="t-sub" x="526" y="297">documents, names, summaries</text>
|
|
<text class="t-sub" x="526" y="309">/datadisk/pg-ai · no host port</text>
|
|
|
|
<rect class="bg-soft" x="726" y="264" width="200" height="50" rx="4"/>
|
|
<rect class="s-muted" x="726" y="264" width="200" height="50" rx="4"/>
|
|
<text class="t-box f-muted" x="738" y="282">cube + cubestore</text>
|
|
<text class="t-sub" x="738" y="297">the data translator</text>
|
|
<text class="t-sub f-amber" x="738" y="309" font-weight="600">reads stand-ins, not imh</text>
|
|
|
|
<rect class="bg-purple" x="514" y="322" width="204" height="50" rx="4"/>
|
|
<rect class="s-purple" x="514" y="322" width="204" height="50" rx="4"/>
|
|
<text class="t-box f-purple" x="526" y="340">ai-api</text>
|
|
<text class="t-sub" x="526" y="355">sorts, gathers, words, checks</text>
|
|
<text class="t-sub" x="526" y="367">and holds the document library</text>
|
|
|
|
<rect class="bg-purple" x="726" y="322" width="200" height="50" rx="4"/>
|
|
<rect class="s-purple" x="726" y="322" width="200" height="50" rx="4"/>
|
|
<text class="t-box f-purple" x="738" y="340">ai-web</text>
|
|
<text class="t-sub" x="738" y="355">the operator’s screen</text>
|
|
<text class="t-sub" x="738" y="367">question box and working panel</text>
|
|
|
|
<rect class="bg-purple" x="514" y="380" width="204" height="50" rx="4"/>
|
|
<rect class="s-purple" x="514" y="380" width="204" height="50" rx="4"/>
|
|
<text class="t-box f-purple" x="526" y="398">langfuse + lf-db</text>
|
|
<text class="t-sub" x="526" y="413">the logbook — every question,</text>
|
|
<text class="t-sub" x="526" y="425">its evidence and its verdict</text>
|
|
|
|
<rect class="bg-soft" x="726" y="380" width="200" height="50" rx="4"/>
|
|
<rect class="s-hair" x="726" y="380" width="200" height="50" rx="4" stroke-dasharray="4 3"/>
|
|
<text class="t-box f-muted" x="738" y="398">ai-ingest · ai-docs-worker</text>
|
|
<text class="t-sub" x="738" y="413">defined but not running — the</text>
|
|
<text class="t-sub" x="738" y="425">upload path lives in ai-api instead</text>
|
|
|
|
<rect class="bg-purple" x="514" y="440" width="412" height="32" rx="4"/>
|
|
<text class="t-tiny f-purple" x="720" y="460" text-anchor="middle">All on the shared ‘proxy’ network · no published host ports · Caddy in front of every screen</text>
|
|
|
|
<text class="t-sub f-muted" x="500" y="508">/datadisk 53% used (was 46%) · / 33% used (was 24%) · InfluxDB is the growth, not us</text>
|
|
<text class="t-sub f-red" x="500" y="526" font-weight="600">Single host, no standby: lin001 is one failure away from taking the demo and the PLC together</text>
|
|
|
|
<!-- ============================================ azure -->
|
|
<rect class="bg-teal" x="972" y="46" width="216" height="128" rx="6"/>
|
|
<rect class="s-teal" x="972" y="46" width="216" height="128" rx="6"/>
|
|
<text class="t-name f-teal" x="988" y="72">Azure OpenAI</text>
|
|
<text class="t-meta f-green" x="988" y="90" font-weight="600">yau-dem-oai · LIVE since 27 Aug</text>
|
|
<text class="t-sub" x="988" y="110">gpt-4o on both chat lanes</text>
|
|
<text class="t-sub" x="988" y="126">text-embedding-3-small</text>
|
|
<text class="t-sub" x="988" y="142">key in a 0600 file, never in Git</text>
|
|
<text class="t-sub f-red" x="988" y="162" font-weight="600">no cheap lane — see the cost note</text>
|
|
|
|
<rect class="bg-red" x="972" y="196" width="216" height="86" rx="5"/>
|
|
<rect class="s-red" x="972" y="196" width="216" height="86" rx="5"/>
|
|
<text class="t-tiny f-red" x="986" y="216">THE COST NOTE</text>
|
|
<text class="t-sub" x="986" y="234">The estimate assumed a small,</text>
|
|
<text class="t-sub" x="986" y="248">cheap model for sorting questions</text>
|
|
<text class="t-sub" x="986" y="262">— the most frequent call by far.</text>
|
|
<text class="t-sub" x="986" y="276">It runs on the large one instead.</text>
|
|
|
|
<!-- ============================================ links -->
|
|
<line class="s-amber" x1="248" y1="130" x2="264" y2="130"/>
|
|
<line class="s-red" x1="468" y1="240" x2="484" y2="240" stroke-dasharray="4 3"/>
|
|
<text class="t-tiny f-red" x="476" y="228" text-anchor="middle">TDS 1433</text>
|
|
<text class="t-tiny f-red" x="476" y="262" text-anchor="middle">not open</text>
|
|
<line class="s-teal" x1="956" y1="110" x2="972" y2="110"/>
|
|
|
|
<path class="s-green" d="M130 330 L130 570 L820 570 L820 486" stroke-width="1.75"/>
|
|
<rect class="bg-green" x="300" y="552" width="360" height="34" rx="4"/>
|
|
<rect class="s-green" x="300" y="552" width="360" height="34" rx="4"/>
|
|
<text class="t-tiny f-green" x="480" y="573" text-anchor="middle">Operator → Caddy → ai-web · no Authelia, no Duo, one IP only</text>
|
|
</svg>
|
|
</div>
|
|
<div class="figcap"><b>Read the dashed lines.</b> Everything solid was probed today and answered.
|
|
The two dashed boxes are the historian and its login, and they are the whole of what somebody
|
|
outside this project still owes us. The green path along the bottom is new as of 28 August:
|
|
the operator no longer signs in.</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================================================================ 2 ==== -->
|
|
<section id="phases">
|
|
<div class="sec-head"><span class="num">02</span><h2>What is done, and what is not</h2></div>
|
|
<p class="sec-sub">Nine phases, each ending in a gate that must pass before the next begins —
|
|
so that when an answer comes out wrong there is one place to look, not four. Two gates have never
|
|
been run, and one phase was built out of order.</p>
|
|
|
|
<div class="phases">
|
|
|
|
<div class="phase s-done">
|
|
<span class="pn">01</span>
|
|
<div class="pbody"><h4>The store — <span style="font-weight:400;color:var(--muted)">pg-ai</span></h4></div>
|
|
<span class="chip c-green"><i class="lamp l-green"></i>Done</span>
|
|
<div class="pnote">Documents, equipment names, tag definitions and the stand-in plant history.
|
|
Five database roles with the split that matters: the answer path can read and nothing else,
|
|
and only the ingestion role can write documents.</div>
|
|
<ul class="evi"><li>healthy 8 days</li><li>92 document chunks</li><li>8 equipment, 65 tags, all aliased</li><li>49 historian items mapped</li><li>no host port</li><li>agent_ro cannot INSERT</li></ul>
|
|
</div>
|
|
|
|
<div class="phase s-done">
|
|
<span class="pn">02</span>
|
|
<div class="pbody"><h4>The logbook — <span style="font-weight:400;color:var(--muted)">Langfuse</span></h4></div>
|
|
<span class="chip c-green"><i class="lamp l-green"></i>Done</span>
|
|
<div class="pnote">Deployed early on purpose, so that every experiment since has been traced.
|
|
<b>One consequence of the 28 August access change:</b> traces are now anonymous. There is a
|
|
record of what was asked, but no longer of who asked it.</div>
|
|
<ul class="evi"><li>both containers healthy 7 days</li><li>lf.yokogawa.tech → 302</li><li>certificate issued</li></ul>
|
|
</div>
|
|
|
|
<div class="phase s-done">
|
|
<span class="pn">03</span>
|
|
<div class="pbody"><h4>The documents</h4></div>
|
|
<span class="chip c-green"><i class="lamp l-green"></i>Done, with real documents</span>
|
|
<div class="pnote">Read, split on section boundaries so a numbered step sequence is never cut in
|
|
half, and indexed for search by meaning. <b>Four real documents</b> — three control
|
|
descriptions and an instrument document — have been through the new upload screen,
|
|
alongside the three fabricated demo documents that carry deliberately impossible numbers.
|
|
<b>The first real one exposed three silent faults in a single upload</b>, none of them
|
|
reachable by the tests as they stood: an eight-page document collapsed into one passage
|
|
because the PDF reader emits no blank lines; every passage was labelled with the revision
|
|
instead of the title; and re-publishing quietly duplicated the document. All three left the
|
|
screen looking correct. All three are fixed, and the two loading paths are now held identical
|
|
by a test that compares them character for character.</div>
|
|
<ul class="evi"><li>7 documents, 92 chunks</li><li>WRPS-CTL-001/002/003</li><li>WRPS-INS-001</li><li>3 demo docs, labelled</li><li>real embeddings since 27 Aug</li><li>19 chunks from the 8-page philosophy, largest 574 tokens</li></ul>
|
|
</div>
|
|
|
|
<div class="phase s-blocked">
|
|
<span class="pn">04</span>
|
|
<div class="pbody"><h4>Access to plant history — <span style="font-weight:400;color:var(--muted)">imh</span></h4></div>
|
|
<span class="chip c-red"><i class="lamp l-red"></i>Blocked — the only one</span>
|
|
<div class="pnote">The host is still being built and the read-only login has not been created.
|
|
Still outstanding: the database and table names, and the firewall rule. <b>The timezone question
|
|
is answered</b> — not by asking, but by reading the SCADA configuration, where all
|
|
49 points carry <span class="mono">Date+time GMT</span> and every history group has
|
|
daylight correction switched off. Storage is UTC. That was the answer two findings were waiting
|
|
on, and it had been sitting in a file in the SCADA repository the whole time.</div>
|
|
<ul class="evi"><li>USE_FIXTURES=true</li><li>IMH_DB blank</li><li>IMH_PASSWORD blank</li><li>svc_agent_ro not created</li><li>NSG rule not raised</li><li>timestamps confirmed UTC</li></ul>
|
|
</div>
|
|
|
|
<div class="phase s-part">
|
|
<span class="pn">05</span>
|
|
<div class="pbody"><h4>The data translator — <span style="font-weight:400;color:var(--muted)">Cube</span></h4></div>
|
|
<span class="chip c-amber"><i class="lamp l-amber"></i>Running on stand-ins</span>
|
|
<div class="pnote">Four models, every definition written down where an engineer can read and
|
|
check it: what counts as an alarm, what “last week” means, what counts as a fill.
|
|
Those definitions are the real ones and will not change when the historian arrives — only
|
|
the source will. <b>Two gate items are open:</b> no number has been verified against the real
|
|
historian by anybody, and the ready-made summaries are not being written into pg-ai as the
|
|
design specifies.</div>
|
|
<ul class="evi"><li>cube + cubestore healthy</li><li>pinned at v1.1.7</li><li>32 alarm activations</li><li>685,440 readings</li><li>72 pump-downs</li><li>7-day window, as CI Server keeps</li><li>cube_preagg schema empty</li></ul>
|
|
</div>
|
|
|
|
<div class="phase s-done">
|
|
<span class="pn">06</span>
|
|
<div class="pbody"><h4>The assistant itself</h4></div>
|
|
<span class="chip c-green"><i class="lamp l-green"></i>Done, on the real model</span>
|
|
<div class="pnote">Four lanes, four contracts, checked in code rather than asked for in a prompt.
|
|
<b>Eight faults have been found and pinned since the model went on</b> — five within an
|
|
hour of switching it on, three more that afternoon when the first real document went through.
|
|
The worst handed the answer writer a procedure’s step list, the one thing it must never
|
|
reproduce, while withholding the title block it actually needed. Every one was invisible while
|
|
the stand-in was in place.</div>
|
|
<ul class="evi"><li>NO_LLM_STUB=false</li><li>proven live 28 Aug in 3.96 s</li><li>steps withheld, identity returned</li><li>sqlglot allow-list enforced</li><li>eval cases L01–L08 pin all eight</li></ul>
|
|
</div>
|
|
|
|
<div class="phase s-part">
|
|
<span class="pn">07</span>
|
|
<div class="pbody"><h4>The operator’s screen</h4></div>
|
|
<span class="chip c-red"><i class="lamp l-red"></i>Built — last gate unproven</span>
|
|
<div class="pnote">The screen is live and the deny half of the new access rule was checked from
|
|
the host: it correctly refuses everybody it should. <b>The half that matters cannot be checked
|
|
from here</b> — that the one machine it is meant to admit is actually admitted. A typo in
|
|
the rule would refuse the control room too and look identical from this side. Until somebody
|
|
opens the page at the console, this phase is not finished.</div>
|
|
<ul class="evi"><li>ai.yokogawa.tech → 403 from lin001</li><li>POST /ask → 403, not 404</li><li>nobody has asked from cicore1</li></ul>
|
|
</div>
|
|
|
|
<div class="phase s-blocked">
|
|
<span class="pn">08</span>
|
|
<div class="pbody"><h4>The exam</h4></div>
|
|
<span class="chip c-red"><i class="lamp l-red"></i>Never run</span>
|
|
<div class="pnote">75 engineer-reviewable questions, every data-dependent one with a pinned time
|
|
window. Nothing blocks running it — the model account arrived on 27 August — and it
|
|
has never been run as a gate. No scorecard exists. Two cases fail by design until the historian
|
|
lands.</div>
|
|
<ul class="evi"><li>28 historical</li><li>18 procedural</li><li>14 advisory</li><li>13 reference</li><li>2 unclear</li><li>pass mark: 85% / 95% / zero breaches</li></ul>
|
|
</div>
|
|
|
|
<div class="phase s-odd">
|
|
<span class="pn">09</span>
|
|
<div class="pbody"><h4>Document management by operators</h4></div>
|
|
<span class="chip c-purple"><i class="lamp l-purple"></i>Live — built out of order</span>
|
|
<div class="pnote">Designed to follow the exam, for a reason: adding a way for more documents to
|
|
arrive makes a wrong answer harder to diagnose, not easier. <b>Built and put live first, at the
|
|
customer’s direction.</b> Upload, convert, review, approve, withdraw and restore all work
|
|
and have been used on four real documents. What has not moved is the judgement — nothing
|
|
is quotable until a named person confirms the number, revision and date, and the database
|
|
itself refuses an approved document without them.</div>
|
|
<ul class="evi"><li>api.yokogawa.tech/documents</li><li>7 published uploads</li><li>5 audit actions recorded</li><li>gate never assessed</li></ul>
|
|
</div>
|
|
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================================================================ 3 ==== -->
|
|
<section id="where">
|
|
<div class="sec-head"><span class="num">03</span><h2>What runs where</h2></div>
|
|
<p class="sec-sub">Every row read from the host today. <b>“Up” is not proof</b> —
|
|
each public address was probed and each claim checked against something other than the container
|
|
list.</p>
|
|
|
|
<div class="tblwrap">
|
|
<table>
|
|
<thead><tr>
|
|
<th>Component</th><th>Host & network</th><th>Reachable at</th><th>What proves it works</th>
|
|
</tr></thead>
|
|
<tbody>
|
|
<tr><td class="name">pg-ai</td>
|
|
<td class="mono">lin001 · ai-internal only<br>/datadisk/pg-ai</td>
|
|
<td class="mono">nothing — no host port</td>
|
|
<td>Healthy 8 days. 92 document chunks queried by hand; the read-only role proven unable to write.</td></tr>
|
|
<tr><td class="name">cube</td>
|
|
<td class="mono">lin001 · ai-internal + proxy</td>
|
|
<td class="mono">cube.yokogawa.tech → 302</td>
|
|
<td>Healthy 7 h and answering the data lane — but off stand-in tables, not the historian.</td></tr>
|
|
<tr><td class="name">cubestore</td>
|
|
<td class="mono">lin001 · ai-internal<br>/datadisk/cubestore</td>
|
|
<td class="mono">nothing — no host port</td>
|
|
<td>Up 7 days. A hard dependency of cube, not an optimisation — cube will not start without it. <b>Absent from the original architecture deck.</b></td></tr>
|
|
<tr><td class="name">ai-api</td>
|
|
<td class="mono">lin001 · ai-internal + proxy<br>0600 env file</td>
|
|
<td class="mono">api.yokogawa.tech → 302<br>and /ask under ai.…</td>
|
|
<td>Healthy 2 h. A procedural question answered end to end in 3.96 s: correct document identity, prerequisites quoted, steps withheld, scope banner present.</td></tr>
|
|
<tr><td class="name">ai-web</td>
|
|
<td class="mono">lin001 · proxy · static build</td>
|
|
<td class="mono">ai.yokogawa.tech → 403</td>
|
|
<td>Up 26 h. The 403 is the deny arm working correctly. The allow arm is unproven and cannot be proven from here.</td></tr>
|
|
<tr><td class="name">langfuse + lf-db</td>
|
|
<td class="mono">lin001 · ai-internal + proxy<br>/datadisk/langfuse</td>
|
|
<td class="mono">lf.yokogawa.tech → 302</td>
|
|
<td>Both healthy 7 days. Every question traced — anonymously, since 28 August.</td></tr>
|
|
<tr><td class="name">ai-ingest</td>
|
|
<td class="mono">lin001 · ai-internal<br>profiles: [ingest]</td>
|
|
<td class="mono">nothing — batch job</td>
|
|
<td><b>Not running, by design.</b> The terminal ingest path, started by hand when documents change.</td></tr>
|
|
<tr><td class="name">ai-docs-worker</td>
|
|
<td class="mono">lin001 · ai-internal<br>profiles: [worker]</td>
|
|
<td class="mono">nothing</td>
|
|
<td><b>Not running and not deployed.</b> The upload path was built inside ai-api instead, so conversion and embedding happen in the request.</td></tr>
|
|
<tr class="row-amber"><td class="name">openplc-runtime</td>
|
|
<td class="mono">lin001 · host ports 502, 8443</td>
|
|
<td class="mono">Modbus TCP, bound 10.0.0.17</td>
|
|
<td>Up 8 days. <b>Live control for this demo.</b> The one deliberate exception to the no-published-ports rule, contained by that binding plus firewall scope. Never restarted as a side effect of AI work.</td></tr>
|
|
<tr class="row-red"><td class="name">svc_agent_ro</td>
|
|
<td class="mono">yau-sls-poc-imh · TDS 1433</td>
|
|
<td class="mono">not reachable</td>
|
|
<td><b>Does not exist.</b> The single thing this project is still waiting on from anybody.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="notes">
|
|
<div class="note e-red">
|
|
<h3>Only one address works from inside the plant network</h3>
|
|
<p><code>ai.yokogawa.tech</code> has an internal record pointing at the host and resolves.
|
|
<code>api</code>, <code>cube</code>, <code>lf</code> and — the one that bit —
|
|
<code>auth</code> do not. All four answer correctly when reached; they simply cannot be
|
|
reached from the plant floor.</p>
|
|
<p class="why"><b>Why it went unnoticed:</b> the equipment that talks to this host writes
|
|
straight to the database over a path that skips sign-in entirely. No browser had ever opened
|
|
the sign-in page from inside the network. It no longer affects the operator, who does not sign
|
|
in — but it will catch the next protected service anyone tries to open from the plant floor.</p>
|
|
</div>
|
|
<div class="note e-amber">
|
|
<h3>Why the operator’s question goes through one address</h3>
|
|
<p>Because <code>api.yokogawa.tech</code> cannot be resolved from a control-room PC, the
|
|
operator’s page would have loaded perfectly and then failed on every single question. So
|
|
the question route is served under the same address as the page itself, and the browser never
|
|
names the assistant.</p>
|
|
<p class="why"><b>Only the question route is shared.</b> Widening it would put the document
|
|
library on an address that carries no identity at all — which is exactly what the
|
|
access change of 28 August made <code>ai.yokogawa.tech</code>.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================================================================ 4 ==== -->
|
|
<section id="findings">
|
|
<div class="sec-head"><span class="num">04</span><h2>Three findings, one defect — closed</h2></div>
|
|
<p class="sec-sub">They were not three problems. They were one substitution, showing through in
|
|
three places: <b>the stand-in historian was keyed on names the historian does not use.</b> The
|
|
earlier plan was to reconcile the tag set against the register map. That would not have found
|
|
this, because the register map is not where the answer lives either.</p>
|
|
|
|
<div class="callout" style="border-left-color:var(--green)">
|
|
<h3 style="color:var(--green)">The thing nobody had written down</h3>
|
|
<p><b>Four different names describe the wet well level, and only the fourth is what the
|
|
historian stores.</b> The instrument on the drawing is <span class="mono">LIT-101</span>. The
|
|
PLC publishes it at <span class="mono">%QW0</span>. SCADA polls that register and calls the
|
|
point <span class="mono">PS_STN_WET_WELL_LEVEL</span>. And then CI Server files the history
|
|
under an <em>item</em> name, <span class="mono">AID.WRPS.STN.LEVEL</span> — which appears
|
|
nowhere in this project’s repository, because nothing had ever needed it before.</p>
|
|
<p><b>Modbus carries register numbers, not names.</b> That is why the point layer and the item
|
|
layer are free to drift apart, and why reconciling against the register map would have proved
|
|
only that the first three agreed with each other. The stand-in was built on the third name. The
|
|
real historian answers to the fourth.</p>
|
|
</div>
|
|
|
|
<div class="notes">
|
|
<div class="note e-green">
|
|
<span class="lbl">Finding A · opened 21 Aug · closed 31 Aug</span>
|
|
<h3>The wet well level tag does not join</h3>
|
|
<p>Confirmed and fixed. The level history is now keyed on the item the historian actually
|
|
uses, and <span class="mono">LIT-101</span> is marked as what it is — a field input to
|
|
the PLC that never reaches SCADA at all. It was the only row in the whole tag list carrying
|
|
two addresses, which is the instrument and the published value quietly merged into one.</p>
|
|
<p class="why"><b>Why it will not come back:</b> the mapping from item to tag is generated
|
|
from the SCADA files, not typed. An item that resolves to neither a tag nor a written reason
|
|
for having none now fails the build, fails the deploy and fails the verify script. The old
|
|
failure mode — a join matching nothing and reporting “no records found”
|
|
— cannot be reached silently any more.</p>
|
|
<p class="mono" style="font-size:.72rem">eval case H26 rewritten · H30 added</p>
|
|
</div>
|
|
|
|
<div class="note e-green">
|
|
<span class="lbl">Finding B · opened 21 Aug · closed 31 Aug</span>
|
|
<h3>First and last alarm times come back in UTC</h3>
|
|
<p>Fixed inside the translator, where it had to be. The conversion now happens in the
|
|
measure itself, and it happens in the right order — the earliest instant is found
|
|
first and converted afterwards. Doing it the other way round takes the earliest
|
|
<em>clock reading</em>, which picks the wrong record on the night the clocks go back and one
|
|
local hour happens twice.</p>
|
|
<p class="why"><b>The answer came from a file, not a meeting.</b> This was waiting on
|
|
confirmation of whether the historian stores UTC or local time. Every one of the 49 SCADA
|
|
points is configured <span class="mono">Date+time GMT</span>, and every history group has
|
|
daylight correction off. The time is also no longer handed out as a bare timestamp: it comes
|
|
with the timezone beside it, because a clock time with no zone is exactly what let this hide
|
|
for a week.</p>
|
|
<p class="mono" style="font-size:.72rem">eval case H27 rewritten</p>
|
|
</div>
|
|
|
|
<div class="note e-green">
|
|
<span class="lbl">Finding C · opened 28 Aug · closed 31 Aug</span>
|
|
<h3>The high level alarm is filed against the wrong equipment</h3>
|
|
<p><b>Both sides were right, which is why neither could win.</b> The tag list said the
|
|
station, because that is the section CI Server files the item under. The stand-in said the
|
|
wet well, because that is what the alarm is about. The defect was never which one was
|
|
correct — it was that the same fact was being asserted twice.</p>
|
|
<p class="why"><b>Fixed by removing the second assertion.</b> The history now carries no
|
|
equipment column at all, which also happens to be faithful: CI Server’s equipment tree
|
|
stops at the station and the three pumps, and has no wet well to put there. Equipment is
|
|
stated once, in the tag list, and reached from an alarm through the alarm bit that raised it.
|
|
The verify script fails if an equipment column ever reappears in the history.</p>
|
|
<p class="mono" style="font-size:.72rem">eval case H31 added, expected count pinned at 14</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="callout">
|
|
<h3>Three things the SCADA files changed that nobody had reported as faults</h3>
|
|
<p><b>The historian keeps seven days, not thirty.</b> The stand-in had been generating a month.
|
|
Every question about last month worked here and would have failed the moment it was pointed at
|
|
the real thing. The stand-in now keeps seven days too, so that failure happens where it can be
|
|
seen. The system distinguishes <em>“the historian does not go back that far”</em>
|
|
from <em>“nothing happened”</em> — different answers, and only one of them
|
|
true. <b>Extending retention is now a written request</b>, because an assistant that cannot
|
|
answer “last month” is of limited use.</p>
|
|
<p><b>The level is sampled every five seconds, not every minute.</b> Two measures turned a count
|
|
of samples into a duration by multiplying by sixty. Against a five-second signal that overstates
|
|
by <b>twelve times</b> — and it would have read as an entirely plausible number. They now
|
|
read the interval from the item rather than assuming one.</p>
|
|
<p><b>The warning about compressed data was wrong.</b> The translator carried a prominent note
|
|
saying real history would arrive irregularly spaced, so ordinary averages could not be trusted.
|
|
The SCADA configuration has compression switched off on every group. The note was steering
|
|
people away from the correct measure, and it has been corrected.</p>
|
|
</div>
|
|
|
|
<div class="callout" style="border-left-color:var(--amber)">
|
|
<h3>What this does <em>not</em> prove</h3>
|
|
<p>The stand-in now checks itself: it asserts its own alarm counts as it loads, and proves them
|
|
twice over by deriving the same figure from two independent signals and refusing to load if they
|
|
disagree. <b>That is a test of the pipeline, not a fact about the plant.</b> The number this
|
|
system currently gives for high level alarms is a fact about generated data and nothing else.</p>
|
|
<p>Two things still need a person. <b>An engineer has to confirm the first real figures by
|
|
hand</b> once the historian is connected. And <b>the SCADA repository and the live server
|
|
disagree about sample rates</b> — the checked-in file says one second, the running system
|
|
says five. The running system was taken as correct here, but that means the repository does not
|
|
currently describe the machine, and somebody should decide which of the two is wrong.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================================================================ 5 ==== -->
|
|
<section id="catalogue">
|
|
<div class="sec-head"><span class="num">05</span><h2>Every component, and what its job is</h2></div>
|
|
<p class="sec-sub">Twenty-eight containers on one shared host. Seven are ours; twenty-one were
|
|
already here and serve other demos. This is the list the architecture deck never carried.</p>
|
|
|
|
<h3 style="margin-top:34px">Built by this project</h3>
|
|
<div class="cat">
|
|
|
|
<div class="comp e-purple">
|
|
<div class="comp-top"><i class="lamp l-green"></i><span class="comp-name">pg-ai</span></div>
|
|
<span class="comp-img">pgvector/pgvector:pg16</span>
|
|
<p class="job">The store. Holds the searchable text of every controlled document, the equipment
|
|
and tag list that turns “Pump 02” into a real tag, the upload queue, the permanent
|
|
audit trail, and — until the historian arrives — the stand-in plant history.</p>
|
|
<p>Five roles, deliberately split: the answer path can only read, only the ingestion role can
|
|
write documents, and a database trigger stops the web role un-withdrawing anything.</p>
|
|
<div class="comp-foot"><span class="k">Reached by</span><span>ai-api, cube and the ingest jobs, container to container. Never published.</span></div>
|
|
</div>
|
|
|
|
<div class="comp e-purple">
|
|
<div class="comp-top"><i class="lamp l-amber"></i><span class="comp-name">cube</span></div>
|
|
<span class="comp-img">cubejs/cube:v1.1.7 · pinned</span>
|
|
<p class="job">The data translator. Turns “high level alarms last week” into an
|
|
exact query over recorded history. The definitions that make an answer right live in files an
|
|
engineer can read and check, rather than being invented per question.</p>
|
|
<p>The assistant never writes database code itself — it fills in a request form and Cube
|
|
does the rest. Today it reads stand-in tables, so no number it produces means anything about
|
|
the plant, and every answer built on one says so on screen.</p>
|
|
<div class="comp-foot"><span class="k">Pinned</span><span>Kept out of the host’s auto-updater on purpose.</span></div>
|
|
</div>
|
|
|
|
<div class="comp e-purple">
|
|
<div class="comp-top"><i class="lamp l-green"></i><span class="comp-name">cubestore</span></div>
|
|
<span class="comp-img">cubejs/cubestore:v1.1.7 · pinned</span>
|
|
<p class="job">Cube’s own storage engine. Holds the queue and the ready-made summaries
|
|
that keep “count last week” fast without repeatedly scanning the historian.</p>
|
|
<p>Not an optimisation and not optional — Cube will not start without it. It is missing
|
|
from the original architecture deck and from the build documents’ container table, which
|
|
is why it is called out here.</p>
|
|
<div class="comp-foot"><span class="k">Storage</span><span>/datadisk/cubestore. Watch its growth; retention is the refresh policy plus manual pruning.</span></div>
|
|
</div>
|
|
|
|
<div class="comp e-purple">
|
|
<div class="comp-top"><i class="lamp l-green"></i><span class="comp-name">ai-api</span></div>
|
|
<span class="comp-img">yau/ai-api:local · Python 3.12 + FastAPI</span>
|
|
<p class="job">The assistant itself, and the busiest thing here. It sorts each question into
|
|
one of four kinds, resolves the plant names in it, sends it down the matching lane, gathers the
|
|
evidence, has the model word an answer, and then checks that answer against a contract written
|
|
in code before letting it out.</p>
|
|
<p>Since 28 August it also serves the document library — upload, review, approve,
|
|
withdraw and restore — because the operator’s screen no longer carries any identity.</p>
|
|
<div class="comp-foot"><span class="k">Refuses</span><span>Any answer failing its contract. Regenerates once, then errors. Never returns it.</span></div>
|
|
</div>
|
|
|
|
<div class="comp e-purple">
|
|
<div class="comp-top"><i class="lamp l-green"></i><span class="comp-name">ai-web</span></div>
|
|
<span class="comp-img">Vite build → nginx:alpine</span>
|
|
<p class="job">The operator’s screen. A question box, the answer, and a
|
|
“show working” panel giving the question’s kind, the query that ran, the row
|
|
count, and every citation with its revision and effective date.</p>
|
|
<p>It carries two banners an operator cannot switch off: one saying the figures are stand-ins,
|
|
and one on procedural and advisory answers saying what the assistant deliberately did
|
|
<em>not</em> do.</p>
|
|
<div class="comp-foot"><span class="k">Access</span><span>One IP address only, no sign-in. Everything else gets 403.</span></div>
|
|
</div>
|
|
|
|
<div class="comp e-purple">
|
|
<div class="comp-top"><i class="lamp l-green"></i><span class="comp-name">langfuse</span> <span class="comp-name" style="color:var(--muted)">+ lf-db</span></div>
|
|
<span class="comp-img">langfuse/langfuse:2 · postgres:16-alpine</span>
|
|
<p class="job">The logbook. Records every question: how it was sorted and with what confidence,
|
|
which tools ran, which passages were retrieved, how many tokens it cost, how long it took, and
|
|
whether the contract passed or failed. Every rejection is logged with the offending output.</p>
|
|
<p>Deployed early on purpose, so no experiment since has been untraceable. Self-hosted and
|
|
MIT-licensed — no licence cost, and no question text leaves the host to reach it.</p>
|
|
<div class="comp-foot"><span class="k">Since 28 Aug</span><span>Traces are anonymous — the sign-in that supplied the name is gone.</span></div>
|
|
</div>
|
|
|
|
<div class="comp e-muted">
|
|
<div class="comp-top"><i class="lamp l-muted"></i><span class="comp-name">ai-ingest</span></div>
|
|
<span class="comp-img">yau/ai-ingest:local · not running</span>
|
|
<p class="job">The terminal path for loading documents, started by hand when documents change.
|
|
Reads a file, splits it on section boundaries so a numbered step sequence is never cut in half,
|
|
asks a person at the keyboard to confirm the document number, revision and effective date, and
|
|
only then stores it.</p>
|
|
<p><b>It cannot see anything loaded through the upload screen</b> — published files stay
|
|
in the upload area rather than moving into the document tree. The two paths must not be used on
|
|
the same document.</p>
|
|
<div class="comp-foot"><span class="k">Guard</span><span>Refuses to resurrect a withdrawn document, whatever folder it is in.</span></div>
|
|
</div>
|
|
|
|
<div class="comp e-muted">
|
|
<div class="comp-top"><i class="lamp l-muted"></i><span class="comp-name">ai-docs-worker</span></div>
|
|
<span class="comp-img">same image as ai-ingest · never deployed</span>
|
|
<p class="job">Designed as a background worker that would pre-scan uploads, ingest approved
|
|
ones and complete withdrawals, kept off the network entirely so that the web-facing part of the
|
|
system could never write documents directly.</p>
|
|
<p><b>Not built.</b> That work happens inside ai-api instead, so the same process holds both
|
|
permissions. The database trigger still stops the web role un-withdrawing anything, so the
|
|
boundary holds — but it is now a code boundary rather than a deployment one.</p>
|
|
<div class="comp-foot"><span class="k">Consequence</span><span>A large upload blocks its own request while it converts.</span></div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<h3 style="margin-top:44px">Already on the host — do not disturb</h3>
|
|
<p class="sec-sub">This host has served demos for months. Restarting the first two logs out every
|
|
active user, including whoever is mid-demo.</p>
|
|
<div class="cat">
|
|
|
|
<div class="comp e-blue">
|
|
<div class="comp-top"><i class="lamp l-green"></i><span class="comp-name">caddy</span></div>
|
|
<span class="comp-img">caddy:2 · host ports 80, 443</span>
|
|
<p class="job">The only front door. Every screen on this host is reached through it, and it
|
|
obtains and renews the certificates automatically. Nothing this project built publishes a port
|
|
of its own — they all sit behind it on a shared internal network.</p>
|
|
<div class="comp-foot"><span class="k">Careful</span><span>A reload is how the 28 August access change is undone, and how it would be widened.</span></div>
|
|
</div>
|
|
|
|
<div class="comp e-blue">
|
|
<div class="comp-top"><i class="lamp l-green"></i><span class="comp-name">authelia</span> <span class="comp-name" style="color:var(--muted)">+ authelia-portal</span></div>
|
|
<span class="comp-img">authelia/authelia · nginx:alpine</span>
|
|
<p class="job">Sign-in for the whole host: Active Directory plus a phone prompt. Omitting it
|
|
from a site block silently makes that service public, which is why every block here carries it
|
|
— with the one deliberate exception of the operator’s screen.</p>
|
|
<div class="comp-foot"><span class="k">Careful</span><span>Restarting it logs out every active user. It also resolves direct group membership only.</span></div>
|
|
</div>
|
|
|
|
<div class="comp e-amber">
|
|
<div class="comp-top"><i class="lamp l-amber"></i><span class="comp-name">openplc-runtime</span></div>
|
|
<span class="comp-img">openplc-runtime-migrated:v4.1.10 · ports 502, 8443</span>
|
|
<p class="job"><b>The PLC for this demo plant.</b> It runs the actual control logic for the
|
|
three pumps and the wet well; the SCADA server polls it over Modbus and historises the result.
|
|
Live control, not a simulation of one.</p>
|
|
<div class="comp-foot"><span class="k">Never</span><span>Restart, update or reconfigure it as a side effect of AI work, or change its binding.</span></div>
|
|
</div>
|
|
|
|
<div class="comp e-blue">
|
|
<div class="comp-top"><i class="lamp l-green"></i><span class="comp-name">influxdb</span></div>
|
|
<span class="comp-img">influxdb:2.7 · 55 GB on /datadisk</span>
|
|
<p class="job">The host’s main time-series store, used by the other demos on this box.
|
|
Nothing this project built reads or writes it. It is listed here because it is effectively the
|
|
only consumer of the shared data disk, growing roughly a gigabyte a week — so it, not us,
|
|
is what will eventually fill it.</p>
|
|
<div class="comp-foot"><span class="k">Watch</span><span>The disk alert exists in a dashboard but notifies nobody.</span></div>
|
|
</div>
|
|
|
|
<div class="comp e-blue">
|
|
<div class="comp-top"><i class="lamp l-green"></i><span class="comp-name">forgejo</span></div>
|
|
<span class="comp-img">codeberg.org/forgejo/forgejo:10</span>
|
|
<p class="job">The internal Git server, and the master copy of this project’s repository.
|
|
It sits outside the shared sign-in because forwarding authentication breaks Git clients —
|
|
a reason that applies to nothing else here.</p>
|
|
<div class="comp-foot"><span class="k">Note</span><span>Like the sign-in service, it resolves direct group membership only.</span></div>
|
|
</div>
|
|
|
|
<div class="comp e-blue">
|
|
<div class="comp-top"><i class="lamp l-green"></i><span class="comp-name">grafana</span> <span class="comp-name" style="color:var(--muted)">· nodered · mosquitto · telegraf</span></div>
|
|
<span class="comp-img">the host’s existing data path</span>
|
|
<p class="job">Device data arrives over MQTT into <b>mosquitto</b>, is processed by
|
|
<b>nodered</b>, and is stored in InfluxDB; <b>telegraf</b> adds host and container metrics; and
|
|
<b>grafana</b> draws the dashboards over the top. None of it touches the assistant.</p>
|
|
<div class="comp-foot"><span class="k">Known defect</span><span>Grafana’s admin password sits in plain text in the host compose file. Not a pattern to copy.</span></div>
|
|
</div>
|
|
|
|
<div class="comp e-blue">
|
|
<div class="comp-top"><i class="lamp l-green"></i><span class="comp-name">portainer</span> <span class="comp-name" style="color:var(--muted)">· dozzle · watchtower</span></div>
|
|
<span class="comp-img">the host’s operations tools</span>
|
|
<p class="job"><b>portainer</b> is graphical Docker management and is root-equivalent;
|
|
<b>dozzle</b> is live searchable container logs and the best first stop when something here
|
|
misbehaves; <b>watchtower</b> auto-updates a safe subset of images on Sunday mornings.</p>
|
|
<div class="comp-foot"><span class="k">Rule</span><span>pg-ai and cube stay off Watchtower’s list. Pinned images stay pinned.</span></div>
|
|
</div>
|
|
|
|
<div class="comp e-blue">
|
|
<div class="comp-top"><i class="lamp l-green"></i><span class="comp-name">wireguard</span> <span class="comp-name" style="color:var(--muted)">· showroom · eqp-licence</span></div>
|
|
<span class="comp-img">unrelated host services</span>
|
|
<p class="job"><b>wireguard</b> is the VPN through which engineers reach this host at all;
|
|
<b>showroom</b> is a static demo site; <b>eqp-licence</b> issues licences with a signing key
|
|
mounted read-only. None is part of the assistant.</p>
|
|
<div class="comp-foot"><span class="k">Since 28 Aug</span><span>The assistant is no longer reachable by browser over the VPN. Engineers need an SSH tunnel.</span></div>
|
|
</div>
|
|
|
|
<div class="comp e-muted">
|
|
<div class="comp-top"><i class="lamp l-muted"></i><span class="comp-name">chirpstack</span> <span class="comp-name" style="color:var(--muted)">×4 support containers</span></div>
|
|
<span class="comp-img">chirpstack + postgres, redis, mqtt, gateway-bridge</span>
|
|
<p class="job">A LoRaWAN network server and its supporting containers. Running but
|
|
<b>not configured</b> — a future capability for this host, unrelated to the assistant.
|
|
Safe to ignore.</p>
|
|
<div class="comp-foot"><span class="k">Counts</span><span>Five of the twenty-one existing containers, which is why the total is larger than it looks.</span></div>
|
|
</div>
|
|
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================================================================ 6 ==== -->
|
|
<section id="shortcuts">
|
|
<div class="sec-head"><span class="num">06</span><h2>Deliberate shortcuts, all reversible</h2></div>
|
|
<p class="sec-sub">Documented rather than hidden, and none of them to be shipped. Production closes
|
|
them in this order: network separation, then secrets, then database guardrails, then document
|
|
control integration, then resilience.</p>
|
|
|
|
<ul class="shortcuts">
|
|
<li><span class="k">Access</span><span><b>The operator’s screen admits one IP address with no sign-in.</b> On a flat network with no boundary between the office and plant sides, anything that can take that address inherits the same access. A demo affordance, not a security control — and the first thing network separation replaces.</span></li>
|
|
<li><span class="k">Network</span><span>No firewall boundary between the office and plant sides — one flat network. Modbus runs with no authentication or encryption, contained only by its binding and firewall scope.</span></li>
|
|
<li><span class="k">Identity</span><span><b>The document publisher is a name typed into a box</b>, checked against a one-entry list with no password. Rows are permanently marked as unverified so they stay tellable apart once real sign-in goes on.</span></li>
|
|
<li><span class="k">Documents</span><span>Scanned documents cannot be read at all and are refused rather than stored empty, because the proper document reader needs software this host must not run. Uploads are not scanned for malware — type and size checks only.</span></li>
|
|
<li><span class="k">Secrets</span><span>Kept in restricted files on the host rather than a vault. A shared login to the host means no per-person audit trail.</span></li>
|
|
<li><span class="k">Resilience</span><span><b>One host, no standby</b> — and it now carries both the demo estate and the simulated plant’s PLC. No automated backup; the assistant’s database needs adding to whatever backup exists.</span></li>
|
|
<li><span class="k">Trust</span><span>The assistant trusts the sign-in headers it is handed, because nothing outside the shared internal network can reach it. That assumption is exactly as strong as the no-published-ports rule.</span></li>
|
|
</ul>
|
|
|
|
<div class="callout">
|
|
<h3 style="color:var(--red)">Before any operator sees a demo</h3>
|
|
<p>What the assistant will and will not say must be reviewed with an OT or safety representative.
|
|
That review is still outstanding, and it now has to cover the unauthenticated console as well.
|
|
Reviewing it was only meaningful once the model was in place — which it now is, so nothing
|
|
stands in the way of booking it.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<footer>
|
|
<p><b>Sources.</b> Read from <code>yau-sls-poc-lin001</code> on 28 August 2026: container list,
|
|
environment keys, TLS probes of all five hostnames, direct queries against <code>pg-ai</code>, and
|
|
two questions put through the live assistant. Cross-checked against
|
|
<code>BUILD-AI-CONTAINERS.md</code>, <code>README.md</code>, <code>REQUESTS.md</code> and
|
|
<code>workflow-map.html</code>.</p>
|
|
<p>Companion documents: <b>workflow-map.html</b> for how a question becomes an answer, and
|
|
<b>yau-poc-status-2026-08-28.pptx</b> for the same picture as slides.</p>
|
|
</footer>
|
|
|
|
</div>
|
|
</body>
|
|
</html>
|