Commit Graph

16 Commits

Author SHA1 Message Date
Joseph Doherty
751248feb6 feat(alarms): HiLo trigger type with per-band level, hysteresis, messages, overrides
Adds a new HiLo alarm trigger type with four configurable setpoints
(LoLo / Lo / Hi / HiHi). Each setpoint carries an optional priority,
deadband (for hysteresis), and operator message. The site runtime emits
AlarmStateChanged with an AlarmLevel field so consumers can differentiate
warning vs critical bands.

Plumbing:
  - new AlarmLevel enum + AlarmStateChanged.Level/Message init properties
  - AlarmTriggerEditor (Blazor) gets a HiLo render with severity tinting
  - AlarmTriggerConfigCodec extracted from the editor for testability
  - sitestream.proto carries level + message over gRPC
  - SemanticValidator enforces numeric attribute, setpoint ordering,
    non-negative deadband
  - on-trigger scripts get an Alarm global (Name/Level/Priority/Message)
    so notification routing can branch by severity
  - per-instance InstanceAlarmOverride entity + EF migration + flattening
    step + CLI commands; HiLo overrides merge setpoint-by-setpoint, binary
    types whole-replace
  - DebugView shows a Level badge + per-band message tooltip
  - App.razor auto-reloads on permanent Blazor circuit failure
  - docker/regen-proto.sh automates the proto regen workflow (the linux/arm64
    protoc segfault means generated files are checked in for now)
2026-05-13 03:23:32 -04:00
Joseph Doherty
783da8e21a feat(ui): structured editors for script schemas and alarm triggers
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.
2026-05-13 00:33:00 -04:00
Joseph Doherty
5c3dc79b8a feat(templates/ui): manage compositions from the tree
Move composition CRUD off the TemplateEdit page and onto the tree
context menu, matching Aveva's Template Toolbox flow.

- New ComposeIntoDialog: pick a parent template, slot name (defaults
  to the source template's name).
- "Compose into…" on every base template's context menu (kebab + right
  click) opens the dialog and calls AddCompositionAsync.
- "Rename…" on composition leaves opens a prompt and calls
  TemplateService.RenameCompositionAsync. The owning composition row
  AND its owned derived template are renamed atomically; duplicate
  slot names or derived-name collisions abort with a clear error.
- "Delete" on composition leaves confirms + cascade-deletes the
  composition (and its derived template via DeleteCompositionAsync).
- "New Derived Template" menu item renamed to "New Inheriting Template"
  to disambiguate from the new derive-on-compose meaning.

TemplateEdit's Compositions tab, Add Composition form, and
Add/DeleteComposition handlers + state fields are deleted — the tree
is now the single source of truth.
2026-05-12 09:22:55 -04:00
Joseph Doherty
a965d4a5bd feat(templates/ui): phase 9 — single-parent editor context
Derive-on-compose guarantees at most one slot owner per template, so the
Parent.* context in the Monaco editor resolves directly via
OwnerCompositionId without a picker. Base templates suppress Parent.*
assistance entirely (empty context).

Removed the multi-parent <select> dropdown from the Add Script form and
the now-redundant _selectedParentIndex / OnParentContextChanged plumbing.
ActiveEditorParent collapses to _editorParents.FirstOrDefault().
2026-05-12 08:57:42 -04:00
Joseph Doherty
f05b03f1cc feat(templates/ui): phase 6-8 — derived template UX
Templates tree hides IsDerived templates by default. A "Show derived"
form-switch in the page header toggles them into the listing so users
can reach orphaned derived templates when they need to.

TemplateEdit:
- Banner on derived templates: links to the base + the composing owner /
  slot name pulled from OwnerCompositionId.
- Attributes/Scripts tables grew a context-aware column:
  * On derived templates: a Source badge (Inherited / Override / Local)
    plus a 🔒 Base-locked badge when the base marks LockedInDerived.
  * On base templates: a switch that flips LockedInDerived through
    UpdateAttribute/UpdateScript.
- Effective Value / Code now resolves from the base when an inherited row
  carries a stale snapshot — matches the flatten-time behavior so the UI
  doesn't lie.
- Override / Revert-to-base actions added to the row kebab; delete is
  hidden on inherited rows (the base owns those).
2026-05-12 08:55:20 -04:00
Joseph Doherty
0139c9ca83 refactor(scripts): scoped parent query + parent picker for multi-parent templates
Two caveats from the script-scope rollout addressed:

