Replace raw-JSON text inputs with rich UI: script parameter/return types use
a JSON Schema builder (SchemaBuilder + JsonSchemaShapeParser, with a migration
to convert existing definitions); alarm trigger config uses a type-aware
editor with a flattened attribute picker (AlarmTriggerEditor). AlarmActor
gains optional direction (rising/falling/either) on RateOfChange triggers.
Phases 3+4 of the script-scope rollout. Wires the runtime accessors
landed in efba01d through to Monaco completion, diagnostics, and
hover.
New analyzer surface in ScriptAnalysisService:
String-literal completion contexts (added to TryStringLiteralCompletions):
Attributes["..."] -> SelfAttributes
Children["..."] -> composition names
Children["X"].Attributes["..."] -> child template's attributes
Children["X"].CallScript("...") -> child template's scripts
Parent.Attributes["..."] -> parent template's attributes
Parent.CallScript("...") -> parent template's scripts
Diagnostics:
SCADA006 Attribute "Typo" is not declared on {this template,
child composition 'X', the parent}. (warning)
SCADA007 Composition "Unknown" is not declared on this template.
(warning)
CallShared / CallScript snippet-expansion now routes through the
child / parent shape catalogs when invoked on Children["X"] /
Parent — picking a child script accepts `Sample", ${1:count})`.
Contract additions:
- AttributeShape (Name, Type) record
- CompositionContext (Name, Attributes, Scripts) record
- SelfAttributes / Children / Parent fields on DiagnoseRequest,
CompletionsRequest, HoverRequest, SignatureHelpRequest
ScriptHost (analyzer-side globals) gains stub AttributeBag /
ChildrenBag / CompositionBag types so Roslyn doesn't emit CS0103 on
Attributes / Children / Parent. The stubs are never invoked — only
their signatures are read by the analyzer's compilation pass.
MonacoEditor.razor exposes SelfAttributes / Children / Parent
parameters; GetContext returns them; monaco-init.js forwards all
three on completion / hover / signature-help / diagnostics requests.
TemplateEdit fetches each composition's resolved child template
shape via GetTemplateWithChildrenAsync, and queries GetAllTemplatesAsync
for any single parent that composes the open template. Multi-parent
or no-parent → Parent is suppressed.
11 new xUnit tests on the new completion / diagnostic paths. Total:
149 -> 159.
Browser-verified via curl:
- Children["..."] suggests composition names
- Attributes["..."] suggests attributes with type detail
- Attributes["Typo"] squiggles SCADA006
- Children["Unknown"] squiggles SCADA007
- No spurious CS0103 on the new accessors
Hover, signature help, and inlay hints for the new accessors keep
working because they reuse the same dispatch logic.
Three more editor features rolled in:
1. Roslyn Format command.
New POST /api/script-analysis/format runs Formatter.Format() from
Microsoft.CodeAnalysis.CSharp.Workspaces on the parsed script
tree. monaco-init.js registers a DocumentFormattingEditProvider
so Ctrl/Cmd-Shift-F and the toolbar "Format" button both work.
2. Inlay hints with parameter names.
New POST /api/script-analysis/inlay-hints walks CallShared /
CallScript invocations and emits InlayHint records positioned at
each argument with the matching parameter's name (e.g. "name:").
Ghost text appears via Monaco's InlayHintsProvider.
3. SCADA005 argument-type diagnostic.
Literal type vs. declared parameter type check on every
CallShared/CallScript argument. Float accepts Integer literals;
Object/List accept anything; null only matches reference-ish
types. Legacy lowercase types ("string" etc) from the DB are
normalized to the canonical set before comparison so existing
data doesn't false-negative. Non-literal args (variables,
expressions) are skipped — out of scope for a cheap pass.
4. Parameters["name"] hover.
Hover endpoint now also resolves Parameters["X"] element-access
keys against the form's DeclaredParameterShapes and returns
"parameter `name: String`"-style markdown. MonacoEditor surfaces
the new DeclaredParameterShapes parameter; ScriptParameterNames
gets a ParseShapes companion.
5. Problems panel.
Bootstrap card under the editor listing every marker with
severity badge, line number, message, and SCADA / CS code. Click
a row to scroll the editor to that line and focus. JS now
invokes OnMarkersChanged on the .NET side whenever
setModelMarkers fires, so the panel stays in sync with the
editor.
6. Editor toolbar.
Small top-right strip on each editor with Format / Wrap /
Minimap / Theme toggles. New MonacoBlazor.format,
setEditorOption, and revealLine JS APIs back the buttons and the
problems-panel scroll-to-line.
Contracts:
- FormatRequest / FormatResponse
- InlayHintsRequest / InlayHintsResponse / InlayHint
- HoverRequest.DeclaredParameters
- MonacoEditor.DeclaredParameterShapes parameter
- MonacoEditor.MarkersChanged callback
- ScadaContext.DeclaredParameterShapes
10 new xUnit tests covering format, inlay hints, SCADA005 (string-
expects-integer, integer-expects-string, float-accepts-integer,
object-accepts-anything, non-literal-skipped), and Parameters key
hover. Total: 139 -> 149.
Microsoft.CodeAnalysis.CSharp.Workspaces 4.13.0 added to pull in
Formatter and AdhocWorkspace.
Browser-verified: typing `CallShared("Greet", 42)` now shows the
"name:" inlay hint and a SCADA005 squiggle on `42`; Parameters["typo"]
shows SCADA003 as before; the toolbar buttons all work.
Now that the form holds parameter + return shapes for declared
parameters, sibling scripts (template Scripts tab), and shared
scripts (via SharedScriptCatalog), the editor leverages them four
ways:
1. Snippet expansion on accept.
Picking a CallShared or CallScript completion inserts the full
call template with tabstops, e.g. `Greet", ${1:name})`. The JS
provider extends the completion range over Monaco's auto-closed
`")` so the snippet replaces the closing pair cleanly. Items
carry insertTextRules=4 (InsertAsSnippet) and a command to
immediately trigger parameter hints after acceptance.
2. Hover info.
Hovering the script name token inside CallShared("X") or
CallScript("Y") shows a markdown tooltip with the call signature
and return type. New endpoint POST /api/script-analysis/hover.
3. Signature help.
Inside CallShared(...) / CallScript(...) Monaco shows the
parameter strip with the active parameter highlighted. The
service walks up from the cursor to the nearest enclosing
InvocationExpression and resolves which argument index the
cursor is on. New endpoint POST /api/script-analysis/signature-help.
4. Argument-count diagnostic (SCADA004) and unknown-Parameters-key
diagnostic (SCADA003). The Diagnose pipeline now consults the
declared parameters and sibling/shared shapes to flag:
- Parameters["typo"] when "typo" isn't on the form (warn)
- CallScript("Calc", 1) when Calc declares 2 required args (err)
- CallShared("Greet", 1, 2, 3) when Greet declares 1 arg (err)
Optional parameters relax the required-count bound.
Contract changes:
- ScriptShape / ParameterShape records
- ISharedScriptCatalog.GetShapesAsync (replaces GetNamesAsync)
- new HoverRequest/Response, SignatureHelpRequest/Response
- CompletionsRequest.SiblingScripts: string[] -> ScriptShape[]
- DiagnoseRequest gains DeclaredParameters + SiblingScripts
- CompletionItem gains InsertTextRules (Monaco snippet rule)
Form wiring:
- TemplateEdit passes ScriptShapeParser.Parse(...) per sibling
- MonacoEditor surfaces SiblingScripts: IReadOnlyList<ScriptShape>
- GetContext returns shapes to JS on each completion/hover/sig
request
10 new ScriptAnalysisServiceTests covering all four features plus
optional-parameter edge cases. Existing tests updated for the
contract changes. Total: 113 -> 139.
Browser-verified via direct curl + Monaco marker readback:
- SCADA003 squiggle on Parameters["typo"]
- Snippet item Greet", ${1:name}) with insertTextRules=4
- Hover markdown shape signature
- Signature help parameter strip
Two pre-flagged follow-ups from the Monaco integration:
1. IMemoryCache for diagnostics keyed by SHA256 of the script body.
Same-code Diagnose() now short-circuits the Roslyn compile and
forbidden-API walk. SizeLimit 200 entries with 5-minute sliding
expiration. Completions aren't cached — position + form context
vary too much for a useful hit rate.
2. Forbidden-API analyzer now resolves identifiers through the
SemanticModel instead of matching names. A user identifier
named File / Thread / Process / etc. no longer false-positives
— only references that resolve to a NamedTypeSymbol whose
containing namespace is on the banned list are flagged. The
diagnostic message now names the offending namespace, e.g.
"Type 'File' from forbidden namespace 'System.IO' is not
allowed in scripts."
Refactor: extracted ISharedScriptCatalog so ScriptAnalysisService
can be unit-tested without standing up SharedScriptService's EF
chain. Concrete SharedScriptCatalog wraps the existing service.
16 new xUnit tests in ScriptAnalysisServiceTests:
- Empty / clean / missing-semicolon paths
- SCADA001 on each banned using namespace (theory)
- SCADA002 on real File.ReadAllText through System.IO
- No-false-positive checks for user-defined File / Thread locals
- Cache returns the same response instance on repeat
- Different code → different cache entries
- String-literal completions for Parameters / CallScript / CallShared
- General completion at file scope returns ScriptHost members
Total CentralUI test count: 113 -> 129.
Editors now set a _normalized flag when ParseFromJson coalesces a
legacy type name (lowercase "string", "Int32", "Double", etc.) to the
canonical set. When flagged, render a small alert-info inline:
"Some parameter types were normalized... Save to persist the
canonical form." The flag clears on any user edit so the notice
doesn't linger after Emit overwrites the JSON.
31 new bUnit tests in tests/.../Shared/:
- ParameterListEditorTests: null/empty rendering, row count per
JSON entry, legacy type normalization across .NET names +
lowercase, the normalized notice trigger, add/remove emission,
List/non-List item-type column visibility, required-flag round
trip, invalid JSON + non-array error paths.
- ReturnTypeEditorTests: null vs simple vs List shape, legacy type
normalization, change-type / clear-type emission, invalid JSON
+ non-object error paths.
Total CentralUI test count: 82 -> 113.
Eliminates the per-page <ConfirmDialog @ref="_confirmDialog"
ConfirmButtonClass="btn-danger" /> boilerplate. Pages now inject
IDialogService and call ConfirmAsync(title, message, danger: true)
programmatically.
New scoped service holds a single active dialog (throws on nested
calls), with a global DialogHost mounted once in MainLayout that
renders the modal markup, owns body scroll-lock via Bootstrap's
modal-open class, traps focus on the modal element, and handles
Escape-to-cancel.
Same service also exposes PromptAsync, used to replace the bespoke
NewFolderDialog. Both ConfirmDialog and NewFolderDialog components
are deleted — their callers (~13 pages across Admin/Design/Deployment
/Monitoring) now go through the service.
DiffDialog stays as-is — different use case (before/after content).
bUnit tests in TopologyPageTests, DataConnectionsPageTests, and
TemplatesPageTests register IDialogService in their service
collection.
Also: a top-of-file Razor comment on Sites.razor pointing future
implementers at it as the reference list-page pattern.
LdapMappings: flex header, search filter, per-row Edit + kebab Delete,
@key, dropped Site-Scope-Rules cell in favor of a {n rule(s)} badge.
LdapMappingForm: two stacked cards (Mapping then Site Scope Rules);
scope rules render as removable chips with an inline "Add scope rule"
form; create-mode disables the scope card with an explainer; role
select gets form-text help.
DataConnections: <h4> in flex header, Bulk actions dropdown holding
Expand/Collapse, hover-visible kebab on tree nodes mirroring the
right-click context menu, aria-labels, "No connections match the
filter." inline empty state.
DataConnectionForm: Site rendered as readonly plaintext + lock-after-
creation note in edit mode; parallel Primary endpoint / Backup endpoint
headings; "Optional" badge on Backup when null; form-text on
FailoverRetryCount.
ApiKeys: search filter, Status column dropped (state now lives in the
kebab menu label "Disable"/"Enable"), Edit + kebab actions, @key,
aria-labels.
ApiKeyForm: nested card removed; fixed-text Back header; real
clipboard copy via IJSRuntime + toast confirmation.
Test selector fix in DataConnectionFormTests for the new Site
readonly-plaintext rendering.
Three new sections inserted into <OpcUaEndpointEditor>:
1. Authentication (between the existing Connection row and Timing)
- 'Enable Authentication' button when Config.UserIdentity is null
- TokenType select (Anonymous / UsernamePassword / X509Certificate)
- Conditional Username + Password inputs for UsernamePassword
- Conditional Certificate path + Certificate password for X509Certificate
- 'Remove Authentication' button
2. Advanced subscription (after the existing Subscription row)
- Subscription display name (text)
- Subscription priority (number 0-255)
- Timestamps to return (Source / Server / Both select)
- Discard oldest (checkbox)
3. Deadband filter (after Advanced subscription)
- 'Enable Deadband' button when Config.Deadband is null
- Type select (Absolute / Percent), Value number input
- 'Remove Deadband' button
EnableAuthentication and EnableDeadband helpers complement EnableHeartbeat.
All new fields use the existing RenderFieldError helper for validator errors.
82/82 CentralUI tests pass (the 10 new editor tests drove the design).
Brings the Data Connections admin page up to the same UX standard as the
Topology page:
- Search box with dim non-matches (opacity 0.4, shape preserved)
- Toolbar: + Connection (disabled until a site is selected), Refresh,
Expand, Collapse
- Site context menu gains "Add Connection here" that navigates with
?siteId= so the form preselects + locks the Site field
- Form gains "Primary Endpoint" / "Backup Endpoint" h6 subsection
headers matching the SiteForm convention; Failover Retry Count moved
inside the Backup subsection
- URL renamed: /admin/connections (primary) + /admin/data-connections
(legacy secondary @page). Same dual-route treatment on the form
- Nav label: "Data Connections" -> "Connections"
- Adds DataConnectionsPageTests bUnit suite (6 tests)
Single /deployment/topology page replaces /deployment/instances (legacy URL
preserved as a secondary @page directive) and the /admin/areas* CRUD pages.
TreeView with Site → Area → Instance, V1–V7 visual guide (bi-building /
bi-diagram-3 / bi-box), always-visible empty containers, search dim, F2
inline area rename, and right-click context menus per node kind (Add Area,
Move to Area…, lifecycle actions, etc.).
Adds AreaService.MoveAreaAsync with cycle prevention, same-site enforcement,
and name-collision check at the new parent. Instance rename intentionally
out of scope — UniqueName is the site-side actor identity, requires its own
design pass.
Right-click a template now offers "New Derived Template" — opens
TemplateCreate with the parent pre-selected via a new ?parentId query
parameter. Composition rows in the tree drop the trailing
"→ TargetName" muted text; the kind glyph plus the instance name carry
enough meaning, and the composed template is one click away from the
row's right-click menu.
Templates page is now a tree-only browser; editing happens on a dedicated
TemplateEdit page. Drag-drop is replaced by context-menu Move-to-Folder.
TreeView gains Bootstrap Icons (chevron + per-kind glyphs), ancestor guide
lines, defined hover/selected/focus tokens, and Escape-dismisses-menu per
the new Visual Design Guide (V1-V7) in Component-TreeView.md.
- Add JoeAppEngine folder to OPC UA nodes.json (BTCS, AlarmCntsBySeverity, Scheduler/ScanTime)
- Fix DataConnectionActor: capture Self in PreStart for use from non-actor threads,
preventing Self.Tell failure in Disconnected event handler
- Implement InstanceActor.HandleConnectionQualityChanged to mark attributes Bad on disconnect
- Fix LmxFakeProxy TagMapper to serialize arrays as JSON instead of "System.Int32[]"
- Allow DataType and DataSourceReference updates in TemplateService.UpdateAttributeAsync
- Update test_infra_opcua.md with JoeAppEngine documentation
17 source projects (Commons + Host + 15 components) and 17 xUnit test projects.
SLNX format, net10.0, nullable enabled, warnings as errors. All components
reference Commons; Host references all components. Builds and tests clean.