Commit Graph

90 Commits

Author SHA1 Message Date
Joseph Doherty 115a43dd47 feat(mtconnect): typed AdminUI tag-config editor + browse round-trip (Task 18 Part A)
Registers the MTConnect typed tag editor in TagConfigEditorMap /
TagConfigValidator (keyed off DriverTypeNames.MTConnect, never a literal) and
adds the razor shell over Task 17's MTConnectTagConfigModel.

The shell stays trivial: the /probe-sourced mtCategory/mtType/mtSubType/units
row is rendered disabled + readonly with NO binding of any kind, and no
inferred-type hint is shown — MTConnectDataTypeInference.Infer() also keys on
the DataItem `representation`, which the tag-config model does not carry, so
calling it here would agree with the browse picker for ordinary items and
silently disagree for exactly the DATA_SET/TABLE/TIME_SERIES ones.

Also fixes the picker -> editor round trip, which was broken in both
directions:

- RawBrowseCommitMapper.BuildTagConfig had no MTConnect branch, so a committed
  tag landed as the generic {"address": "<dataItemId>"} while the model read
  only `fullName`. The data plane kept working (the driver accepts all three id
  spellings), so the only symptom was an empty DataItem-id field in the editor
  — and saving from that state stamped fullName:"" over the tag.
- MTConnectTagConfigModel.FromJson now accepts dataItemId/address as fallbacks
  (fixing tags committed before this change) and ToJson normalises onto
  `fullName`, dropping the aliases so the two spellings cannot drift.
2026-07-27 13:58:19 -04:00
Joseph Doherty b0046d6959 feat(mtconnect): typed AdminUI tag-config model (Task 17)
MTConnectTagConfigModel: pure FromJson/ToJson/Validate for the MTConnect
tag TagConfig JSON blob, mirroring ModbusTagConfigModel/OpcUaClientTagConfigModel.
Fields: fullName (DataItem@id, required), dataType (DriverDataType override,
written as an enum name string), mtCategory/mtType/mtSubType/units (probe
metadata), mtDevice/mtComponent (author context). Preserves unknown JSON
keys across load->save (isHistorized/historianTagname and anything else).

Guards against the enum-serialization trap (numeric dataType would fault the
driver at deploy) both on write (always TagConfigJson.Set via enum name) and
on read (Validate() rejects a dataType that was stored as a bare digit
string, since Enum.TryParse silently accepts numeric text).

AdminUI.csproj gains a ProjectReference to Driver.MTConnect.Contracts,
matching the existing POCO-only driver-Contracts pattern used by the other
six typed tag editors.

Scope: pure model only (Task 17). The razor editor shell + TagConfigEditorMap/
TagConfigValidator registration is Task 18.
2026-07-27 13:58:06 -04:00
Joseph Doherty c3a2b0f773 Merge branch 'feat/mqtt-sparkplug-driver'
v2-ci / build (push) Successful in 5m36s
v2-ci / unit-tests (push) Failing after 22m40s
# Conflicts:
#	src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeNames.cs
#	src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverConfigModal.razor
#	src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/EndpointRouteBuilderExtensions.cs
#	src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigEditorMap.cs
#	src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigValidator.cs
#	src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ZB.MOM.WW.OtOpcUa.AdminUI.csproj
#	src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs
2026-07-27 13:31:39 -04:00
Joseph Doherty 8d9155682d fix(mqtt): browse-commit writes a bindable address + a Request-rebirth affordance
Two gaps the Task 23/24 review found, both blocking Task 26's live gate.

Gap 1 — browse-commit produced a silently dead MQTT tag. RawBrowseCommitMapper
had no `Mqtt` case, so a committed leaf fell through to the generic
`{"address": …}` key. Neither MqttTagDefinitionFactory entry point reads
`address`: the tag deployed clean and reported BadNodeIdUnknown forever, with
no signal at commit time. Predates Task 23 (Plain was affected too), but Task 23
built the Sparkplug metric tree precisely so an operator could browse and commit
a binding.

The address is a DESCRIPTOR, not a reference string, and it cannot be recovered
from the browse node id: `{group}/{node}[/{device}]::{metric}` where a metric
name legitimately contains `/` (`Node Control/Rebirth`) — the ambiguity
MetricSeparator's remarks already name. So the session STATES it, via a new
`AttributeInfo.AddressFields` seam, and the mapper reads it. Which keys are
emitted is also how the mapper learns Plain vs Sparkplug — the driver type
reaching it is just `Mqtt`, and the mode lives on the driver config. Key names
are single-sourced in the new `MqttTagConfigKeys` (producer, mapper, factory),
with the literals pinned by a test so a symmetric rename cannot silently unbind
already-persisted blobs. A leaf with no stated address is refused at commit in
words rather than committed dead.