1. ITemplateEngineRepository.GetTemplatesComposingAsync — a scoped
   query that returns only the templates referencing a given template
   via Compositions, eager-loaded with their Attributes / Scripts /
   Compositions. Replaces the GetAllTemplatesAsync + filter pattern
   in TemplateEdit so the Monaco metadata fetch doesn't pull the
   entire template catalog to find one parent.

2. Multi-parent picker. The previous implementation suppressed Parent
   assistance entirely when more than one template composes the open
   one. Now TemplateEdit collects every parent into _editorParents
   and renders a small `select` above the script editor when there
   are >1, letting the user choose which parent's metadata drives
   Parent.Attributes / Parent.CallScript completion + diagnostics.
   Single-parent templates skip the picker (no UI change). Zero
   parents (root template) hide the picker and surface no Parent
   assistance.

Browser-verified on the Sensor Module template (composed by both Pump
and Variable Speed Motor): picker shows both options, switching
updates the editor's parent metadata immediately via the existing
GetContext callback.

Test counts unchanged (159 / 199); the new repo method is exercised
end-to-end by the parent-picker browser path.
2026-05-12 06:00:02 -04:00
Joseph Doherty
0b24b4537d feat(ui/scripts): editor support for self/child/parent accessors
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.
2026-05-12 05:53:13 -04:00
Joseph Doherty
0528c65cba feat(ui/scripts): format, inlay hints, problems panel, type diagnostic
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.
2026-05-12 05:28:13 -04:00
Joseph Doherty
004c5da582 feat(ui/scripts): shape-aware Monaco features for script calls
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
2026-05-12 05:17:59 -04:00
Joseph Doherty
225817eac9 feat(ui/scripts): SCADA-specific Monaco extensions
Wave 3 of the Monaco/Roslyn integration. Adds the four extensions
agreed in the design Q&A:

  1. Parameters["..."] keys — when the cursor is inside a string
     literal that's the index of a Parameters[] element-access,
     completions return the parameter names declared in the form's
     ParameterListEditor.
  2. CallShared("...") names — when the cursor is inside a string
     literal argument to a CallShared(...) invocation, completions
     return the names of all shared scripts (resolved server-side
     via SharedScriptService).
  3. CallScript("...") names — same shape, but uses sibling-script
     names passed from the form (TemplateEdit's _scripts list).
  4. Forbidden-API diagnostic — squiggles uses of the documented
     script trust model bans: System.IO / Diagnostics / Reflection /
     Net / Threading.Thread namespaces, plus the named types File,
     Directory, Process, Thread, Socket, etc. New diagnostic codes
     SCADA001 (using directive) and SCADA002 (type identifier).

ScriptAnalysisService gains a SharedScriptService dependency
(scoped, hence the analyzer is now scoped too); CompletionsRequest
carries DeclaredParameters and SiblingScripts; Complete is now async.

MonacoEditor.razor exposes DeclaredParameters / SiblingScripts
parameters plus a [JSInvokable] GetContext() so the JS side asks
for the latest form state on every completion request. The
provider in monaco-init.js looks up the owning editor from the
internal editors map and forwards the context.

ScriptParameterNames helper parses the ParameterListEditor JSON
into a name list — used by SharedScriptForm, ApiMethodForm, and
TemplateEdit's Add-Script form to populate the Monaco context.

Smoke-verified via direct fetch + Monaco trigger:
  - var x = Parameters["  →  popup: "name" (declared parameter)
  - var y = CallShared("  →  popup: GetWeather, Greet
  - using System.IO;      →  SCADA001 squiggle
  - Process.Start(...)    →  SCADA002 squiggle
  - File.ReadAllText(...) →  SCADA002 squiggle

Also fixed: ScriptAnalysisService scoped (was singleton, broke DI
because SharedScriptService is scoped); JS normalizes Pascal-case
context keys from Blazor's record serialization to camel-case for
the request body.
2026-05-12 04:56:56 -04:00
Joseph Doherty
7f01c5547a feat(ui/design): Monaco editor for script code fields
Vendors Monaco 0.55.1 min/vs/ (~15 MB) at
wwwroot/lib/monaco/vs/. No CDN dependency; works on air-gapped
deployments. Loaded lazily on first script-edit via the AMD loader.

wwwroot/js/monaco-init.js exposes window.MonacoBlazor with
createEditor / setValue / getValue / setMarkers / dispose. Handles
loader bootstrap, DotNet round-trip on content change, and marker
sets for later diagnostic wiring.

Components/Shared/MonacoEditor.razor is a Blazor wrapper with
Value / ValueChanged / Language / Height / ReadOnly parameters and
IAsyncDisposable teardown. Bidirectional binding tracks
_lastSentValue to avoid push/pull loops.

Replaces the plain textareas in SharedScriptForm, TemplateEdit's
Add-Script form, and ApiMethodForm. Default height 320px ≈ the
previous rows=10. Build / tests / dialog flow unaffected.

Wave 1 of three. Roslyn-backed completions and SCADA-specific
extensions follow in subsequent commits.
2026-05-12 04:34:41 -04:00
Joseph Doherty
1b98d37919 refactor(ui/design): replace JSON inputs with structured editors
Two new shared components in Components/Shared:
  - ParameterListEditor: table of rows (name + type + item type + required + remove)
  - ReturnTypeEditor: single type (+ item type when List)

Both round-trip the same JSON shape already stored on the entity:
  parameters: [{"name":"x","type":"String","required":true},...]
  return:     {"type":"List","itemType":"Integer"} | null

Type set follows the Inbound API validator (Boolean, Integer, Float,
String, Object, List). Legacy values normalize on read — Int32 / int64
/ Double / Decimal / lowercase string / etc all coalesce to the new
set so existing rows render correctly. Re-saving persists the
normalized form.

Applied to:
  - SharedScriptForm
  - TemplateEdit Add Script form (also surfaces ParameterDefinitions
    + ReturnDefinition which the entity supported but the form was
    never wiring through)
  - ApiMethodForm

Graceful degradation: invalid JSON is shown with a "Start fresh"
escape hatch instead of crashing the form.
2026-05-12 04:22:58 -04:00
Joseph Doherty
8038aa7cb5 refactor(ui/shared): introduce IDialogService + DialogHost
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.
2026-05-12 03:57:37 -04:00
Joseph Doherty
b6e2ec8a50 refactor(ui/design): card grid, SMTP split, TemplateEdit vertical-stack
Templates: <h4> in flex header, Expand/Collapse moved into a Bulk
actions dropdown, hover-visible kebab on tree nodes with aria-labels.
TreeView CSS gets a .tv-kebab opacity-on-hover utility.

TemplateCreate: form-control (not -sm) for primary inputs; accessible
Back button.

TemplateEdit: Properties card vertical-stacked with Save at the
bottom-right and Parent rendered as readonly plaintext. Add-member
forms (Attributes, Alarms, Scripts, Compositions) reflowed from
horizontal row g-2 align-items-end into cards with stacked col-12
inputs (Scripts gets rows=10). Lock/Unlock badges show full words.
Per-row Delete moved into a kebab dropdown. Tab nav gains
role="tablist" / role="tab" / aria-selected / aria-controls and panels
get role="tabpanel". Validation entries get consistent strong-and-
muted styling.

SharedScripts: migrated from table to card grid (col-lg-6) matching
Sites; cards show code preview + param/return badges + Edit + kebab.
Search filter, empty state CTA, @key.

SharedScriptForm: small ?-icon tooltips next to Parameters and Return
Definition labels.

ExternalSystems: SMTP split out to its own page; remaining tabs (
External Systems, DB Connections, Notification Lists, API Methods,
API Keys) unified as card grids with per-tab search + empty-state CTA.
Tab nav gets full ARIA instrumentation. Header gains a link to the
new SMTP page.

New page SmtpConfiguration.razor at /design/smtp: vertical-stacked
form using the existing Credentials field on the entity.

ExternalSystemForm: AuthConfig placeholder updates based on the
selected AuthType (None / ApiKey / BasicAuth).

DbConnectionForm: form-text below Connection String noting that the
value is stored in plain text and is admin-only.

ApiMethodForm: Script textarea rows=10; JSON example placeholders
for Params and Returns.

NotificationListForm: form-control sizing on Name/email inputs;
thead.table-dark -> table-light on the recipients table.
2026-05-12 03:32:39 -04:00
Joseph Doherty
b4cb7e6f5f feat(templates): lock ParentTemplateId after creation
Template inheritance is set once at create time and immutable on update.
UpdateTemplateAsync now returns "Parent template cannot be changed after
creation." when the caller sends a parent that differs from the stored
value — server-side enforcement covers UI, ManagementService, and CLI.
TemplateEdit renders the parent as static plaintext rather than an
editable dropdown; TemplateCreate's parent picker is unchanged.
2026-05-11 21:29:21 -04:00
Joseph Doherty
8e388a89c5 feat(ui/templates): adopt TreeView design guide; split editor to /design/templates/{id}
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.
2026-05-11 20:52:34 -04:00