diff --git a/ui/src/App.affine b/ui/src/App.affine index eb92faa..c848c71 100644 --- a/ui/src/App.affine +++ b/ui/src/App.affine @@ -1,7 +1,389 @@ // SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// Ported via Harvard Engine bulk-processor +// Ported via Harvard Engine (Semantic pass) module App; -// TODO: Complete semantic implementation +struct apiState { + | Idle + | Loading + | Loaded(string) + | Failed(string) + +struct scriptAction { { + id: string, + label: string, + detail: string, +} + +struct model { { + route: CadreTeaRouter.route, + harCategory: string, + harTarget: string, + harStatus: apiState, + harTargets: apiState, + harRoute: apiState, + scriptRun: apiState, +} + +struct msg { + | SyncRoute + | Navigate(CadreTeaRouter.route) + | SetHarCategory(string) + | SetHarTarget(string) + | FetchHarStatus + | FetchHarTargets + | RunHarRoute + | RunScript(string) + | HarStatusResult(result) + | HarTargetsResult(result) + | HarRouteResult(result) + | ScriptResult(result) + +fn scriptActions: array = [ + { + id: "wiki_audit", + label: "Wiki Audit", + detail: "Run scripts/wiki-audit.sh", + }, + { + id: "project_tabs_audit", + label: "Project Tabs Audit", + detail: "Run scripts/project-tabs-audit.sh", + }, + { + id: "branch_protection_dry_run", + label: "Branch Protection (dry-run)", + detail: "Run scripts/branch-protection-apply.sh --dry-run", + }, + { + id: "md_to_adoc", + label: "MD to ADOC", + detail: "Run scripts/md_to_adoc_converter.sh", + }, + { + id: "standardize_readmes", + label: "Standardize READMEs", + detail: "Run scripts/standardize_readmes.sh", + }, + { + id: "audit_scripts", + label: "Audit Scripts", + detail: "Run scripts/audit_script.sh", + }, + { + id: "verify", + label: "Verify", + detail: "Run scripts/verify.sh", + }, + { + id: "gh_cli", + label: "Use GH CLI", + detail: "Run scripts/USE-GH-CLI.sh", + }, +] + +fn init = (): (model, Tea.Cmd.t) => { + fn route = CadreTeaRouter.currentRoute() + ( + { + route, + harCategory: "filesystem", + harTarget: "", + harStatus: Idle, + harTargets: Idle, + harRoute: Idle, + scriptRun: Idle, + }, + Tea.Cmd.batch(list{Tea.Cmd.msg(FetchHarStatus), Tea.Cmd.msg(FetchHarTargets)}), + ) +} + +fn failWithHttpError = (httpError: Tea.Http.error): apiState => Failed(Tea.Http.errorToString(httpError)) + +fn update = (model: model, msg: msg): (model, Tea.Cmd.t) => { + switch msg { + | SyncRoute => ({...model, route: CadreTeaRouter.currentRoute()}, Tea.Cmd.none) + | Navigate(route) => { + if route == model.route { + (model, Tea.Cmd.none) + } else { + ({...model, route}, CadreTeaRouter.navigate(route, SyncRoute)) + } + } + | SetHarCategory(category) => ({...model, harCategory: category}, Tea.Cmd.none) + | SetHarTarget(target) => ({...model, harTarget: target}, Tea.Cmd.none) + | FetchHarStatus => + ( + {...model, harStatus: Loading}, + HarApi.fetchStatus(result => HarStatusResult(result)), + ) + | FetchHarTargets => + ( + {...model, harTargets: Loading}, + HarApi.fetchTargets(result => HarTargetsResult(result)), + ) + | RunHarRoute => { + fn category = model.harCategory->String.trim + if category == "" { + ({...model, harRoute: Failed("Category is required.")}, Tea.Cmd.none) + } else { + fn target = + switch model.harTarget->String.trim { + | "" => None + | value => Some(value) + } + ( + {...model, harRoute: Loading}, + HarApi.routeCategory(~category, ~target, ~toMsg=result => HarRouteResult(result)), + ) + } + } + | RunScript(action) => ( + {...model, scriptRun: Loading}, + HarApi.runScript(~action, ~toMsg=result => ScriptResult(result)), + ) + | HarStatusResult(result) => { + switch result { + | Ok(body) => ({...model, harStatus: Loaded(body)}, Tea.Cmd.none) + | Error(httpError) => ({...model, harStatus: failWithHttpError(httpError)}, Tea.Cmd.none) + } + } + | HarTargetsResult(result) => { + switch result { + | Ok(body) => ({...model, harTargets: Loaded(body)}, Tea.Cmd.none) + | Error(httpError) => ({...model, harTargets: failWithHttpError(httpError)}, Tea.Cmd.none) + } + } + | HarRouteResult(result) => { + switch result { + | Ok(body) => ({...model, harRoute: Loaded(body)}, Tea.Cmd.none) + | Error(httpError) => ({...model, harRoute: failWithHttpError(httpError)}, Tea.Cmd.none) + } + } + | ScriptResult(result) => { + switch result { + | Ok(body) => ({...model, scriptRun: Loaded(body)}, Tea.Cmd.none) + | Error(httpError) => ({...model, scriptRun: failWithHttpError(httpError)}, Tea.Cmd.none) + } + } + } +} + +fn viewApiState = (state: apiState): Tea.Html.t => { + open Tea.Html + switch state { + | Idle => div(list{Attrs.class_("status")}, list{text("Idle.")}) + | Loading => div(list{Attrs.class_("status loading")}, list{text("Running...")}) + | Failed(error) => div(list{Attrs.class_("status err")}, list{text(error)}) + | Loaded(payload) => + div( + list{}, + list{ + div(list{Attrs.class_("status ok")}, list{text("Completed")}), + pre(list{Attrs.class_("output")}, list{text(payload)}), + }, + ) + } +} + +fn viewTab = (activeRoute: CadreTeaRouter.route, route: CadreTeaRouter.route): Tea.Html.t => { + open Tea.Html + fn className = if activeRoute == route { "tab active" } else { "tab" } + button(list{Attrs.class_(className), Events.onClick(Navigate(route))}, list{text(CadreTeaRouter.label(route))}) +} + +fn viewScriptAction = (action: scriptAction): Tea.Html.t => { + open Tea.Html + li( + list{}, + list{ + div( + list{Attrs.class_("row")}, + list{ + button( + list{Attrs.class_("button secondary"), Events.onClick(RunScript(action.id))}, + list{text(action.label)}, + ), + span(list{Attrs.class_("muted")}, list{text(action.detail)}), + }, + ), + }, + ) +} + +fn viewDashboard = (model: model): Tea.Html.t => { + open Tea.Html + fragment( + list{ + div( + list{Attrs.class_("panel")}, + list{ + h2(list{}, list{text("Hybrid Automation Router Status")}), + p(list{Attrs.class_("muted")}, list{text("Live status from HAR CLI.")}), + div( + list{Attrs.class_("row")}, + list{ + button(list{Attrs.class_("button"), Events.onClick(FetchHarStatus)}, list{text("Refresh Status")}), + button( + list{Attrs.class_("button secondary"), Events.onClick(FetchHarTargets)}, + list{text("Load Targets")}, + ), + }, + ), + viewApiState(model.harStatus), + }, + ), + div( + list{Attrs.class_("panel")}, + list{ + h2(list{}, list{text("Current HAR Targets")}), + p(list{Attrs.class_("muted")}, list{text("Resolved from the active HAR command source.")}), + viewApiState(model.harTargets), + }, + ), + div( + list{Attrs.class_("panel")}, + list{ + h2(list{}, list{text("Latest Script Output")}), + p( + list{Attrs.class_("muted")}, + list{text("Run script actions from Operations tab. Output appears here and in Operations.")}, + ), + viewApiState(model.scriptRun), + }, + ), + }, + ) +} + +fn viewOperations = (model: model): Tea.Html.t => { + open Tea.Html + fn actionNodes = scriptActions->Array.map(viewScriptAction)->List.fromArray + + fragment( + list{ + div( + list{Attrs.class_("panel")}, + list{ + h2(list{}, list{text("Script Operations")}), + p( + list{Attrs.class_("muted")}, + list{text("These actions run allowlisted scripts from git-scripts/scripts.")}, + ), + ul(list{Attrs.class_("actions")}, actionNodes), + }, + ), + div( + list{Attrs.class_("panel")}, + list{ + h2(list{}, list{text("Operation Result")}), + viewApiState(model.scriptRun), + }, + ), + }, + ) +} + +fn viewHybridRouter = (model: model): Tea.Html.t => { + open Tea.Html + fragment( + list{ + div( + list{Attrs.class_("panel")}, + list{ + h2(list{}, list{text("Route A Category")}), + p( + list{Attrs.class_("muted")}, + list{text("Send a category to HAR and inspect the routing decision payload.")}, + ), + div( + list{Attrs.class_("row")}, + list{ + input( + list{ + Attrs.class_("input"), + Attrs.placeholder("category, e.g. filesystem"), + Attrs.value(model.harCategory), + Events.onInput(value => SetHarCategory(value)), + }, + list{}, + ), + input( + list{ + Attrs.class_("input"), + Attrs.placeholder("optional target hint"), + Attrs.value(model.harTarget), + Events.onInput(value => SetHarTarget(value)), + }, + list{}, + ), + }, + ), + div( + list{Attrs.class_("row")}, + list{ + button(list{Attrs.class_("button"), Events.onClick(RunHarRoute)}, list{text("Route Event")}), + }, + ), + viewApiState(model.harRoute), + }, + ), + div( + list{Attrs.class_("panel")}, + list{ + h2(list{}, list{text("HAR Status Snapshot")}), + viewApiState(model.harStatus), + }, + ), + }, + ) +} + +fn view = (model: model): Tea.Html.t => { + open Tea.Html + div( + list{Attrs.class_("shell")}, + list{ + header( + list{}, + list{ + h1(list{Attrs.class_("title")}, list{text("Git Scripts Cadre-TEA Router UI")}), + p( + list{Attrs.class_("subtitle")}, + list{ + text( + "ReScript-TEA control plane with hybrid-automation-router + allowlisted script execution.", + ), + }, + ), + }, + ), + nav( + list{Attrs.class_("tabs")}, + list{ + viewTab(model.route, CadreTeaRouter.Dashboard), + viewTab(model.route, CadreTeaRouter.Operations), + viewTab(model.route, CadreTeaRouter.HybridRouter), + }, + ), + switch model.route { + | CadreTeaRouter.Dashboard => viewDashboard(model) + | CadreTeaRouter.Operations => viewOperations(model) + | CadreTeaRouter.HybridRouter => viewHybridRouter(model) + }, + }, + ) +} + +fn subscriptions = (_model: model): Tea.Sub.t => CadreTeaRouter.subscriptions(SyncRoute) + +fn start = () => { + Tea.standardProgram( + ~init, + ~update, + ~view, + ~subscriptions, + (), + ) +} + diff --git a/ui/src/CadreTeaRouter.affine b/ui/src/CadreTeaRouter.affine index 714806d..1d7f745 100644 --- a/ui/src/CadreTeaRouter.affine +++ b/ui/src/CadreTeaRouter.affine @@ -1,7 +1,51 @@ // SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// Ported via Harvard Engine bulk-processor +// Ported via Harvard Engine (Semantic pass) module CadreTeaRouter; -// TODO: Complete semantic implementation +struct route { + | Dashboard + | Operations + | HybridRouter + +fn fromPath = (path: string): route => { + switch path { + | "/operations" => Operations + | "/har" => HybridRouter + | _ => Dashboard + } +} + +fn toPath = (route: route): string => { + switch route { + | Dashboard => "/" + | Operations => "/operations" + | HybridRouter => "/har" + } +} + +fn label = (route: route): string => { + switch route { + | Dashboard => "Dashboard" + | Operations => "Operations" + | HybridRouter => "Hybrid Router" + } +} + +fn currentPath = (): string => %raw(`window.location.pathname || "/"`) + +fn currentRoute = (): route => fromPath(currentPath()) + +fn subscriptions = (msg: 'msg): Tea.Sub.t<'msg> => { + Tea.Window.onPopState(msg) +} + +fn navigate = (route: route, syncMsg: 'msg): Tea.Cmd.t<'msg> => { + Tea.Cmd.call(callbacks => { + fn path = toPath(route) + fn _ = %raw(`window.history.pushState({}, "", path)`) + callbacks.enqueue(syncMsg) + }) +} + + diff --git a/ui/src/HarApi.affine b/ui/src/HarApi.affine index de49e6b..c06de01 100644 --- a/ui/src/HarApi.affine +++ b/ui/src/HarApi.affine @@ -1,7 +1,53 @@ // SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// Ported via Harvard Engine bulk-processor +// Ported via Harvard Engine (Semantic pass) module HarApi; -// TODO: Complete semantic implementation +@val external jsonStringify: 'a => string = "JSON.stringify" + +fn decodeRawString = (body: string): result => Ok(body) + +fn makeJsonBody = (pairs: array<(string, string)>): string => { + fn dict = Js.Dict.empty() + Array.forEach(pairs, ((key, value)) => Js.Dict.set(dict, key, value)) + jsonStringify(dict) +} + +fn fetchStatus = (toMsg: result => 'msg): Tea.Cmd.t<'msg> => { + Tea.Http.getString(~url="/api/har/status", ~toMsg) +} + +fn fetchTargets = (toMsg: result => 'msg): Tea.Cmd.t<'msg> => { + Tea.Http.getString(~url="/api/har/targets", ~toMsg) +} + +fn routeCategory = ( + ~category: string, + ~target: option, + ~toMsg: result => 'msg, +): Tea.Cmd.t<'msg> => { + fn trimmedTarget = target->Option.map(String.trim) + fn body = + switch trimmedTarget { + | Some(value) if value != "" => makeJsonBody([("category", category), ("target", value)]) + | _ => makeJsonBody([("category", category)]) + } + + Tea.Http.postJson( + ~url="/api/har/route", + ~body, + ~decoder=decodeRawString, + ~toMsg, + ) +} + +fn runScript = (~action: string, ~toMsg: result => 'msg): Tea.Cmd.t<'msg> => { + fn body = makeJsonBody([("action", action)]) + Tea.Http.postJson( + ~url="/api/scripts/run", + ~body, + ~decoder=decodeRawString, + ~toMsg, + ) +} + diff --git a/ui/src/Main.affine b/ui/src/Main.affine index d410d4c..a23012f 100644 --- a/ui/src/Main.affine +++ b/ui/src/Main.affine @@ -1,7 +1,8 @@ // SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -// Ported via Harvard Engine bulk-processor +// Ported via Harvard Engine (Semantic pass) module Main; -// TODO: Complete semantic implementation +fn _ = App.start() + +