Gap 2 — RequestRebirthAsync had no UI. Task 23 shipped it backend-only; Task 26
step 1 assumes the button, and Task 25's runbook notes the picker tree stays
empty until a birth lands, so it is the only way to fill it on demand. Added to
the /raw browse modal: scope = the clicked tree node (device/metric resolve up to
their edge node, group fans out and is refused whole past 32), an explicit
two-click confirm naming the resolved scope and its consequence, the outcome or
the refusal shown rather than swallowed. Offered only for a Sparkplug session
(new `IRebirthCapableBrowseSession.RebirthAvailable` — one session class serves
both modes, so a type test alone would draw a button that can only throw) and
only to a DriverOperator; the server-side gate remains the boundary.

Tests: MQTT 545 → 581, AdminUI 781 → 800. The round-trip tests feed the emitted
blob to the REAL factory and were falsified by mutating the emitted key name.

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-24 23:43:07 -04:00
Joseph Doherty cd0157a3b8 feat(mqtt): typed tag editor Sparkplug mode + validation
Task 24 (P2): replaces the P1 Sparkplug Validate() stub (always null) and the
"not available yet" editor placeholder with real groupId/edgeNodeId/deviceId/
metricName authoring, mirroring MqttTagDefinitionFactory.FromSparkplugTagConfig
in both directions -- required ids + optional deviceId, dataType/qos read
strictly but only when present (dataType is optional: the birth certificate
declares it). One rule is deliberately stricter than the factory: group/edge-
node/device ids reject '/', '+', '#' (topic-segment characters a decoded
incoming id can never carry, so an authored one that does can never bind);
metricName is exempt since Sparkplug's own canonical names use '/' (e.g.
"Node Control/Rebirth"). No FullName key is written -- TagConfig carries no
identity under v3, and the plan's snippet asking for one was corrected per the
brief. A SparkplugB tag now survives save/reopen because its descriptor keys
re-infer Mode on load; there was never a 'mode' key to persist.

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-24 22:43:44 -04:00
Joseph Doherty ff62b62a55 refactor(mqtt): accept a blank jsonPath; guide with a seeded "$" instead
Review follow-up. Dropped the hard Validate rejection of a blank jsonPath under
a Json payload: the runtime defaults it to the document root, which is the real
"publisher puts a bare JSON scalar on the topic" case, so rejecting it made the
editor refuse what the driver accepts — inverting "authoring surface accepts <=>
publish accepts" — and blocked whole CSV-import batches, since this validator
also gates RawManualTagEntryModal's review grid.

Replaced with guidance: an UNAUTHORED tag (no topic AND no jsonPath) seeds the
field with "$", so a fresh tag emits an explicit "jsonPath":"$" while an
existing path-less tag keeps the key ABSENT and the driver defaults it. The
wildcard-topic rule stays strict — a wildcard has no legitimate single-Tag
interpretation.

Also: omit a blank "topic" rather than writing "topic":"", and record why the
_lastConfigJson guard diverges from the Modbus template — the landmine is a
DERIVED, NON-PERSISTED UI FIELD INFERRED FROM BLOB CONTENT (Mode), which Task
24's Sparkplug field group will be tempted to add more of. Named the host-side
dependency (RawTagModal only mutates through this callback) that keeps the
staleness trade benign today.

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-24 17:37:13 -04:00
Joseph Doherty 36abd871a1 feat(mqtt): typed tag editor + validator (plain mode)
Thin typed model over a preserved JsonObject key bag, mirroring the sibling
<Driver>TagConfigModel template. Key names and strictness rules mirror
MqttTagDefinitionFactory rather than inventing an editor-side schema; enums
round-trip as NAMES (the factory reads them strictly by name).

No FullName key: under v3 a tag is identified by its RawPath, which the factory
keys the definition's Name off — a composed identity key here would be dead
weight nothing reads.

Mode is a UI-only sub-shape selector inferred from the blob (any Sparkplug
descriptor key present) and never serialised — the driver takes Plain vs
SparkplugB from its DRIVER config. Only Plain Validate() is implemented; the
Sparkplug branch is a stub Task 24 fills, and its keys are preserved untouched.

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-24 17:17:37 -04:00
Joseph Doherty 443b718158 feat(sql): typed AdminUI Sql tag-config model + validator (string enums)
SqlTagConfigModel + SqlRowSelectorModel mirror SqlEquipmentTagParser's
accept/reject boundary byte-for-byte on the fields they own: model/type
serialize as NAME strings (never numbers), KeyValue requires
table+keyColumn+keyValue+valueColumn, WideRow requires table+columnName+a
selector (where-pair OR topByTimestamp), Query is rejected, and unknown keys
(top-level and nested rowSelector) survive load->save. Registered in
TagConfigEditorMap + TagConfigValidator keyed off SqlDriver.DriverTypeName
(DriverTypeNames.Sql is deferred to Task 11). A minimal placeholder
SqlTagConfigEditor.razor lands the map entry now; Task 20 fleshes out the UI.

The load-bearing test rounds editor output back through
SqlEquipmentTagParser.TryParse (editor-output <=> parser-input agreement).

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-24 15:27:14 -04:00
Joseph Doherty 96af69e3d2 review(wave-b): close SA {{equip}} authoring gap (M1) + harden parity tests (L3)
MEDIUM-1: the editor<->authoring<->deploy invariant now holds on the ScriptedAlarm
surface too. CreateScriptedAlarmAsync/UpdateScriptedAlarmAsync validate {{equip}}/<RefName>
in BOTH the predicate script and the message template (via ExtractEquipReferenceNamesFromText),
matching what DraftValidator already enforces at deploy. Refactored ValidateEquipTokenAsync
into shared LoadEquipReferenceNamesAsync + CheckEquipReferencesResolve helpers.

LOW-1: LoadEquipReferenceNamesAsync uses IsNullOrEmpty (defense-in-depth) to match
EquipmentReferenceMap.Build; empty override is already normalized->null at the write path.

LOW-3: parity test hardened with two edge cases — unresolved-ref-left-intact (both seams
leave {{equip}}/X identical) and folder+tag-group RawPath ancestry.

Tests: VirtualTagEquipTokenValidationTests 9/9 (+3 SA), parity 4/4 (+2). AdminUI 659/0.

Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
2026-07-16 07:18:59 -04:00
Joseph Doherty 940303276c merge worktree-agent-ad3207d913b3b74e6 (B3-WP4) into v3/batch3-uns-rework 2026-07-16 06:59:22 -04:00
Joseph Doherty dc0d7653b9 feat(v3-batch3): B3-WP4 — {{equip}}/<RefName> reference-relative resolution
Replace the deleted equipment-base-prefix {{equip}} mechanism (impossible now
that equipment is reference-only) with per-reference resolution:
{{equip}}/<RefName> resolves through the owning equipment's UnsTagReference rows
(by effective name) to the backing raw tag's RawPath. Every unresolved <RefName>
is a clear deploy error + authoring rejection + Monaco diagnostic — preserving
"editor accepts <=> publish accepts".

Commons:
- EquipmentScriptPaths: delete DeriveEquipmentBase; SubstituteEquipmentToken now
  takes the equipment's reference map (effectiveName -> RawPath) and substitutes
  {{equip}}/<RefName> inside path literals (unresolved left intact, never throws);
  add ExtractEquipReferenceNames (path-literal scoped) +
  ExtractEquipReferenceNamesFromText (message templates). Slash syntax replaces
  the v2 dot joint.
- New EquipmentReferenceMap: shared, input-shape-agnostic builder (entity + JSON
  sides) over RawPathResolver — the single authority both compose seams + the
  validator use, so resolved RawPaths agree byte-for-byte.

Compose seams (byte-parity kept):
- AddressSpaceComposer.Compose + DeploymentArtifact.ParseComposition both build
  the per-equipment reference map from UnsTagReferences + Tags + raw topology and
  substitute VirtualTag AND ScriptedAlarm-predicate sources before dependency
  extraction (runtime dep refs = resolved RawPaths).

Deploy gate + authoring + editor:
- DraftValidator.ValidateEquipReferenceResolution: EquipReferenceUnresolved error
  for unresolved {{equip}}/<RefName> in VirtualTag/ScriptedAlarm scripts + alarm
  message-template tokens (naming script, equipment, missing ref).
- UnsTreeService.ValidateEquipTokenAsync (Wave-A M1): reference-relative authoring
  rejection, replacing the stale DeriveEquipmentBase(empty)->reject-all logic.
- ScriptAnalysisService: {{equip}}/ completion offers the equipment's reference
  effective names; DiagnoseAsync flags unresolved refs (OTSCRIPT_EQUIPREF) same as
  the deploy gate. Requests carry optional EquipmentId (threaded MonacoEditor ->
  monaco-init.js -> fetch bodies). IScriptTagCatalog.GetEquipmentRelativeLeavesAsync
  repurposed to GetEquipmentReferenceNamesAsync(equipmentId, filter).

Tests: EquipmentScriptPaths substitution/extraction (slash pinned); composer<->
artifact reference-resolution byte-parity; DraftValidator unresolved-ref;
ScriptAnalysis completion+diagnostic; M1 authoring gate. Build clean (0/0); all
five affected suites green.

Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
2026-07-16 06:58:17 -04:00
Joseph Doherty ac12eec924 review(wave-a L1): correct concurrency-catch comment (no DB uniqueness on effective name)
Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
2026-07-16 06:24:05 -04:00
Joseph Doherty ac8d4eef30 merge worktree-agent-aaae13cca624ecb2d into v3/batch3-uns-rework 2026-07-16 06:16:42 -04:00
Joseph Doherty 8c05a9fe0a merge worktree-agent-a2c703868462add4a into v3/batch3-uns-rework 2026-07-16 06:16:42 -04:00
Joseph Doherty 77bc010ba9 v3 Batch 3 WP1: UNS Equipment Tags tab → reference list + raw-tag picker
Replace the Batch-1-stubbed authored-tag flow with a UnsTagReference list on the
equipment page's Tags tab (v3 reference-only equipment): columns are effective name,
computed raw path, inherited datatype/access (read-only), display-name override
(editable), and remove. The retired equipment-authored-tag editor (TagModal.razor)
is deleted; the VirtualTag and ScriptedAlarm flows are untouched.

- "+ Add reference" opens a new AddReferenceModal that reuses RawTree in a new opt-in
  PickerMode (Tag-leaf checkboxes + Device/TagGroup "select all tags below"), scoped
  structurally to the equipment's cluster via LoadReferencePickerRootAsync (a single
  cluster root — cross-cluster tags are unreachable, not merely hidden). Default /raw
  usage is byte-unchanged (PickerMode defaults false).
- UnsTreeService (+ IUnsTreeService) gain the reference mutations:
  LoadReferencesForEquipmentAsync (computes each RawPath via the shared RawPathResolver),
  AddReferencesAsync (cluster-checked, all-or-nothing), RemoveReferenceAsync,
  SetReferenceOverrideAsync, plus the picker helpers LoadReferencePickerRootAsync /
  LoadDescendantTagIdsAsync.
- Consume IEffectiveNameGuard (constructor-injected, no-op fallback when unregistered):
  AddReferences, SetReferenceOverride, and the VirtualTag + ScriptedAlarm create/update
  mutations now call CheckAsync before persisting and surface a collision as the failure.
  WP2 supplies the implementation + DI registration.
- Drop the retired equipment↔driver binding: remove DriverInstanceId from EquipmentInput,
  the ImportEquipmentModal CSV column, the EquipmentPage Details driver select, and the
  dead decision-#122 driver-cluster guard.

Tests: new UnsTreeServiceReferenceTests (add/remove/override, cross-cluster + duplicate
rejection, guard-consumed rejection via a fake guard, RawPath projection, picker helpers);
Equipment/Import test suites rewritten to drop the retired driver-guard cases. AdminUI
suite green (639 passed, 3 skipped); full solution builds 0 errors.

Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
2026-07-16 06:15:51 -04:00
Joseph Doherty 09ff43910c feat(raw): B3-WP3 rename-warning script scan + historized-override refine
Extend RawTreeService.BuildRenameWarningsAsync so a tag/ancestor rename now
raises three warning kinds over the affected (prefix-scanned) tags:

(a) refined — historized WITHOUT a historianTagname override (default tagname
    is the moving RawPath, so history forks); a pinned (override) tag no longer
    warns. Count/message adjusted.
(b) unchanged — UNS-referenced (names the equipment).
(c) NEW — substring-scan every Script body for the affected tag's OLD RawPath
    (ordinal, false-positives tolerated by design; no AST). OLD RawPaths are
    recomputed from the pre-save in-DB topology via RawPathResolver.

CollectTagsBeneath* + TagsForDevices now carry (DeviceId, TagGroupId, Name) so
the OLD RawPath can be rebuilt. Contained entirely within RawTreeService.cs;
IRawTreeService + RawTree.razor untouched (warnings flow through the existing
RawRenameResult.Warnings). Note: a direct tag rename goes through UpdateTagAsync
(UnsMutationResult, no warning surface) so it is out of scope without widening
the interface.

Tests: new RawTreeServiceRenameWarningTests (4) — all-three, pinned-no-warn,
unrelated-empty, ancestor-rename script match. AdminUI.Tests 642 green.

Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
2026-07-16 06:04:06 -04:00
Joseph Doherty b68c313372 feat(uns-v3): implement EffectiveNameGuard authoring-time uniqueness guard (B3-WP2)
Implements IEffectiveNameGuard (the Wave-A contract): per-equipment effective-name
uniqueness across UnsTagReferences (DisplayNameOverride else backing raw tag Name),
VirtualTags (Name), and ScriptedAlarms (Name). Ordinal comparison, self-row exclusion
by (kind, id), one pooled context per CheckAsync call. Registered Scoped in
EndpointRouteBuilderExtensions so WP1's UnsTreeService consumer goes live.

Verified the deploy-time DraftValidator UnsEffectiveNameCollision rule already
computes reference effective names from the backing raw tag's current Name (catches
rename-induced collisions), spans all three sources, compares ordinal, and names both
colliding sources + the equipment — no hardening needed. DraftSnapshotFactory already
loads Tags/UnsTagReferences/VirtualTags/ScriptedAlarms.

Tests: 8 guard tests (in-memory EF) + 3 deploy-gate tests (rename-induced collision,
override-vs-VT, ordinal case-sensitivity). All green.

Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
2026-07-16 06:01:10 -04:00
Joseph Doherty 6dc5af7aa2 v3 Batch 3 contracts: IEffectiveNameGuard authoring-time uniqueness seam
Wave-A shared contract. WP2 implements + registers the guard; WP1 consumes it
in UnsTreeService reference/VirtualTag/ScriptedAlarm mutations. Ordinal, mirrors
the deploy-time DraftValidator UnsEffectiveNameCollision rule.

Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
2026-07-16 05:53:52 -04:00
Joseph Doherty 96471d2345 refactor(adminui): B2-WP8 DriverTypeNames rewire + retire routed driver-authoring flow
Part 1 — rewire the three DriverType dispatch maps from hand-authored string
literals to the canonical DriverTypeNames constants (single source of truth):
- TagConfigEditorMap, TagConfigValidator (AdminUI)
- EquipmentTagConfigInspector (ControlPlane)
This FIXES the live drift ("TwinCat"->TwinCAT, "Focas"->FOCAS). The maps are
OrdinalIgnoreCase so behavior is identical; the value is no future drift.
Calculation is not added to the inspector (no CalculationTagDefinitionFactory.Inspect).
Added TagConfigDriverTypeNameGuardTests pinning the maps to the constants.

Part 2 — retire the routed /clusters/{id}/drivers driver-authoring flow (/raw
Waves A-C now cover driver/device/tag authoring end-to-end):
- Deleted DriverTypePicker, DriverEditRouter, the 8 *DriverPage shells, and the
  ClusterDrivers list page.
- Removed the "Drivers" tab from ClusterNav. Routes /clusters/{id}/drivers* now
  simply do not exist -> clean 404.
- The Forms/DeviceForms bodies, shared driver sections, and address pickers used
  by the /raw modals are untouched.

Part 3 — updated PageAuthorizationGuardTests census: removed the 10 deleted
ConfigEditor pages + ClusterDrivers, updated group-count comments (ConfigEditor
21->11, AuthenticatedRead 17->16). Removed the now-dead Clusters.Drivers usings
from the 4 *DriverPageFormSerializationTests + the guard test.

Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
2026-07-16 04:39:22 -04:00
Joseph Doherty 1f43449942 feat(adminui): B2-WP6 browse re-target — /raw device browse commits raw tags
Browse device… on a /raw Device/TagGroup opens the discovery-browser modal
against the merged Driver+Device config and commits selected browse leaves as
raw Tag rows under the target, via IRawTreeService.ImportTagsAsync.

- RawBrowseModal.razor: two-tier browse gate (bespoke IDriverBrowser →
  universal DiscoveryDriverBrowser via SupportsOnlineDiscovery → disabled),
  merged config from LoadMergedProbeConfigAsync, multi-select over
  DriverBrowseTree, opt-in 'create matching tag-groups' folder mirror,
  commit via ImportTagsAsync.
- RawBrowseCommitMapper: pure leaf→RawTagImportRow mapper — DriverDataType→
  OPC UA type, per-driver address field (nodeId/tagPath/symbolPath/address/
  attributeRef), target-group + mirror path combine. Unit-tested (39 tests).
- DriverBrowseTree: additive multi-select mode (checkboxes + ancestor-path
  callback), default off preserves single-select pickers.
- RawTree: OnBrowseDevice wired to the modal (Device + TagGroup menus).

Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
2026-07-16 04:14:27 -04:00
Joseph Doherty f986519317 merge wt/wp5 into v3/batch2-raw-ui (Wave B UI) 2026-07-16 03:27:23 -04:00
Joseph Doherty 59ea02d971 feat(adminui): B2-WP5 CSV tag import/export + review grid
Staged CSV tag import (upload -> parse -> review grid with per-row verdicts
-> all-or-nothing commit) and export for the /raw tree Device/TagGroup.

- CsvColumnMap: per-driver typed column <-> TagConfig JSON maps (model-backed
  for the 7 typed drivers, raw-key for Galaxy/Calculation) + the common column
  dictionary; typed columns merge OVER the TagConfigJson fallback (typed wins).
- RawTagCsvMapper: pure parse (review rows + verdicts + typed/fallback
  provenance + intra-batch dup detection) and export (residual TagConfigJson)
  over the T0-2 CsvParser/CsvWriter.
- RawTagCsvExportReader: DbContext-backed read helper (no IRawTreeService
  extension) enumerating a device's tags + reconstructing group paths.
- RawCsvImportModal + RawCsvExportModal (data-URI download).
- Reflection guard test (typed column -> real model property + batch-plan
  dictionary parity) + export->import round-trip property test.

Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
2026-07-16 03:19:14 -04:00
Joseph Doherty ada552fbec feat(adminui): B2-WP4 manual tag entry + raw tag modal + Calculation editor
Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
2026-07-16 03:08:05 -04:00
Joseph Doherty 76cffe1f49 feat(adminui): B2 Wave-B service prelude — tag CRUD/import + driver/device config + merged probe
Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
2026-07-16 02:58:13 -04:00
Joseph Doherty 928e06dd01 review(b2-waveA): /raw auto-expand enterprise roots; friendly create-race + delete failures; auth-guard classifies /raw + dev demo
Wave-A reviewer findings (no Critical/High):
- M-2: GlobalRaw auto-expands Enterprise roots so the Cluster level is visible on load (mirrors GlobalUns.ExpandStructural); clusters stay lazily collapsed.
- L-1: create mutations route SaveChanges through SaveCreateAsync — a lost sibling-name race (filtered-unique-index clash) becomes a friendly failure instead of an uncaught DbUpdateException.
- L-2: SaveDeleteAsync no longer leaks raw EF/SQL text; operator-friendly guidance.
- L-5: RawNode.ChildCount doc clarified (direct children only).
- Regression fix: PageAuthorizationGuardTests classifies the two new routable pages (/raw → ConfigEditor, /dev/context-menu-demo → AuthenticatedRead).
M-1 (ordinal vs SQL-collation sibling uniqueness) assessed benign: name indexes use the DB default collation and the server-side pre-check runs under that same collation, so pre-check and index agree — no case-variant rows can coexist, so runtime ordinal RawPath keying never collides.

Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
2026-07-16 02:45:06 -04:00
Joseph Doherty 76b8325b84 feat(adminui): B2-WP1 RawTreeService + lazy tree data layer + mutations
Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
2026-07-16 02:34:36 -04:00
Joseph Doherty fbf3c26c2a contracts(b2-waveA): RawNode view-model + IRawTreeService read surface + RawRenameResult
Wave A contract-first fan-out types for the /raw project tree:
- RawNode/RawNodeKind: shared tree view-model (Enterprise→Cluster→Folder→Driver→Device→TagGroup→Tag), lazy-load metadata, per-kind ids + DriverType propagation for menu gating.
- IRawTreeService: committed READ surface (LoadRootsAsync + LoadChildrenAsync); WP1 extends with the mutation surface, WP2 consumes read-only.
- RawRenameResult: rename outcome carrying non-blocking warnings (historized/UNS-referenced now; script-scan in Batch 3).

Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
2026-07-16 02:20:05 -04:00
Joseph Doherty 11576460a7 v3(b1-wp4b): make AdminUI compile against greenfield schema behind Batch-2/3 stubs
Namespace entity + NamespaceKind retired; DriverInstance dropped NamespaceId;
Tag is raw-only (no EquipmentId/DriverInstanceId/FullName/FolderPath); driver
options replaced pre-declared Tags with RawTags. Stub the retired surfaces so
the AdminUI + Host build green; real Raw/UNS authoring arrives in v3 Batch 2/3.

- ClusterNamespaces/NamespaceEdit: bodies replaced with 'Namespaces retired' banner
- DriverIdentitySection: dropped Namespace dropdown + NamespaceId model field
- 8 driver pages: dropped Namespace binding; retired the pre-declared Tags editor
  (replaced with a '/raw Batch 2' note); multi-device pages keep their Devices editor
- UnsTreeService: equipment-tag counts/list -> empty; Create/UpdateTag -> failure
  result ('Tag authoring moved to the Raw tree (/raw) in v3 Batch 2'); Namespace/
  driver-binding queries removed
- IScriptTagCatalog: project surviving Tag columns only (path from TagConfig FullName, else Name)
- OpcUaClientTagConfigEditor: rebind stale FullName -> model's NodeId
- ClusterDrivers: drop retired NamespaceId column

AdminUI + Host build 0/0. AdminUI.Tests / Host.IntegrationTests remain red (WP6).
2026-07-15 21:09:04 -04:00
Joseph Doherty d4dab75323 v3 B1-WP2: TagConfigIntent sheds FullName identity; RawPaths tests; nodeId key normalization 2026-07-15 18:58:57 -04:00
Joseph Doherty 4449baac69 feat(adminui): writable checkbox in driver-typed tag editors (R2-11, 05/UNDER-6)
Models gain an optional bool? Writable (absent stays omitted; explicit false round-trips,
unknown keys preserved); the six editors gain a Writable checkbox (FOCAS disabled+read-only
hint). 9 model round-trip tests green. docker-dev /run verify of one editor deferred-live.
2026-07-13 11:22:12 -04:00
Joseph Doherty 4b86beef28 refactor(core): EquipmentNodeWalker.ExtractFullName delegates to TagConfigIntent; repoint canonical-parser doc cites (R2-11)
Full walker deletion remains round-1 U-1's task.
2026-07-13 10:50:50 -04:00
Joseph Doherty 9cad9ed0fc docs(xmldoc): fill missing XML docs + strip tracking-ID comments across src
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>/<param>/<returns>/<inheritdoc> where missing and removes
project bookkeeping IDs (task/tracking refs) from shipped code comments,
so the docs read cleanly and CommentChecker is quiet except for known
false positives (PLC/protocol terms, event/IEqualityComparer inheritdoc).
Doc/comment-only; no logic changed; solution builds clean.
2026-07-07 12:38:39 -04:00
Joseph Doherty 0b4b2e4cfd refactor(historian-gateway): retire Wonderware historian projects (gateway is sole backend)
The HistorianGateway driver is now the sole historian read/write+alarm backend, so the
Wonderware sidecar projects are dead code. Removes the 5 Wonderware projects (driver,
.Client, .Client.Contracts, + their 2 test projects) from the solution and tree, and fully
retires the vestigial 'Historian.Wonderware' driver type (UI/probe-only; it had no driver
factory): the Host probe registration, the AdminUI driver-config surface (driver page,
tag-config editor/model/validator entry, address picker/builder, driver-type catalog +
dropdown + edit-router entries), and their tests. Prunes the now-unused Wonderware
connection fields (Host/Port/UseTls/ServerCertThumbprint/SharedSecret) from
AlarmHistorianOptions (keeping Enabled + the SQLite store-and-forward knobs) and refreshes
the stale XML docs that named Wonderware as the production backend.

Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
2026-06-26 19:25:21 -04:00
Joseph Doherty 3c908f1df0 review(AdminUI): fix null-TagConfig crash, CTS leak, unencoded historian tag
Review at HEAD 7286d320. AdminUI-002: IsValidJson null/blank -> friendly error (was
ArgumentNullException). AdminUI-003: DriverStatusPanel Reconnect/Restart dispose CTS (build-
verified, live /run deferred). AdminUI-005: HistorianWonderware picker URL-encodes tag name.
AdminUI-008: Format round-trip test. 001 (script-page authz) + 004 (hub [Authorize]) left
Open as cross-cutting w/ Host/Security.
2026-06-19 10:52:23 -04:00
Joseph Doherty e047af0553 refactor(adminui): harden SetFullName (reject blank ref) + cover alarm-flag re-pick
Code-review nits: SetFullName now throws on a blank reference (was silently
persisting FullName:null → silent deploy-time bind failure), and a new test
covers the alarm-typed re-pick combo (SeedDefaultAlarm over an already-edited
alarm leaves it intact).
2026-06-19 02:08:43 -04:00
Joseph Doherty 2dd723e195 fix(adminui): preserve edited alarm fields on Galaxy address re-pick 2026-06-19 02:02:06 -04:00
Joseph Doherty 40e8a23e7c refactor(opcuaserver): rename Phase7* address-space pipeline to AddressSpace*
v2-ci / build (push) Failing after 37s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
The OPC UA address-space build pipeline was named after a v2-roadmap
milestone number rather than its domain. Rename the family to describe
what it does (build/diff/apply the OPC UA address space):

  Phase7Composer          -> AddressSpaceComposer
  Phase7CompositionResult -> AddressSpaceComposition
  Phase7Planner           -> AddressSpacePlanner
  Phase7Plan              -> AddressSpacePlan
  Phase7Applier           -> AddressSpaceApplier
  Phase7ApplyOutcome      -> AddressSpaceApplyOutcome

The 9 Phase7*Tests suites follow suit; Phase7ScriptingEntitiesTests ->
ScriptingEntitiesTests (it tests the scripting migration, not the
pipeline). Log-message prefixes move to the new class names.

Pure mechanical rename, no behavioral change. EF migration classes/IDs
(AddPhase7ScriptingTables, ExtendComputeGenerationDiffWithPhase7) are
immutable and left untouched, as are historical design docs.

Build clean; OpcUaServer 261/261, Runtime 272/272, ScriptingEntities
12/12 green.
2026-06-18 19:16:28 -04:00
Joseph Doherty 0f92e9e238 fix(adminui): reject AbLegacy array length >256 at author-time (review I-3)
v2-ci / build (push) Failing after 1m3s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
2026-06-16 22:31:42 -04:00
Joseph Doherty c2006dfb57 feat(adminui): driver-agnostic isArray/arrayLength Tag-modal control 2026-06-16 21:50:27 -04:00
Joseph Doherty 8b4675b1a5 fix(adminui): canonicalize Modbus driver-type string on "Modbus" (was ModbusTcp) 2026-06-16 19:39:41 -04:00
Joseph Doherty 069a5f3165 feat(adminui): Galaxy picker pre-fills native-alarm fields from IsAlarm 2026-06-16 16:55:05 -04:00
Joseph Doherty c00a547191 feat(adminui): isHistorized + historianTagname as first-class Tag fields 2026-06-16 16:44:46 -04:00
Joseph Doherty c98625fd9f feat(adminui): create-new-script from the inline virtual-tag panel 2026-06-16 16:44:14 -04:00
Joseph Doherty 526eebb3bb feat(adminui): UNS-tree delete for Cluster + Enterprise (refuse-if-children, no rowversion) 2026-06-16 16:35:07 -04:00
Joseph Doherty 6a8020e7e7 feat(adminui): native-alarm HistorizeToAveva opt-out 2026-06-16 16:27:31 -04:00
Joseph Doherty 72d414ada7 feat(adminui): typed TagConfig editors for OpcUaClient + Historian 2026-06-16 16:25:19 -04:00
Joseph Doherty 0945f19a50 feat(adminui): wire Galaxy live-browse picker into the standard TagModal 2026-06-12 22:09:22 -04:00
Joseph Doherty 5dfb797817 refactor(adminui): strip alias/relay machinery from UnsTreeService + EquipmentPage; Galaxy tags use standard TagModal 2026-06-12 21:28:13 -04:00
Joseph Doherty ca2698949b refactor(adminui): delete alias-tag/relay-converter files (build red until spine stripped) 2026-06-12 21:20:41 -04:00
Joseph Doherty d19271fff8 fix(adminui): converter skips name-collisions + disabled relays (review) 2026-06-11 21:44:19 -04:00