diff --git a/backend-deno/Main.affine b/backend-deno/Main.affine new file mode 100644 index 0000000..d410d4c --- /dev/null +++ b/backend-deno/Main.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Main; + +// TODO: Complete semantic implementation diff --git a/backend-deno/Main.res b/backend-deno/Main.res deleted file mode 100644 index 54e648b..0000000 --- a/backend-deno/Main.res +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/** - * Kaldor Community Manufacturing Platform — Deno Backend (ReScript). - * - * This module implements the central orchestrator for the Kaldor platform. - * It integrates verified WASM kernels for compute-intensive tasks and - * provides a unified API for managing distributed manufacturing jobs. - * - * TECHNOLOGY STACK: - * - **Oak**: High-assurance middleware-based web framework for Deno. - * - **WASM**: Accelerated pattern generation and job scheduling. - * - **Matter/OPC UA**: Industrial protocol support for hardware connectivity. - */ - -// BOOTSTRAP: Load environment and instantiate acceleration modules. -let env = await Deno.loadEnv() - -// WASM LOADING: Fetches and instantiates binary kernels for the scheduler. -let schedulerWasm = await Deno.instantiateStreaming( - Deno.fetch(Deno.makeUrl("./wasm/scheduler.wasm", Deno.importMetaUrl)["href"]), -) - -// SERVICE KERNEL: Initializes connections to the multi-protocol stack. -let db = Database.make(envGet("DATABASE_URL", "postgres://kaldor...")) -let mqtt = Mqtt.make(envGet("MQTT_URL", "mqtt://localhost:1883")) - -await Database.connect(db) -await Mqtt.connect(mqtt) - -/** - * API ROUTING: Hierarchical organization of platform capabilities. - * - * ENDPOINTS: - * - `/api/v2/auth`: Identity and access management. - * - `/api/v2/machines`: Real-time status of connected looms. - * - `/api/v2/jobs`: Manufacturing queue management. - * - `/api/v2/wasm/pattern-gen`: Direct interface to the WASM pattern engine. - */ -Oak.Router.use2(router, "/api/v2/machines", Auth.authMiddleware, Oak.Router.routes(MachineRoutes.router)) -// ... [Remaining routes] diff --git a/backend-deno/bindings/Bcrypt.affine b/backend-deno/bindings/Bcrypt.affine new file mode 100644 index 0000000..2ae5cee --- /dev/null +++ b/backend-deno/bindings/Bcrypt.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Bcrypt; + +// TODO: Complete semantic implementation diff --git a/backend-deno/bindings/Bcrypt.res b/backend-deno/bindings/Bcrypt.res deleted file mode 100644 index 19fa9c8..0000000 --- a/backend-deno/bindings/Bcrypt.res +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// FFI bindings for the bcrypt password hashing library. - -/// Hash a plaintext password. Returns a bcrypt hash string. -@module("bcrypt") -external hash: string => promise = "hash" - -/// Compare a plaintext password with a bcrypt hash. -/// Returns true if they match. -@module("bcrypt") -external compare: (string, string) => promise = "compare" diff --git a/backend-deno/bindings/Deno.affine b/backend-deno/bindings/Deno.affine new file mode 100644 index 0000000..0d05f97 --- /dev/null +++ b/backend-deno/bindings/Deno.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Deno; + +// TODO: Complete semantic implementation diff --git a/backend-deno/bindings/Deno.res b/backend-deno/bindings/Deno.res deleted file mode 100644 index 6a87a34..0000000 --- a/backend-deno/bindings/Deno.res +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// FFI bindings for Deno runtime APIs. -/// Provides access to environment variables, signals, versioning, and hostname. - -module Env = { - @val @scope(("Deno", "env")) - external get: string => Js.Nullable.t = "get" -} - -module Version = { - @val @scope(("Deno", "version")) - external deno: string = "deno" -} - -@val @scope("Deno") -external hostname: unit => string = "hostname" - -@val @scope("Deno") -external addSignalListener: (string, unit => unit) => unit = "addSignalListener" - -@val @scope("Deno") -external exit: int => unit = "exit" - -/// Load .env file using deno std library. -@module("std/dotenv/mod.ts") -external loadEnv: unit => promise> = "load" - -/// WebAssembly instantiateStreaming. -@val @scope("WebAssembly") -external instantiateStreaming: promise => promise<{"instance": {"exports": Js.Json.t}}> = "instantiateStreaming" - -/// Fetch API. -@val external fetch: string => promise = "fetch" - -module Fetch = { - module Response = { - type t - } -} - -/// URL constructor. -@new external makeUrl: (string, string) => {"href": string} = "URL" - -/// import.meta.url -@val @scope("import.meta") -external importMetaUrl: string = "url" - -/// TextEncoder for encoding strings to Uint8Array. -type textEncoder -@new external makeTextEncoder: unit => textEncoder = "TextEncoder" -@send external encode: (textEncoder, string) => Js.TypedArray2.Uint8Array.t = "encode" - -/// TextDecoder for decoding Uint8Array to string. -type textDecoder -@new external makeTextDecoder: unit => textDecoder = "TextDecoder" -@send external decode: (textDecoder, Js.TypedArray2.Uint8Array.t) => string = "decode" - -/// setInterval for periodic callbacks. -@val external setInterval: (unit => unit, int) => float = "setInterval" - -/// parseInt. -@val external parseInt: (string, ~radix: int=?) => int = "parseInt" diff --git a/backend-deno/bindings/Jose.affine b/backend-deno/bindings/Jose.affine new file mode 100644 index 0000000..9d2050d --- /dev/null +++ b/backend-deno/bindings/Jose.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Jose; + +// TODO: Complete semantic implementation diff --git a/backend-deno/bindings/Jose.res b/backend-deno/bindings/Jose.res deleted file mode 100644 index 5a37095..0000000 --- a/backend-deno/bindings/Jose.res +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// FFI bindings for the jose JWT library. -/// Provides JWT creation and verification. - -/// Create a JWT token. -/// create(header, payload, secret) => promise -@module("jose") -external create: ({"alg": string}, Js.Json.t, Js.TypedArray2.Uint8Array.t) => promise = - "create" - -/// Verify a JWT token. -/// verify(token, secret) => promise<{ payload: Js.Json.t }> -@module("jose") -external verify: ( - string, - Js.TypedArray2.Uint8Array.t, -) => promise<{"payload": Js.Json.t}> = "verify" - -/// Get a numeric date value (seconds since epoch + offset). -@module("jose") -external getNumericDate: int => int = "getNumericDate" diff --git a/backend-deno/bindings/Matter.affine b/backend-deno/bindings/Matter.affine new file mode 100644 index 0000000..00ef7e5 --- /dev/null +++ b/backend-deno/bindings/Matter.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Matter; + +// TODO: Complete semantic implementation diff --git a/backend-deno/bindings/Matter.res b/backend-deno/bindings/Matter.res deleted file mode 100644 index 4b9e41f..0000000 --- a/backend-deno/bindings/Matter.res +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// FFI bindings for Matter protocol bridge service. -/// These types match the Matter bridge implementation in services/Matter.res. - -/// Matter device record, matching MatterDevice interface in the TS original. -type matterDevice = { - id: string, - name: string, - vendorId: int, - productId: int, - commissioned: bool, - online: bool, - lastSeen: Js.Date.t, - capabilities: array, - nodeId: option, -} - -/// Commission result returned by commissionDevice. -type commissionResult = { - success: bool, - deviceId: option, -} - -/// Command result returned by sendCommand. -type commandResult = { - success: bool, - response: option, -} - -/// Attribute read result returned by readAttribute. -type attributeResult = { - success: bool, - value: option, -} - -/// Fabric information. -type fabricInfo = { - fabricId: string, - deviceCount: int, - onlineDevices: int, -} diff --git a/backend-deno/bindings/Mqtt_Client.affine b/backend-deno/bindings/Mqtt_Client.affine new file mode 100644 index 0000000..62b724c --- /dev/null +++ b/backend-deno/bindings/Mqtt_Client.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Mqtt_Client; + +// TODO: Complete semantic implementation diff --git a/backend-deno/bindings/Mqtt_Client.res b/backend-deno/bindings/Mqtt_Client.res deleted file mode 100644 index c51c865..0000000 --- a/backend-deno/bindings/Mqtt_Client.res +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// FFI bindings for MQTT client library. -/// Provides MQTT pub/sub for device telemetry. - -/// Opaque type for the MQTT client. -type t - -/// MQTT connection options. -type connectOptions = { - clientId: string, - clean: bool, - reconnectPeriod: int, - connectTimeout: int, - username: option, - password: option, -} - -/// QoS publish options. -type publishOptions = {qos: int} - -/// QoS subscribe options. -type subscribeOptions = {qos: int} - -/// The default export of the mqtt module, containing the connect function. -type mqttModule = {connect: (string, connectOptions) => t} - -@module("mqtt") external mqttDefault: mqttModule = "default" - -@send -external on: (t, string, @uncurry (string, Js.TypedArray2.Uint8Array.t) => unit) => unit = "on" - -@send -external onConnect: (t, @as("connect") _, @uncurry unit => unit) => unit = "on" - -@send -external onError: (t, @as("error") _, @uncurry Js.Exn.t => unit) => unit = "on" - -@send -external onOffline: (t, @as("offline") _, @uncurry unit => unit) => unit = "on" - -@send -external onReconnect: (t, @as("reconnect") _, @uncurry unit => unit) => unit = "on" - -@send -external onMessage: ( - t, - @as("message") _, - @uncurry (string, Js.TypedArray2.Uint8Array.t) => unit, -) => unit = "on" - -@send -external publish: ( - t, - string, - string, - publishOptions, - @uncurry Js.Nullable.t => unit, -) => unit = "publish" - -@send -external subscribe: ( - t, - string, - subscribeOptions, - @uncurry Js.Nullable.t => unit, -) => unit = "subscribe" - -@send -external unsubscribe: ( - t, - string, - @uncurry Js.Nullable.t => unit, -) => unit = "unsubscribe" - -@send -external end_: (t, bool, Js.Json.t, @uncurry unit => unit) => unit = "end" diff --git a/backend-deno/bindings/Oak.affine b/backend-deno/bindings/Oak.affine new file mode 100644 index 0000000..fdf3d72 --- /dev/null +++ b/backend-deno/bindings/Oak.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Oak; + +// TODO: Complete semantic implementation diff --git a/backend-deno/bindings/Oak.res b/backend-deno/bindings/Oak.res deleted file mode 100644 index ea8faf4..0000000 --- a/backend-deno/bindings/Oak.res +++ /dev/null @@ -1,142 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// FFI bindings for Oak web framework (Deno). -/// Provides types and external declarations for Application, Router, and Context. - -module Context = { - /// Opaque type for Oak Context objects. - type t - - module Request = { - /// Opaque type for Oak Request objects. - type t - - module Headers = { - /// Opaque type for HTTP Headers. - type t - - @send external get: (t, string) => Js.Nullable.t = "get" - @send external set: (t, string, string) => unit = "set" - } - - @get external headers: t => Headers.t = "headers" - @get external method: t => string = "method" - @get external ip: t => Js.Nullable.t = "ip" - - module Url = { - type t - @get external pathname: t => string = "pathname" - } - - @get external url: t => Url.t = "url" - - /// Call ctx.request.body({ type: 'json' }).value to get parsed JSON body. - @send external body: (t, {"type": string}) => {"value": promise} = "body" - } - - module Response = { - /// Opaque type for Oak Response objects. - type t - - @set external setBody: (t, Js.Json.t) => unit = "body" - @set external setStatus: (t, int) => unit = "status" - - module Headers = { - type t - @send external set: (t, string, string) => unit = "set" - } - - @get external headers: t => Headers.t = "headers" - } - - @get external request: t => Request.t = "request" - @get external response: t => Response.t = "response" - @get external params: t => Js.Dict.t = "params" - - module State = { - /// Opaque type for context state storage. - type t - - @get_index external get: (t, string) => option = "" - @set_index external set: (t, string, Js.Json.t) => unit = "" - } - - @get external state: t => State.t = "state" -} - -/// Type for Oak middleware next() function. -type next = unit => promise - -/// Type alias for middleware functions. -type middleware = (Context.t, next) => promise - -module Router = { - /// Opaque type for Oak Router objects. - type t - - module Routes = { - /// Opaque type for the return value of router.routes(). - type t - } - - module AllowedMethods = { - /// Opaque type for the return value of router.allowedMethods(). - type t - } - - @module("oak") @new external make: unit => t = "Router" - - @send external get: (t, string, middleware) => unit = "get" - @send external post: (t, string, middleware) => unit = "post" - @send external put: (t, string, middleware) => unit = "put" - @send external delete: (t, string, middleware) => unit = "delete" - - /// Mount a sub-router's routes at a path prefix. - @send external use1: (t, string, Routes.t) => unit = "use" - - /// Mount a middleware then sub-router routes at a path prefix. - @send external use2: (t, string, middleware, Routes.t) => unit = "use" - - @send external routes: t => Routes.t = "routes" - @send external allowedMethods: t => AllowedMethods.t = "allowedMethods" -} - -module Application = { - /// Opaque type for Oak Application objects. - type t - - @module("oak") @new external make: unit => t = "Application" - - /// Register middleware on the application. - @send external useMiddleware: (t, middleware) => unit = "use" - - /// Register router routes on the application. - @send external useRoutes: (t, Router.Routes.t) => unit = "use" - - /// Register router allowed methods on the application. - @send external useAllowedMethods: (t, Router.AllowedMethods.t) => unit = "use" - - /// Listen on a port. - @send external listen: (t, {"port": int}) => promise = "listen" - - /// Add an event listener (e.g. 'listen'). - type listenEvent = { - hostname: string, - port: int, - secure: bool, - } - - @send - external addEventListener: (t, string, listenEvent => unit) => unit = "addEventListener" -} - -/// CORS middleware from deno.land/x/cors. -@module("https://deno.land/x/cors@v1.2.2/mod.ts") -external oakCors: {"origin": string, "credentials": bool} => middleware = "oakCors" - -/// Check if an error is an Oak HTTP error. -@module("oak") external isHttpError: Js.Exn.t => bool = "isHttpError" - -/// Get status code from HTTP error. -@get external httpErrorStatus: Js.Exn.t => int = "status" diff --git a/backend-deno/bindings/Opcua.affine b/backend-deno/bindings/Opcua.affine new file mode 100644 index 0000000..3a62c28 --- /dev/null +++ b/backend-deno/bindings/Opcua.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Opcua; + +// TODO: Complete semantic implementation diff --git a/backend-deno/bindings/Opcua.res b/backend-deno/bindings/Opcua.res deleted file mode 100644 index 7b351a5..0000000 --- a/backend-deno/bindings/Opcua.res +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// FFI bindings for OPC UA server types. -/// These types match the OPC UA service in services/Opcua.res. - -/// OPC UA node representation. -type opcuaNode = { - nodeId: string, - browseName: string, - value: Js.Json.t, - dataType: string, - accessLevel: string, -} - -/// Device info for adding a device to the OPC UA address space. -type deviceInfo = { - name: string, - @as("type") type_: string, -} - -/// Device metrics update payload. -type deviceMetrics = { - temperature: option, - vibration: option, - status: option, -} - -/// Server info returned by getServerInfo. -type serverInfo = { - endpoint: string, - namespace: string, - namespaceIndex: int, - securityMode: string, - securityPolicy: string, - authentication: array, - nodeCount: int, -} diff --git a/backend-deno/bindings/Postgres.affine b/backend-deno/bindings/Postgres.affine new file mode 100644 index 0000000..18d181c --- /dev/null +++ b/backend-deno/bindings/Postgres.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Postgres; + +// TODO: Complete semantic implementation diff --git a/backend-deno/bindings/Postgres.res b/backend-deno/bindings/Postgres.res deleted file mode 100644 index 1ee4831..0000000 --- a/backend-deno/bindings/Postgres.res +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// FFI bindings for deno.land/x/postgres module. -/// Provides PostgreSQL client for Deno. - -module Client = { - /// Opaque type for the postgres Client. - type t - - @module("postgres") @new external make: string => t = "Client" - - @send external connect: t => promise = "connect" - @send external end: t => promise = "end" - - module Session = { - type t - @get external dbName: t => string = "dbName" - } - - @get external session: t => Session.t = "session" - - /// Execute a parameterized query, returning rows as an array of JSON objects. - @send - external queryObject: (t, string, option>) => promise<{"rows": array, "rowCount": Js.Nullable.t}> = "queryObject" -} diff --git a/backend-deno/bindings/Redis_Client.affine b/backend-deno/bindings/Redis_Client.affine new file mode 100644 index 0000000..89cb277 --- /dev/null +++ b/backend-deno/bindings/Redis_Client.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Redis_Client; + +// TODO: Complete semantic implementation diff --git a/backend-deno/bindings/Redis_Client.res b/backend-deno/bindings/Redis_Client.res deleted file mode 100644 index f17693a..0000000 --- a/backend-deno/bindings/Redis_Client.res +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// FFI bindings for deno.land/x/redis module. -/// Provides Redis client for Deno. - -/// Opaque type for the Redis connection object. -type t - -@module("redis") -external connect: {"hostname": string, "port": int} => promise = "connect" - -@send external get: (t, string) => promise> = "get" -@send external set: (t, string, string) => promise = "set" -@send external setex: (t, string, int, string) => promise = "setex" -@send external del: (t, string) => promise = "del" -@send external exists: (t, string) => promise = "exists" -@send external close: t => unit = "close" - -@send external publish: (t, string, string) => promise = "publish" - -/// Subscribe to a channel with a callback. -@send -external subscribe: (t, string, string => unit) => promise = "subscribe" - -/// Sorted set operations for rate limiting. -@send -external zremrangebyscore: (t, string, string, string) => promise = "zremrangebyscore" - -@send external zcard: (t, string) => promise = "zcard" -@send external zadd: (t, string, float, string) => promise = "zadd" -@send external expire: (t, string, int) => promise = "expire" diff --git a/backend-deno/middleware/Auth.affine b/backend-deno/middleware/Auth.affine new file mode 100644 index 0000000..dbc5519 --- /dev/null +++ b/backend-deno/middleware/Auth.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Auth; + +// TODO: Complete semantic implementation diff --git a/backend-deno/middleware/Auth.res b/backend-deno/middleware/Auth.res deleted file mode 100644 index 7da6d9b..0000000 --- a/backend-deno/middleware/Auth.res +++ /dev/null @@ -1,264 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// Authentication middleware. -/// JWT-based authentication with Redis session storage. - -/// Auth payload carried in JWT claims and attached to request context. -type authPayload = { - userId: string, - username: string, - roles: array, - perimeter: int, // TPCF perimeter: 1, 2, or 3 -} - -/// JWT secret, read from environment or using a default for development. -let jwtSecret = { - let encoder = Deno.makeTextEncoder() - let secret = - Deno.Env.get("JWT_SECRET") - ->Js.Nullable.toOption - ->Option.getOr("kaldor-secret-change-in-production") - Deno.encode(encoder, secret) -} - -let jwtAlgorithm = "HS256" - -/// Generate a JWT token from an auth payload. -let generateToken = async (payload: authPayload): string => { - let claims = Js.Dict.empty() - Js.Dict.set(claims, "sub", Js.Json.string(payload.userId)) - Js.Dict.set(claims, "username", Js.Json.string(payload.username)) - Js.Dict.set( - claims, - "roles", - Js.Json.array(payload.roles->Array.map(Js.Json.string)), - ) - Js.Dict.set(claims, "perimeter", Js.Json.number(Int.toFloat(payload.perimeter))) - Js.Dict.set( - claims, - "exp", - Js.Json.number(Int.toFloat(Jose.getNumericDate(60 * 60 * 24))), - ) // 24 hours - Js.Dict.set( - claims, - "iat", - Js.Json.number(Int.toFloat(Jose.getNumericDate(0))), - ) - - await Jose.create({"alg": jwtAlgorithm}, Js.Json.object_(claims), jwtSecret) -} - -/// Verify a JWT token and extract the auth payload. -/// Returns None if verification fails. -let verifyToken = async (token: string): option => { - try { - let result = await Jose.verify(token, jwtSecret) - let payload = result["payload"] - - // Extract fields from the JWT payload JSON - let getStr = (json, key) => { - switch Js.Json.decodeObject(json) { - | Some(dict) => - switch Js.Dict.get(dict, key) { - | Some(v) => Js.Json.decodeString(v)->Option.getOr("") - | None => "" - } - | None => "" - } - } - - let getInt = (json, key) => { - switch Js.Json.decodeObject(json) { - | Some(dict) => - switch Js.Dict.get(dict, key) { - | Some(v) => Js.Json.decodeNumber(v)->Option.map(Float.toInt)->Option.getOr(3) - | None => 3 - } - | None => 3 - } - } - - let getRoles = json => { - switch Js.Json.decodeObject(json) { - | Some(dict) => - switch Js.Dict.get(dict, "roles") { - | Some(v) => - switch Js.Json.decodeArray(v) { - | Some(arr) => arr->Array.map(r => Js.Json.decodeString(r)->Option.getOr("")) - | None => [] - } - | None => [] - } - | None => [] - } - } - - Some({ - userId: getStr(payload, "sub"), - username: getStr(payload, "username"), - roles: getRoles(payload), - perimeter: getInt(payload, "perimeter"), - }) - } catch { - | exn => - let meta = Js.Dict.empty() - Js.Dict.set( - meta, - "error", - Js.Json.string( - exn->Js.Exn.asJsExn->Option.flatMap(Js.Exn.message)->Option.getOr("unknown"), - ), - ) - Logger.warn(Logger.logger, "JWT verification failed", ~meta) - None - } -} - -/// Encode an authPayload as Js.Json.t for storing in context state. -let authPayloadToJson = (payload: authPayload): Js.Json.t => { - let dict = Js.Dict.empty() - Js.Dict.set(dict, "userId", Js.Json.string(payload.userId)) - Js.Dict.set(dict, "username", Js.Json.string(payload.username)) - Js.Dict.set( - dict, - "roles", - Js.Json.array(payload.roles->Array.map(Js.Json.string)), - ) - Js.Dict.set(dict, "perimeter", Js.Json.number(Int.toFloat(payload.perimeter))) - Js.Json.object_(dict) -} - -/// Decode an authPayload from Js.Json.t stored in context state. -let authPayloadFromJson = (json: Js.Json.t): option => { - switch Js.Json.decodeObject(json) { - | Some(dict) => { - let userId = - Js.Dict.get(dict, "userId")->Option.flatMap(Js.Json.decodeString)->Option.getOr("") - let username = - Js.Dict.get(dict, "username")->Option.flatMap(Js.Json.decodeString)->Option.getOr("") - let roles = switch Js.Dict.get(dict, "roles")->Option.flatMap(Js.Json.decodeArray) { - | Some(arr) => - arr->Array.map(r => Js.Json.decodeString(r)->Option.getOr("")) - | None => [] - } - let perimeter = - Js.Dict.get(dict, "perimeter") - ->Option.flatMap(Js.Json.decodeNumber) - ->Option.map(Float.toInt) - ->Option.getOr(3) - Some({userId, username, roles, perimeter}) - } - | None => None - } -} - -/// Authentication middleware. Validates JWT Bearer token and attaches -/// the auth payload to ctx.state.auth. -let authMiddleware: Oak.middleware = async (ctx, next) => { - let authHeader = - Oak.Context.request(ctx) - ->Oak.Context.Request.headers - ->Oak.Context.Request.Headers.get("Authorization") - ->Js.Nullable.toOption - - switch authHeader { - | None | Some("") => - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(401) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Unauthorized")) - Js.Dict.set(body, "message", Js.Json.string("Missing or invalid authorization header")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - | Some(header) => - if !String.startsWith(header, "Bearer ") { - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(401) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Unauthorized")) - Js.Dict.set(body, "message", Js.Json.string("Missing or invalid authorization header")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } else { - let token = String.sliceToEnd(header, ~start=7) - let payload = await verifyToken(token) - - switch payload { - | None => - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(401) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Unauthorized")) - Js.Dict.set(body, "message", Js.Json.string("Invalid or expired token")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - | Some(auth) => - Oak.Context.state(ctx)->Oak.Context.State.set("auth", authPayloadToJson(auth)) - await next() - } - } - } -} - -/// Role-based authorization middleware factory. -/// Returns a middleware that checks if the user has any of the required roles. -let requireRole = (roles: array): Oak.middleware => { - async (ctx, next) => { - let authJson = Oak.Context.state(ctx)->Oak.Context.State.get("auth") - let auth = authJson->Option.flatMap(authPayloadFromJson) - - switch auth { - | None => - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(401) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Unauthorized")) - Js.Dict.set(body, "message", Js.Json.string("Authentication required")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - | Some(auth) => - let hasRole = roles->Array.some(role => auth.roles->Array.includes(role)) - if !hasRole { - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(403) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Forbidden")) - Js.Dict.set( - body, - "message", - Js.Json.string(`Required roles: ${Array.join(roles, ", ")}`), - ) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } else { - await next() - } - } - } -} - -/// TPCF perimeter authorization middleware factory. -/// Returns a middleware that checks if the user's perimeter is at or below -/// the required maximum. -let requirePerimeter = (maxPerimeter: int): Oak.middleware => { - async (ctx, next) => { - let authJson = Oak.Context.state(ctx)->Oak.Context.State.get("auth") - let auth = authJson->Option.flatMap(authPayloadFromJson) - - switch auth { - | None => - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(401) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Unauthorized")) - Js.Dict.set(body, "message", Js.Json.string("Authentication required")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - | Some(auth) => - if auth.perimeter > maxPerimeter { - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(403) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Forbidden")) - Js.Dict.set( - body, - "message", - Js.Json.string( - `Required perimeter: ${Int.toString(maxPerimeter)} or lower (you have ${Int.toString(auth.perimeter)})`, - ), - ) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } else { - await next() - } - } - } -} diff --git a/backend-deno/middleware/Error.affine b/backend-deno/middleware/Error.affine new file mode 100644 index 0000000..ea39196 --- /dev/null +++ b/backend-deno/middleware/Error.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Error; + +// TODO: Complete semantic implementation diff --git a/backend-deno/middleware/Error.res b/backend-deno/middleware/Error.res deleted file mode 100644 index 505e5db..0000000 --- a/backend-deno/middleware/Error.res +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// Error handling middleware. -/// Catches and formats errors for API responses. - -/// Error handling middleware. Wraps downstream middleware in a try/catch -/// and returns structured JSON error responses. -let errorHandler: Oak.middleware = async (ctx, next) => { - try { - await next() - } catch { - | exn => - let jsExn = exn->Js.Exn.asJsExn - let errorMessage = jsExn->Option.flatMap(Js.Exn.message)->Option.getOr("Unknown error") - let errorStack = - jsExn->Option.flatMap(e => %raw(`e.stack || undefined`))->Option.getOr("") - let errorName = - jsExn->Option.flatMap(e => %raw(`e.name || undefined`))->Option.getOr("Error") - - // Log the error - let meta = Js.Dict.empty() - Js.Dict.set( - meta, - "method", - Js.Json.string(Oak.Context.request(ctx)->Oak.Context.Request.method), - ) - Js.Dict.set( - meta, - "url", - Js.Json.string( - Oak.Context.request(ctx)->Oak.Context.Request.url->Oak.Context.Request.Url.pathname, - ), - ) - Js.Dict.set(meta, "error", Js.Json.string(errorMessage)) - Js.Dict.set(meta, "stack", Js.Json.string(errorStack)) - Logger.error(Logger.logger, "Request error", ~meta) - - // Handle HTTP errors (thrown by Oak) - let isHttp = switch jsExn { - | Some(e) => Oak.isHttpError(e) - | None => false - } - - if isHttp { - let status = switch jsExn { - | Some(e) => Oak.httpErrorStatus(e) - | None => 500 - } - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(status) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string(errorName)) - Js.Dict.set(body, "message", Js.Json.string(errorMessage)) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } else if errorName === "ValidationError" { - // Handle validation errors - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(400) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Validation Error")) - Js.Dict.set(body, "message", Js.Json.string(errorMessage)) - let details = switch jsExn { - | Some(e) => %raw(`e.details || []`) - | None => Js.Json.array([]) - } - Js.Dict.set(body, "details", details) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } else if errorName === "PostgresError" { - // Handle database errors - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(500) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Database Error")) - Js.Dict.set(body, "message", Js.Json.string("A database error occurred")) - let code = switch jsExn { - | Some(e) => %raw(`e.code || "unknown"`) - | None => Js.Json.string("unknown") - } - Js.Dict.set(body, "code", code) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } else { - // Generic server error - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(500) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Internal Server Error")) - let isProd = - Deno.Env.get("NODE_ENV")->Js.Nullable.toOption->Option.getOr("") === "production" - let msg = if isProd { - "An unexpected error occurred" - } else { - errorMessage - } - Js.Dict.set(body, "message", Js.Json.string(msg)) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } - } -} diff --git a/backend-deno/middleware/RateLimit.affine b/backend-deno/middleware/RateLimit.affine new file mode 100644 index 0000000..721bed9 --- /dev/null +++ b/backend-deno/middleware/RateLimit.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module RateLimit; + +// TODO: Complete semantic implementation diff --git a/backend-deno/middleware/RateLimit.res b/backend-deno/middleware/RateLimit.res deleted file mode 100644 index ab2b943..0000000 --- a/backend-deno/middleware/RateLimit.res +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// Rate limiting middleware. -/// Uses an in-memory sliding window for development -/// (use Redis in production for distributed rate limiting). - -/// Rate limit configuration options. -type rateLimitOptions = { - windowMs: int, - max: int, - skipSuccessfulRequests: bool, - skipFailedRequests: bool, -} - -/// Internal record tracking request counts per key. -type requestRecord = { - mutable count: int, - resetTime: float, -} - -/// In-memory store for rate limit tracking. -let requestCounts: Js.Dict.t = Js.Dict.empty() - -/// Create a rate limiter middleware with the given options. -let rateLimit = (~windowMs: int, ~max: int, ~skipSuccessfulRequests=false, ~skipFailedRequests=false): Oak.middleware => { - let _ = {windowMs, max, skipSuccessfulRequests, skipFailedRequests} - - async (ctx, next) => { - let ip = - Oak.Context.request(ctx) - ->Oak.Context.Request.ip - ->Js.Nullable.toOption - ->Option.getOr("unknown") - let key = `ratelimit:${ip}` - let now = Js.Date.now() - - // Get or create record - let record = switch Js.Dict.get(requestCounts, key) { - | Some(r) if now <= r.resetTime => r - | _ => { - let r = {count: 0, resetTime: now +. Int.toFloat(windowMs)} - Js.Dict.set(requestCounts, key, r) - r - } - } - - // Check if rate limit exceeded - if record.count >= max { - let retryAfter = Float.toInt(Math.ceil((record.resetTime -. now) /. 1000.0)) - - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(429) - let headers = Oak.Context.response(ctx)->Oak.Context.Response.headers - Oak.Context.Response.Headers.set(headers, "Retry-After", Int.toString(retryAfter)) - Oak.Context.Response.Headers.set(headers, "X-RateLimit-Limit", Int.toString(max)) - Oak.Context.Response.Headers.set(headers, "X-RateLimit-Remaining", "0") - Oak.Context.Response.Headers.set( - headers, - "X-RateLimit-Reset", - Float.toString(record.resetTime), - ) - - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Too Many Requests")) - Js.Dict.set( - body, - "message", - Js.Json.string(`Rate limit exceeded. Try again in ${Int.toString(retryAfter)} seconds.`), - ) - Js.Dict.set(body, "retryAfter", Js.Json.number(Int.toFloat(retryAfter))) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - - let meta = Js.Dict.empty() - Js.Dict.set(meta, "key", Js.Json.string(key)) - Js.Dict.set(meta, "count", Js.Json.number(Int.toFloat(record.count))) - Js.Dict.set(meta, "max", Js.Json.number(Int.toFloat(max))) - Logger.warn(Logger.logger, "Rate limit exceeded", ~meta) - } else { - // Increment counter - record.count = record.count + 1 - - // Set rate limit headers - let headers = Oak.Context.response(ctx)->Oak.Context.Response.headers - Oak.Context.Response.Headers.set(headers, "X-RateLimit-Limit", Int.toString(max)) - Oak.Context.Response.Headers.set( - headers, - "X-RateLimit-Remaining", - Int.toString(max - record.count), - ) - Oak.Context.Response.Headers.set( - headers, - "X-RateLimit-Reset", - Float.toString(record.resetTime), - ) - - await next() - - // Optionally skip counting based on response status - let status: int = %raw(`ctx.response.status`) - if (skipSuccessfulRequests && status < 400) || (skipFailedRequests && status >= 400) { - record.count = record.count - 1 - } - } - } -} - -/// Periodic cleanup of expired rate limit entries (every 60 seconds). -let _ = Deno.setInterval(() => { - let now = Js.Date.now() - Js.Dict.entries(requestCounts)->Array.forEach(((key, record)) => { - if now > record.resetTime { - let _ = %raw(`delete requestCounts[key]`) - } - }) -}, 60000) diff --git a/backend-deno/rescript.json b/backend-deno/rescript.json deleted file mode 100644 index 074492d..0000000 --- a/backend-deno/rescript.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "kaldor-iiot-backend", - "sources": [{"dir": ".", "subdirs": true}], - "package-specs": [{"module": "es6", "in-source": true}], - "suffix": ".res.js", - "dependencies": ["@rescript/core"] -} diff --git a/backend-deno/routes/AuthRoutes.affine b/backend-deno/routes/AuthRoutes.affine new file mode 100644 index 0000000..b9e4e89 --- /dev/null +++ b/backend-deno/routes/AuthRoutes.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AuthRoutes; + +// TODO: Complete semantic implementation diff --git a/backend-deno/routes/AuthRoutes.res b/backend-deno/routes/AuthRoutes.res deleted file mode 100644 index 4e57105..0000000 --- a/backend-deno/routes/AuthRoutes.res +++ /dev/null @@ -1,160 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// Authentication routes. -/// Login, logout, token refresh. - -/// Helper to extract a string field from a parsed JSON body. -let getStr = (json: Js.Json.t, key: string): option => { - switch Js.Json.decodeObject(json) { - | Some(dict) => Js.Dict.get(dict, key)->Option.flatMap(Js.Json.decodeString) - | None => None - } -} - -/// Create and configure the auth router with all authentication endpoints. -let makeRouter = (): Oak.Router.t => { - let router = Oak.Router.make() - - // POST /register - Oak.Router.post(router, "/register", async (ctx, _next) => { - let bodyObj = Oak.Context.request(ctx)->Oak.Context.Request.body({"type": "json"}) - let json = await bodyObj["value"] - - let username = getStr(json, "username") - let password = getStr(json, "password") - - switch (username, password) { - | (Some(username), Some(password)) => - try { - let _passwordHash = await Bcrypt.hash(password) - - // Simulated user creation - let userId = `user-${%raw(`Date.now().toString(36)`)}` - - let meta = Js.Dict.empty() - Js.Dict.set(meta, "userId", Js.Json.string(userId)) - Js.Dict.set(meta, "username", Js.Json.string(username)) - Logger.info(Logger.logger, "User registered", ~meta) - - let body = Js.Dict.empty() - Js.Dict.set(body, "success", Js.Json.boolean(true)) - Js.Dict.set(body, "userId", Js.Json.string(userId)) - Js.Dict.set(body, "message", Js.Json.string("User registered successfully")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } catch { - | _exn => - Logger.error(Logger.logger, "Registration failed") - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(500) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Registration failed")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } - | _ => - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(400) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Username and password required")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } - }) - - // POST /login - Oak.Router.post(router, "/login", async (ctx, _next) => { - let bodyObj = Oak.Context.request(ctx)->Oak.Context.Request.body({"type": "json"}) - let json = await bodyObj["value"] - - let username = getStr(json, "username") - let password = getStr(json, "password") - - switch (username, password) { - | (Some(username), Some(password)) => - try { - // Simulated user lookup (in production: fetch from database) - let passwordHash = await Bcrypt.hash(password) - let valid = await Bcrypt.compare(password, passwordHash) - - if !valid { - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(401) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Invalid credentials")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } else { - let payload: Auth.authPayload = { - userId: "user-demo", - username, - roles: ["user"], - perimeter: 3, - } - - let token = await Auth.generateToken(payload) - - let meta = Js.Dict.empty() - Js.Dict.set(meta, "userId", Js.Json.string("user-demo")) - Js.Dict.set(meta, "username", Js.Json.string(username)) - Logger.info(Logger.logger, "User logged in", ~meta) - - let userObj = Js.Dict.empty() - Js.Dict.set(userObj, "id", Js.Json.string("user-demo")) - Js.Dict.set(userObj, "username", Js.Json.string(username)) - Js.Dict.set( - userObj, - "roles", - Js.Json.array(["user"]->Array.map(Js.Json.string)), - ) - Js.Dict.set(userObj, "perimeter", Js.Json.number(3.0)) - - let body = Js.Dict.empty() - Js.Dict.set(body, "success", Js.Json.boolean(true)) - Js.Dict.set(body, "token", Js.Json.string(token)) - Js.Dict.set(body, "user", Js.Json.object_(userObj)) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } - } catch { - | _exn => - Logger.error(Logger.logger, "Login failed") - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(500) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Login failed")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } - | _ => - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(400) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Username and password required")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } - }) - - // POST /logout - Oak.Router.post(router, "/logout", async (ctx, _next) => { - Logger.info(Logger.logger, "User logged out") - - let body = Js.Dict.empty() - Js.Dict.set(body, "success", Js.Json.boolean(true)) - Js.Dict.set(body, "message", Js.Json.string("Logged out successfully")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - }) - - // GET /me - Oak.Router.get(router, "/me", async (ctx, _next) => { - let authJson = Oak.Context.state(ctx)->Oak.Context.State.get("auth") - let auth = authJson->Option.flatMap(Auth.authPayloadFromJson) - - switch auth { - | None => - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(401) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Not authenticated")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - | Some(authData) => - let body = Js.Dict.empty() - Js.Dict.set(body, "user", Auth.authPayloadToJson(authData)) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } - }) - - router -} - -/// Pre-built router instance. -let router = makeRouter() diff --git a/backend-deno/routes/CommunityRoutes.affine b/backend-deno/routes/CommunityRoutes.affine new file mode 100644 index 0000000..a263971 --- /dev/null +++ b/backend-deno/routes/CommunityRoutes.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module CommunityRoutes; + +// TODO: Complete semantic implementation diff --git a/backend-deno/routes/CommunityRoutes.res b/backend-deno/routes/CommunityRoutes.res deleted file mode 100644 index 0d53fb0..0000000 --- a/backend-deno/routes/CommunityRoutes.res +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// Community routes. -/// Network statistics, node discovery, community metrics. - -/// Create and configure the community routes router. -let makeRouter = (): Oak.Router.t => { - let router = Oak.Router.make() - - // GET /stats - Community statistics - Oak.Router.get(router, "/stats", async (ctx, _next) => { - let body = Js.Dict.empty() - Js.Dict.set(body, "nodes", Js.Json.number(12.0)) - Js.Dict.set(body, "activeDevices", Js.Json.number(36.0)) - Js.Dict.set(body, "totalProduction", Js.Json.number(450.0)) - Js.Dict.set(body, "communitySize", Js.Json.number(25.0)) - Js.Dict.set(body, "kaldorCoefficient", Js.Json.number(0.52)) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - }) - - // GET /nodes - Community nodes - Oak.Router.get(router, "/nodes", async (ctx, _next) => { - let node1 = Js.Dict.empty() - Js.Dict.set(node1, "id", Js.Json.string("node-1")) - Js.Dict.set(node1, "type", Js.Json.string("household")) - Js.Dict.set(node1, "devices", Js.Json.number(3.0)) - Js.Dict.set(node1, "status", Js.Json.string("online")) - - let node2 = Js.Dict.empty() - Js.Dict.set(node2, "id", Js.Json.string("node-2")) - Js.Dict.set(node2, "type", Js.Json.string("social-enterprise")) - Js.Dict.set(node2, "devices", Js.Json.number(9.0)) - Js.Dict.set(node2, "status", Js.Json.string("online")) - - let body = Js.Dict.empty() - Js.Dict.set( - body, - "nodes", - Js.Json.array([Js.Json.object_(node1), Js.Json.object_(node2)]), - ) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - }) - - router -} - -/// Pre-built router instance. -let router = makeRouter() diff --git a/backend-deno/routes/GovernanceRoutes.affine b/backend-deno/routes/GovernanceRoutes.affine new file mode 100644 index 0000000..d5ee35c --- /dev/null +++ b/backend-deno/routes/GovernanceRoutes.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module GovernanceRoutes; + +// TODO: Complete semantic implementation diff --git a/backend-deno/routes/GovernanceRoutes.res b/backend-deno/routes/GovernanceRoutes.res deleted file mode 100644 index d2e0e47..0000000 --- a/backend-deno/routes/GovernanceRoutes.res +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// Governance routes. -/// CURP consensus, quadratic voting, proposals. - -/// Simulated proposal storage. -let proposals: Js.Dict.t = Js.Dict.empty() - -/// Create and configure the governance routes router. -let makeRouter = (): Oak.Router.t => { - let router = Oak.Router.make() - - // GET /proposals - List all proposals - Oak.Router.get(router, "/proposals", async (ctx, _next) => { - let body = Js.Dict.empty() - Js.Dict.set(body, "proposals", Js.Json.array(Js.Dict.values(proposals))) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - }) - - // POST /proposals - Create a new proposal - Oak.Router.post(router, "/proposals", async (ctx, _next) => { - let bodyObj = Oak.Context.request(ctx)->Oak.Context.Request.body({"type": "json"}) - let json = await bodyObj["value"] - - let proposalId = `prop-${%raw(`Date.now().toString(36)`)}` - - // Merge body with proposal metadata - let proposal: Js.Json.t = %raw(`Object.assign({}, json, { - id: proposalId, - votes: [], - status: "active", - createdAt: new Date().toISOString() - })`) - - Js.Dict.set(proposals, proposalId, proposal) - - let meta = Js.Dict.empty() - Js.Dict.set(meta, "proposalId", Js.Json.string(proposalId)) - Logger.info(Logger.logger, "Proposal created", ~meta) - - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(201) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(proposal) - }) - - // POST /proposals/:id/vote - Vote on a proposal - Oak.Router.post(router, "/proposals/:id/vote", async (ctx, _next) => { - let id = Js.Dict.get(Oak.Context.params(ctx), "id")->Option.getOr("") - - switch Js.Dict.get(proposals, id) { - | None => - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(404) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Proposal not found")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - | Some(proposal) => - let bodyObj = Oak.Context.request(ctx)->Oak.Context.Request.body({"type": "json"}) - let json = await bodyObj["value"] - - // Add vote with timestamp to the proposal - let updated: Js.Json.t = %raw(`(() => { - const p = Object.assign({}, proposal); - const vote = Object.assign({}, json, { timestamp: Date.now() }); - p.votes = [...(p.votes || []), vote]; - return p; - })()`) - - Js.Dict.set(proposals, id, updated) - - let meta = Js.Dict.empty() - Js.Dict.set(meta, "proposalId", Js.Json.string(id)) - Logger.info(Logger.logger, "Vote cast", ~meta) - - Oak.Context.response(ctx)->Oak.Context.Response.setBody(updated) - } - }) - - router -} - -/// Pre-built router instance. -let router = makeRouter() diff --git a/backend-deno/routes/JobRoutes.affine b/backend-deno/routes/JobRoutes.affine new file mode 100644 index 0000000..9ffb9fa --- /dev/null +++ b/backend-deno/routes/JobRoutes.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module JobRoutes; + +// TODO: Complete semantic implementation diff --git a/backend-deno/routes/JobRoutes.res b/backend-deno/routes/JobRoutes.res deleted file mode 100644 index 3e5508b..0000000 --- a/backend-deno/routes/JobRoutes.res +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// Job queue routes. -/// Manage manufacturing jobs (spinning, weaving, printing). - -/// Simulated job storage. -let jobs: Js.Dict.t = Js.Dict.empty() - -/// Create and configure the job routes router. -let makeRouter = (): Oak.Router.t => { - let router = Oak.Router.make() - - // GET / - List all jobs - Oak.Router.get(router, "/", async (ctx, _next) => { - let body = Js.Dict.empty() - Js.Dict.set(body, "jobs", Js.Json.array(Js.Dict.values(jobs))) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - }) - - // POST / - Create a new job - Oak.Router.post(router, "/", async (ctx, _next) => { - let bodyObj = Oak.Context.request(ctx)->Oak.Context.Request.body({"type": "json"}) - let json = await bodyObj["value"] - - let jobId = `job-${%raw(`Date.now().toString(36)`)}` - - // Merge the body with job metadata - let job: Js.Json.t = %raw(`Object.assign({}, json, { - id: jobId, - status: "queued", - createdAt: new Date().toISOString() - })`) - - Js.Dict.set(jobs, jobId, job) - - let meta = Js.Dict.empty() - Js.Dict.set(meta, "jobId", Js.Json.string(jobId)) - Logger.info(Logger.logger, "Job created", ~meta) - - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(201) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(job) - }) - - router -} - -/// Pre-built router instance. -let router = makeRouter() diff --git a/backend-deno/routes/MachineRoutes.affine b/backend-deno/routes/MachineRoutes.affine new file mode 100644 index 0000000..7b3d666 --- /dev/null +++ b/backend-deno/routes/MachineRoutes.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MachineRoutes; + +// TODO: Complete semantic implementation diff --git a/backend-deno/routes/MachineRoutes.res b/backend-deno/routes/MachineRoutes.res deleted file mode 100644 index 3aadfd2..0000000 --- a/backend-deno/routes/MachineRoutes.res +++ /dev/null @@ -1,252 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// Device/machine routes. -/// Manage IoT devices (looms, spinners, 3D printers). - -/// Simulated device storage (production would use database). -let devices: Js.Dict.t = Js.Dict.empty() - -/// Helper to extract a string field from parsed JSON. -let getStr = (json: Js.Json.t, key: string): option => { - switch Js.Json.decodeObject(json) { - | Some(dict) => Js.Dict.get(dict, key)->Option.flatMap(Js.Json.decodeString) - | None => None - } -} - -/// Create and configure the machine routes router. -let makeRouter = (): Oak.Router.t => { - let router = Oak.Router.make() - - // GET / - List all devices - Oak.Router.get(router, "/", async (ctx, _next) => { - try { - let deviceList = Js.Dict.values(devices) - let body = Js.Dict.empty() - Js.Dict.set(body, "devices", Js.Json.array(deviceList)) - Js.Dict.set(body, "count", Js.Json.number(Array.length(deviceList)->Int.toFloat)) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } catch { - | _exn => - Logger.error(Logger.logger, "Failed to list devices") - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(500) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Failed to list devices")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } - }) - - // GET /:id - Get a single device - Oak.Router.get(router, "/:id", async (ctx, _next) => { - let id = Js.Dict.get(Oak.Context.params(ctx), "id")->Option.getOr("") - - switch Js.Dict.get(devices, id) { - | None => - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(404) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Device not found")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - | Some(device) => - Oak.Context.response(ctx)->Oak.Context.Response.setBody(device) - } - }) - - // POST / - Create a new device - Oak.Router.post(router, "/", async (ctx, _next) => { - try { - let bodyObj = Oak.Context.request(ctx)->Oak.Context.Request.body({"type": "json"}) - let json = await bodyObj["value"] - - let name = getStr(json, "name") - let type_ = getStr(json, "type") - let location = getStr(json, "location") - - switch (name, type_) { - | (Some(name), Some(type_)) => { - let deviceId = `device-${%raw(`Date.now().toString(36)`)}` - - let metrics = Js.Dict.empty() - Js.Dict.set(metrics, "temperature", Js.Json.null) - Js.Dict.set(metrics, "vibration", Js.Json.null) - Js.Dict.set(metrics, "uptime", Js.Json.number(0.0)) - - let device = Js.Dict.empty() - Js.Dict.set(device, "id", Js.Json.string(deviceId)) - Js.Dict.set(device, "name", Js.Json.string(name)) - Js.Dict.set(device, "type", Js.Json.string(type_)) - switch location { - | Some(loc) => Js.Dict.set(device, "location", Js.Json.string(loc)) - | None => () - } - Js.Dict.set(device, "status", Js.Json.string("offline")) - Js.Dict.set(device, "commissioned", Js.Json.boolean(false)) - Js.Dict.set(device, "createdAt", Js.Json.string(Js.Date.make()->Js.Date.toISOString)) - Js.Dict.set(device, "metrics", Js.Json.object_(metrics)) - - let deviceJson = Js.Json.object_(device) - Js.Dict.set(devices, deviceId, deviceJson) - - let meta = Js.Dict.empty() - Js.Dict.set(meta, "deviceId", Js.Json.string(deviceId)) - Js.Dict.set(meta, "name", Js.Json.string(name)) - Js.Dict.set(meta, "type", Js.Json.string(type_)) - Logger.info(Logger.logger, "Device created", ~meta) - - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(201) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(deviceJson) - } - | _ => - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(400) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Name and type required")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } - } catch { - | _exn => - Logger.error(Logger.logger, "Failed to create device") - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(500) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Failed to create device")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } - }) - - // PUT /:id - Update a device - Oak.Router.put(router, "/:id", async (ctx, _next) => { - let id = Js.Dict.get(Oak.Context.params(ctx), "id")->Option.getOr("") - - switch Js.Dict.get(devices, id) { - | None => - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(404) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Device not found")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - | Some(existing) => - try { - let bodyObj = Oak.Context.request(ctx)->Oak.Context.Request.body({"type": "json"}) - let updates = await bodyObj["value"] - - // Merge existing with updates - let merged: Js.Json.t = %raw(`Object.assign({}, existing, updates, { updatedAt: new Date().toISOString() })`) - Js.Dict.set(devices, id, merged) - - let meta = Js.Dict.empty() - Js.Dict.set(meta, "deviceId", Js.Json.string(id)) - Logger.info(Logger.logger, "Device updated", ~meta) - - Oak.Context.response(ctx)->Oak.Context.Response.setBody(merged) - } catch { - | _exn => - let meta = Js.Dict.empty() - Js.Dict.set(meta, "deviceId", Js.Json.string(id)) - Logger.error(Logger.logger, "Failed to update device", ~meta) - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(500) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Failed to update device")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } - } - }) - - // DELETE /:id - Delete a device - Oak.Router.delete(router, "/:id", async (ctx, _next) => { - let id = Js.Dict.get(Oak.Context.params(ctx), "id")->Option.getOr("") - - switch Js.Dict.get(devices, id) { - | None => - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(404) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Device not found")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - | Some(_) => - let _ = %raw(`delete devices[id]`) - - let meta = Js.Dict.empty() - Js.Dict.set(meta, "deviceId", Js.Json.string(id)) - Logger.info(Logger.logger, "Device deleted", ~meta) - - let body = Js.Dict.empty() - Js.Dict.set(body, "success", Js.Json.boolean(true)) - Js.Dict.set(body, "message", Js.Json.string("Device deleted")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } - }) - - // GET /:id/metrics - Get device metrics - Oak.Router.get(router, "/:id/metrics", async (ctx, _next) => { - let id = Js.Dict.get(Oak.Context.params(ctx), "id")->Option.getOr("") - - switch Js.Dict.get(devices, id) { - | None => - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(404) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Device not found")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - | Some(device) => - let metricsVal = switch Js.Json.decodeObject(device) { - | Some(dict) => Js.Dict.get(dict, "metrics")->Option.getOr(Js.Json.null) - | None => Js.Json.null - } - - let body = Js.Dict.empty() - Js.Dict.set(body, "deviceId", Js.Json.string(id)) - Js.Dict.set(body, "current", metricsVal) - Js.Dict.set(body, "history", Js.Json.array([])) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } - }) - - // POST /:id/command - Send command to device - Oak.Router.post(router, "/:id/command", async (ctx, _next) => { - let id = Js.Dict.get(Oak.Context.params(ctx), "id")->Option.getOr("") - - switch Js.Dict.get(devices, id) { - | None => - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(404) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Device not found")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - | Some(_) => - try { - let bodyObj = Oak.Context.request(ctx)->Oak.Context.Request.body({"type": "json"}) - let json = await bodyObj["value"] - - let command = getStr(json, "command") - - switch command { - | None => - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(400) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Command required")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - | Some(cmd) => - let meta = Js.Dict.empty() - Js.Dict.set(meta, "deviceId", Js.Json.string(id)) - Js.Dict.set(meta, "command", Js.Json.string(cmd)) - Logger.info(Logger.logger, "Device command sent", ~meta) - - let body = Js.Dict.empty() - Js.Dict.set(body, "success", Js.Json.boolean(true)) - Js.Dict.set(body, "message", Js.Json.string(`Command '${cmd}' sent to device`)) - Js.Dict.set(body, "timestamp", Js.Json.number(Js.Date.now())) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } - } catch { - | _exn => - let meta = Js.Dict.empty() - Js.Dict.set(meta, "deviceId", Js.Json.string(id)) - Logger.error(Logger.logger, "Failed to send device command", ~meta) - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(500) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("Failed to send command")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } - } - }) - - router -} - -/// Pre-built router instance. -let router = makeRouter() diff --git a/backend-deno/routes/PatternRoutes.affine b/backend-deno/routes/PatternRoutes.affine new file mode 100644 index 0000000..8f53556 --- /dev/null +++ b/backend-deno/routes/PatternRoutes.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module PatternRoutes; + +// TODO: Complete semantic implementation diff --git a/backend-deno/routes/PatternRoutes.res b/backend-deno/routes/PatternRoutes.res deleted file mode 100644 index 63bcfa1..0000000 --- a/backend-deno/routes/PatternRoutes.res +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// Pattern generation routes. -/// 3D weave patterns, WASM-accelerated computation. - -/// Simulated pattern storage. -let patterns: Js.Dict.t = Js.Dict.empty() - -/// Helper to extract a string field from parsed JSON. -let getStr = (json: Js.Json.t, key: string): option => { - switch Js.Json.decodeObject(json) { - | Some(dict) => Js.Dict.get(dict, key)->Option.flatMap(Js.Json.decodeString) - | None => None - } -} - -/// Helper to extract a numeric field from parsed JSON. -let getNum = (json: Js.Json.t, key: string): option => { - switch Js.Json.decodeObject(json) { - | Some(dict) => Js.Dict.get(dict, key)->Option.flatMap(Js.Json.decodeNumber) - | None => None - } -} - -/// Create and configure the pattern routes router. -let makeRouter = (): Oak.Router.t => { - let router = Oak.Router.make() - - // GET / - List all patterns - Oak.Router.get(router, "/", async (ctx, _next) => { - let body = Js.Dict.empty() - Js.Dict.set(body, "patterns", Js.Json.array(Js.Dict.values(patterns))) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - }) - - // POST /generate - Generate a new pattern - Oak.Router.post(router, "/generate", async (ctx, _next) => { - let bodyObj = Oak.Context.request(ctx)->Oak.Context.Request.body({"type": "json"}) - let json = await bodyObj["value"] - - let warp = getNum(json, "warp") - let weft = getNum(json, "weft") - let type_ = getStr(json, "type") - - switch (warp, weft, type_) { - | (Some(warp), Some(weft), Some(type_)) => { - let patternId = `pattern-${%raw(`Date.now().toString(36)`)}` - - let pattern = Js.Dict.empty() - Js.Dict.set(pattern, "id", Js.Json.string(patternId)) - Js.Dict.set(pattern, "warp", Js.Json.number(warp)) - Js.Dict.set(pattern, "weft", Js.Json.number(weft)) - Js.Dict.set(pattern, "type", Js.Json.string(type_)) - Js.Dict.set(pattern, "data", Js.Json.array([])) - Js.Dict.set(pattern, "createdAt", Js.Json.string(Js.Date.make()->Js.Date.toISOString)) - - let patternJson = Js.Json.object_(pattern) - Js.Dict.set(patterns, patternId, patternJson) - - let meta = Js.Dict.empty() - Js.Dict.set(meta, "patternId", Js.Json.string(patternId)) - Js.Dict.set(meta, "type", Js.Json.string(type_)) - Logger.info(Logger.logger, "Pattern generated", ~meta) - - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(201) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(patternJson) - } - | _ => - Oak.Context.response(ctx)->Oak.Context.Response.setStatus(400) - let body = Js.Dict.empty() - Js.Dict.set(body, "error", Js.Json.string("warp, weft, and type required")) - Oak.Context.response(ctx)->Oak.Context.Response.setBody(Js.Json.object_(body)) - } - }) - - router -} - -/// Pre-built router instance. -let router = makeRouter() diff --git a/backend-deno/services/Database.affine b/backend-deno/services/Database.affine new file mode 100644 index 0000000..c60db60 --- /dev/null +++ b/backend-deno/services/Database.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Database; + +// TODO: Complete semantic implementation diff --git a/backend-deno/services/Database.res b/backend-deno/services/Database.res deleted file mode 100644 index d44e48d..0000000 --- a/backend-deno/services/Database.res +++ /dev/null @@ -1,176 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// PostgreSQL + TimescaleDB database service. -/// Manages connections and provides a query interface. - -/// Database client state. -type t = { - mutable client: option, - connectionString: string, - mutable isConnectedFlag: bool, -} - -/// Create a new database client wrapper (not yet connected). -let make = (connectionString: string): t => { - client: None, - connectionString, - isConnectedFlag: false, -} - -/// Connect to the PostgreSQL database and verify TimescaleDB extension. -let connect = async (db: t): unit => { - try { - let client = Postgres.Client.make(db.connectionString) - await Postgres.Client.connect(client) - db.client = Some(client) - db.isConnectedFlag = true - - // Verify TimescaleDB extension - let result = await Postgres.Client.queryObject( - client, - "SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'timescaledb') as installed", - None, - ) - - let rows = result["rows"] - if Array.length(rows) > 0 { - Logger.info(Logger.logger, "TimescaleDB extension verified") - } else { - Logger.warn( - Logger.logger, - "TimescaleDB extension not found - install with CREATE EXTENSION timescaledb", - ) - } - - let dbName = Postgres.Client.session(client)->Postgres.Client.Session.dbName - let meta = Js.Dict.empty() - Js.Dict.set(meta, "database", Js.Json.string(dbName)) - Logger.info(Logger.logger, "PostgreSQL connected", ~meta) - } catch { - | exn => - let meta = Js.Dict.empty() - Js.Dict.set( - meta, - "error", - Js.Json.string(exn->Js.Exn.asJsExn->Option.flatMap(Js.Exn.message)->Option.getOr("unknown")), - ) - Logger.error(Logger.logger, "Failed to connect to PostgreSQL", ~meta) - raise(exn) - } -} - -/// Disconnect from the database. -let disconnect = async (db: t): unit => { - switch db.client { - | Some(client) => - await Postgres.Client.end(client) - db.isConnectedFlag = false - Logger.info(Logger.logger, "PostgreSQL disconnected") - | None => () - } -} - -/// Check if the database is connected. -let isConnected = (db: t): bool => db.isConnectedFlag - -/// Get the raw client, raising if not connected. -let getClient = (db: t): Postgres.Client.t => { - switch db.client { - | Some(client) => client - | None => Js.Exn.raiseError("Database not connected") - } -} - -/// Execute a query and return all result rows. -let query = async (db: t, sql: string, ~params: option>=?): array => { - let client = getClient(db) - let result = await Postgres.Client.queryObject(client, sql, params) - result["rows"] -} - -/// Execute a query and return the first row, or None. -let queryOne = async (db: t, sql: string, ~params: option>=?): option< - Js.Json.t, -> => { - let rows = await query(db, sql, ~params?) - rows->Array.get(0) -} - -/// Execute a query and return the affected row count. -let execute = async (db: t, sql: string, ~params: option>=?): int => { - let client = getClient(db) - let result = await Postgres.Client.queryObject(client, sql, params) - result["rowCount"]->Js.Nullable.toOption->Option.getOr(0) -} - -/// Create a TimescaleDB hypertable. -let createHypertable = async (db: t, tableName: string, ~timeColumn: string="timestamp"): unit => { - try { - let _ = await execute( - db, - `SELECT create_hypertable('${tableName}', '${timeColumn}', if_not_exists => TRUE)`, - ) - Logger.info(Logger.logger, `Hypertable created: ${tableName}`) - } catch { - | exn => - let meta = Js.Dict.empty() - Js.Dict.set( - meta, - "error", - Js.Json.string(exn->Js.Exn.asJsExn->Option.flatMap(Js.Exn.message)->Option.getOr("unknown")), - ) - Logger.error(Logger.logger, `Failed to create hypertable: ${tableName}`, ~meta) - raise(exn) - } -} - -/// Set a TimescaleDB compression policy. -let setCompressionPolicy = async ( - db: t, - tableName: string, - ~compressAfter: string="7 days", -): unit => { - try { - let _ = await execute( - db, - `SELECT add_compression_policy('${tableName}', INTERVAL '${compressAfter}')`, - ) - Logger.info(Logger.logger, `Compression policy set: ${tableName} after ${compressAfter}`) - } catch { - | exn => - let meta = Js.Dict.empty() - Js.Dict.set( - meta, - "error", - Js.Json.string(exn->Js.Exn.asJsExn->Option.flatMap(Js.Exn.message)->Option.getOr("unknown")), - ) - Logger.error(Logger.logger, `Failed to set compression policy: ${tableName}`, ~meta) - raise(exn) - } -} - -/// Set a TimescaleDB data retention policy. -let setRetentionPolicy = async ( - db: t, - tableName: string, - ~retainFor: string="90 days", -): unit => { - try { - let _ = await execute( - db, - `SELECT add_retention_policy('${tableName}', INTERVAL '${retainFor}')`, - ) - Logger.info(Logger.logger, `Retention policy set: ${tableName} retain ${retainFor}`) - } catch { - | exn => - let meta = Js.Dict.empty() - Js.Dict.set( - meta, - "error", - Js.Json.string(exn->Js.Exn.asJsExn->Option.flatMap(Js.Exn.message)->Option.getOr("unknown")), - ) - Logger.error(Logger.logger, `Failed to set retention policy: ${tableName}`, ~meta) - raise(exn) - } -} diff --git a/backend-deno/services/Logger.affine b/backend-deno/services/Logger.affine new file mode 100644 index 0000000..5025565 --- /dev/null +++ b/backend-deno/services/Logger.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Logger; + +// TODO: Complete semantic implementation diff --git a/backend-deno/services/Logger.res b/backend-deno/services/Logger.res deleted file mode 100644 index 2c59230..0000000 --- a/backend-deno/services/Logger.res +++ /dev/null @@ -1,120 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// Structured logging service for Kaldor IIoT. -/// Provides leveled logging with timestamps and context. - -/// Log levels ordered by severity. -type logLevel = - | @as(0) DEBUG - | @as(1) INFO - | @as(2) WARN - | @as(3) ERROR - | @as(4) FATAL - -/// Convert a log level to its string name. -let logLevelName = (level: logLevel): string => - switch level { - | DEBUG => "DEBUG" - | INFO => "INFO" - | WARN => "WARN" - | ERROR => "ERROR" - | FATAL => "FATAL" - } - -/// Convert a log level to its numeric value for comparison. -let logLevelToInt = (level: logLevel): int => - switch level { - | DEBUG => 0 - | INFO => 1 - | WARN => 2 - | ERROR => 3 - | FATAL => 4 - } - -/// Log context is a dictionary of arbitrary key-value pairs. -type logContext = Js.Dict.t - -/// Internal: console.log binding. -@val external consoleLog: string => unit = "console.log" -/// Internal: console.warn binding. -@val external consoleWarn: string => unit = "console.warn" -/// Internal: console.error binding. -@val external consoleError: string => unit = "console.error" - -/// Logger record holding the minimum log level and default context. -type t = { - minLevel: logLevel, - context: logContext, -} - -/// Create a new logger with the given minimum level and context. -let make = (~minLevel: logLevel=INFO, ~context: logContext=Js.Dict.empty()): t => { - minLevel, - context, -} - -/// Internal log method. Outputs structured JSON to the appropriate console -/// stream based on severity. -let log = (logger: t, level: logLevel, message: string, ~meta: logContext=Js.Dict.empty()) => { - if logLevelToInt(level) >= logLevelToInt(logger.minLevel) { - let timestamp = Js.Date.make()->Js.Date.toISOString - let levelName = logLevelName(level) - - // Merge logger context with call-site meta - let merged = Js.Dict.empty() - Js.Dict.entries(logger.context)->Array.forEach(((k, v)) => Js.Dict.set(merged, k, v)) - Js.Dict.entries(meta)->Array.forEach(((k, v)) => Js.Dict.set(merged, k, v)) - - // Build the log entry - let entry = Js.Dict.empty() - Js.Dict.set(entry, "timestamp", Js.Json.string(timestamp)) - Js.Dict.set(entry, "level", Js.Json.string(levelName)) - Js.Dict.set(entry, "message", Js.Json.string(message)) - Js.Dict.entries(merged)->Array.forEach(((k, v)) => Js.Dict.set(entry, k, v)) - - let output = Js.Json.stringifyAny(entry)->Option.getOr("{}") - - if logLevelToInt(level) >= logLevelToInt(ERROR) { - consoleError(output) - } else if level == WARN { - consoleWarn(output) - } else { - consoleLog(output) - } - } -} - -let debug = (logger: t, message: string, ~meta: logContext=Js.Dict.empty()) => - log(logger, DEBUG, message, ~meta) - -let info = (logger: t, message: string, ~meta: logContext=Js.Dict.empty()) => - log(logger, INFO, message, ~meta) - -let warn = (logger: t, message: string, ~meta: logContext=Js.Dict.empty()) => - log(logger, WARN, message, ~meta) - -let error = (logger: t, message: string, ~meta: logContext=Js.Dict.empty()) => - log(logger, ERROR, message, ~meta) - -let fatal = (logger: t, message: string, ~meta: logContext=Js.Dict.empty()) => - log(logger, FATAL, message, ~meta) - -/// Create a child logger that inherits and extends the parent's context. -let child = (logger: t, context: logContext): t => { - let merged = Js.Dict.empty() - Js.Dict.entries(logger.context)->Array.forEach(((k, v)) => Js.Dict.set(merged, k, v)) - Js.Dict.entries(context)->Array.forEach(((k, v)) => Js.Dict.set(merged, k, v)) - {minLevel: logger.minLevel, context: merged} -} - -/// Default logger instance, configured from LOG_LEVEL environment variable. -let logger: t = { - let minLevel = switch Deno.Env.get("LOG_LEVEL")->Js.Nullable.toOption { - | Some("DEBUG") => DEBUG - | _ => INFO - } - let ctx = Js.Dict.empty() - Js.Dict.set(ctx, "service", Js.Json.string("kaldor-iiot")) - make(~minLevel, ~context=ctx) -} diff --git a/backend-deno/services/Matter.affine b/backend-deno/services/Matter.affine new file mode 100644 index 0000000..00ef7e5 --- /dev/null +++ b/backend-deno/services/Matter.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Matter; + +// TODO: Complete semantic implementation diff --git a/backend-deno/services/Matter.res b/backend-deno/services/Matter.res deleted file mode 100644 index 65ab01a..0000000 --- a/backend-deno/services/Matter.res +++ /dev/null @@ -1,284 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// Matter protocol bridge service. -/// Manages ESP32-C6 devices using Matter 1.2+ over Thread/WiFi. -/// -/// Note: This is a simplified implementation. Production would use -/// official Matter SDK (chip-tool) via subprocess or native bindings. - -open Bindings.Matter - -/// Matter bridge state. -type t = { - port: string, - devices: Js.Dict.t, - mutable isRunningFlag: bool, - fabricId: float, // Using float since ReScript doesn't have BigInt -} - -/// Create a new Matter bridge (not yet started). -let make = (port: string): t => { - port, - devices: Js.Dict.empty(), - isRunningFlag: false, - fabricId: 1.0, -} - -/// Check device heartbeats and mark stale devices offline. -let checkDeviceHeartbeats = (bridge: t): unit => { - let now = Js.Date.now() - Js.Dict.values(bridge.devices)->Array.forEach(device => { - let timeSinceLastSeen = now -. Js.Date.getTime(device.lastSeen) - if timeSinceLastSeen > 60000.0 { - // 1 minute timeout - if device.online { - let updated = {...device, online: false} - Js.Dict.set(bridge.devices, device.id, updated) - let meta = Js.Dict.empty() - Js.Dict.set(meta, "deviceId", Js.Json.string(device.id)) - Logger.warn(Logger.logger, "Matter device went offline", ~meta) - } - } - }) -} - -/// Start device discovery (simulated with periodic heartbeat checks). -let startDiscovery = (bridge: t): unit => { - Logger.debug(Logger.logger, "Matter device discovery started") - let _ = Deno.setInterval(() => checkDeviceHeartbeats(bridge), 30000) -} - -/// Start the Matter bridge. -let start = async (bridge: t): unit => { - try { - bridge.isRunningFlag = true - - let meta = Js.Dict.empty() - Js.Dict.set(meta, "port", Js.Json.string(bridge.port)) - Js.Dict.set(meta, "fabricId", Js.Json.string(Float.toString(bridge.fabricId))) - Js.Dict.set(meta, "protocol", Js.Json.string("Matter 1.2+")) - Js.Dict.set(meta, "transport", Js.Json.string("Thread/WiFi")) - Logger.info(Logger.logger, "Matter bridge started", ~meta) - - startDiscovery(bridge) - } catch { - | exn => - Logger.error(Logger.logger, "Failed to start Matter bridge") - raise(exn) - } -} - -/// Stop the Matter bridge. -let stop = async (bridge: t): unit => { - bridge.isRunningFlag = false - Logger.info(Logger.logger, "Matter bridge stopped") -} - -/// Check if the bridge is running. -let isRunning = (bridge: t): bool => bridge.isRunningFlag - -/// Commission a new Matter device. -let commissionDevice = async (bridge: t, _setupCode: string): commissionResult => { - try { - Logger.info(Logger.logger, "Commissioning Matter device") - - let deviceId = `matter-${%raw(`Date.now().toString(36)`)}` - let deviceCount = Js.Dict.keys(bridge.devices)->Array.length - - let device: matterDevice = { - id: deviceId, - name: "ESP32-C6 Loom Controller", - vendorId: 0xfff1, - productId: 0x8000, - commissioned: true, - online: true, - lastSeen: Js.Date.make(), - capabilities: ["temperature", "vibration", "loom-control"], - nodeId: Some(Int.toFloat(deviceCount + 1)), - } - - Js.Dict.set(bridge.devices, deviceId, device) - - let meta = Js.Dict.empty() - Js.Dict.set(meta, "deviceId", Js.Json.string(deviceId)) - Logger.info(Logger.logger, "Matter device commissioned", ~meta) - - {success: true, deviceId: Some(deviceId)} - } catch { - | _exn => - Logger.error(Logger.logger, "Matter commissioning failed") - {success: false, deviceId: None} - } -} - -/// Get all devices. -let getDevices = (bridge: t): array => Js.Dict.values(bridge.devices) - -/// Get a specific device by ID. -let getDevice = (bridge: t, deviceId: string): option => - Js.Dict.get(bridge.devices, deviceId) - -/// Send a command to a Matter device (simulated). -let sendCommand = async ( - bridge: t, - deviceId: string, - clusterId: int, - commandId: int, - _payload: Js.Json.t, -): commandResult => { - switch Js.Dict.get(bridge.devices, deviceId) { - | None => - let meta = Js.Dict.empty() - Js.Dict.set(meta, "deviceId", Js.Json.string(deviceId)) - Logger.error(Logger.logger, "Device not found", ~meta) - {success: false, response: None} - | Some(device) => - if !device.online { - let meta = Js.Dict.empty() - Js.Dict.set(meta, "deviceId", Js.Json.string(deviceId)) - Logger.error(Logger.logger, "Device offline", ~meta) - {success: false, response: None} - } else { - try { - let meta = Js.Dict.empty() - Js.Dict.set(meta, "deviceId", Js.Json.string(deviceId)) - Js.Dict.set( - meta, - "clusterId", - Js.Json.string(`0x${%raw(`clusterId.toString(16)`)}`), - ) - Js.Dict.set( - meta, - "commandId", - Js.Json.string(`0x${%raw(`commandId.toString(16)`)}`), - ) - Logger.debug(Logger.logger, "Sending Matter command", ~meta) - - // Update last seen - Js.Dict.set(bridge.devices, deviceId, {...device, lastSeen: Js.Date.make()}) - - let resp = Js.Dict.empty() - Js.Dict.set(resp, "status", Js.Json.string("ok")) - Js.Dict.set(resp, "timestamp", Js.Json.number(Js.Date.now())) - {success: true, response: Some(Js.Json.object_(resp))} - } catch { - | _exn => - Logger.error(Logger.logger, "Matter command failed") - {success: false, response: None} - } - } - } -} - -/// Simulate reading an attribute based on cluster and attribute IDs. -let simulateAttributeRead = (clusterId: int, attributeId: int): option => { - switch (clusterId, attributeId) { - | (0x0028, 0x0005) => Some(Js.Json.string("Kaldor Loom v1.0")) - | (0x0402, 0x0000) => Some(Js.Json.number(2150.0)) - | (0x0406, 0x0000) => Some(Js.Json.number(1.0)) - | _ => None - } -} - -/// Read an attribute from a Matter device (simulated). -let readAttribute = async ( - bridge: t, - deviceId: string, - clusterId: int, - attributeId: int, -): attributeResult => { - switch Js.Dict.get(bridge.devices, deviceId) { - | None => {success: false, value: None} - | Some(device) => - try { - let meta = Js.Dict.empty() - Js.Dict.set(meta, "deviceId", Js.Json.string(deviceId)) - Js.Dict.set( - meta, - "clusterId", - Js.Json.string(`0x${%raw(`clusterId.toString(16)`)}`), - ) - Js.Dict.set( - meta, - "attributeId", - Js.Json.string(`0x${%raw(`attributeId.toString(16)`)}`), - ) - Logger.debug(Logger.logger, "Reading Matter attribute", ~meta) - - Js.Dict.set(bridge.devices, deviceId, {...device, lastSeen: Js.Date.make()}) - - let value = simulateAttributeRead(clusterId, attributeId) - {success: true, value} - } catch { - | _exn => - Logger.error(Logger.logger, "Matter attribute read failed") - {success: false, value: None} - } - } -} - -/// Subscribe to attribute changes (simulated with periodic polling). -let subscribeAttribute = async ( - bridge: t, - deviceId: string, - clusterId: int, - attributeId: int, - callback: Js.Json.t => unit, -): bool => { - let meta = Js.Dict.empty() - Js.Dict.set(meta, "deviceId", Js.Json.string(deviceId)) - Js.Dict.set( - meta, - "clusterId", - Js.Json.string(`0x${%raw(`clusterId.toString(16)`)}`), - ) - Js.Dict.set( - meta, - "attributeId", - Js.Json.string(`0x${%raw(`attributeId.toString(16)`)}`), - ) - Logger.info(Logger.logger, "Matter attribute subscription created", ~meta) - - // Simulated periodic updates - let _ = Deno.setInterval(async () => { - let result = await readAttribute(bridge, deviceId, clusterId, attributeId) - if result.success { - switch result.value { - | Some(value) => callback(value) - | None => () - } - } - }, 5000) - - true -} - -/// Decommission (remove) a device from the bridge. -let decommissionDevice = async (bridge: t, deviceId: string): bool => { - switch Js.Dict.get(bridge.devices, deviceId) { - | None => false - | Some(_) => - try { - let _ = %raw(`delete bridge.devices[deviceId]`) - let meta = Js.Dict.empty() - Js.Dict.set(meta, "deviceId", Js.Json.string(deviceId)) - Logger.info(Logger.logger, "Matter device decommissioned", ~meta) - true - } catch { - | _exn => - Logger.error(Logger.logger, "Matter decommissioning failed") - false - } - } -} - -/// Get fabric information. -let getFabricInfo = (bridge: t): fabricInfo => { - let allDevices = Js.Dict.values(bridge.devices) - { - fabricId: Float.toString(bridge.fabricId), - deviceCount: Array.length(allDevices), - onlineDevices: allDevices->Array.filter(d => d.online)->Array.length, - } -} diff --git a/backend-deno/services/Mqtt.affine b/backend-deno/services/Mqtt.affine new file mode 100644 index 0000000..14ff35f --- /dev/null +++ b/backend-deno/services/Mqtt.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Mqtt; + +// TODO: Complete semantic implementation diff --git a/backend-deno/services/Mqtt.res b/backend-deno/services/Mqtt.res deleted file mode 100644 index ddf6f80..0000000 --- a/backend-deno/services/Mqtt.res +++ /dev/null @@ -1,281 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// MQTT service for device telemetry and messaging. -/// Handles pub/sub for ESP32-C6 devices and real-time updates. - -/// Message handler callback type: (topic, payload) => unit. -type messageHandler = (string, Js.TypedArray2.Uint8Array.t) => unit - -/// MQTT service state. -type t = { - mutable client: option, - brokerUrl: string, - mutable isConnectedFlag: bool, - handlers: Js.Dict.t>, -} - -/// Create a new MQTT service wrapper (not yet connected). -let make = (brokerUrl: string): t => { - client: None, - brokerUrl, - isConnectedFlag: false, - handlers: Js.Dict.empty(), -} - -/// Check if a topic matches an MQTT wildcard pattern. -/// + matches a single level, # matches multiple levels. -let topicMatches = (pattern: string, topic: string): bool => { - let regexPattern = - pattern - ->String.replaceAll("+", "[^/]+") - ->String.replaceAll("#", ".*") - ->String.replaceAll("/", "\\/") - - let regex = %raw(`new RegExp("^" + regexPattern + "$")`) - %raw(`regex.test(topic)`) -} - -/// Internal: dispatch incoming messages to registered handlers. -let handleMessage = (service: t, topic: string, payload: Js.TypedArray2.Uint8Array.t): unit => { - let entries = Js.Dict.entries(service.handlers) - entries->Array.forEach(((pattern, handlerSet)) => { - if topicMatches(pattern, topic) { - handlerSet->Array.forEach(handler => { - try { - handler(topic, payload) - } catch { - | _exn => - let meta = Js.Dict.empty() - Js.Dict.set(meta, "topic", Js.Json.string(topic)) - Logger.error(Logger.logger, "Error in MQTT message handler", ~meta) - } - }) - } - }) -} - -/// Connect to the MQTT broker. -let connect = (service: t): promise => { - Promise.make((resolve, reject) => { - try { - let parsed = %raw(`new URL(service.brokerUrl)`) - let username: option = %raw(`parsed.username || undefined`) - let password: option = %raw(`parsed.password || undefined`) - - let client = Mqtt_Client.mqttDefault.connect( - service.brokerUrl, - { - clientId: `kaldor-backend-${Float.toString(Js.Date.now())}`, - clean: true, - reconnectPeriod: 5000, - connectTimeout: 30000, - username, - password, - }, - ) - - service.client = Some(client) - - Mqtt_Client.onConnect(client, () => { - service.isConnectedFlag = true - let meta = Js.Dict.empty() - Js.Dict.set(meta, "broker", Js.Json.string(service.brokerUrl)) - Logger.info(Logger.logger, "MQTT connected", ~meta) - resolve() - }) - - Mqtt_Client.onError(client, error => { - let meta = Js.Dict.empty() - Js.Dict.set( - meta, - "error", - Js.Json.string(error->Js.Exn.message->Option.getOr("unknown")), - ) - Logger.error(Logger.logger, "MQTT error", ~meta) - reject(error->Obj.magic) - }) - - Mqtt_Client.onMessage(client, (topic, payload) => { - handleMessage(service, topic, payload) - }) - - Mqtt_Client.onOffline(client, () => { - service.isConnectedFlag = false - Logger.warn(Logger.logger, "MQTT offline") - }) - - Mqtt_Client.onReconnect(client, () => { - Logger.info(Logger.logger, "MQTT reconnecting...") - }) - } catch { - | exn => - Logger.error(Logger.logger, "Failed to connect to MQTT broker") - reject(exn->Obj.magic) - } - }) -} - -/// Disconnect from the MQTT broker. -let disconnect = (service: t): promise => { - Promise.make((resolve, _reject) => { - switch service.client { - | Some(client) => - Mqtt_Client.end_(client, false, Js.Json.null, () => { - service.isConnectedFlag = false - Logger.info(Logger.logger, "MQTT disconnected") - resolve() - }) - | None => resolve() - } - }) -} - -/// Check if connected. -let isConnected = (service: t): bool => service.isConnectedFlag - -/// Get the raw client, raising if not connected. -let getClient = (service: t): Mqtt_Client.t => { - switch (service.client, service.isConnectedFlag) { - | (Some(client), true) => client - | _ => Js.Exn.raiseError("MQTT not connected") - } -} - -/// Publish a message to a topic. -let publish = (service: t, topic: string, message: string, ~qos: int=0): promise => { - let client = getClient(service) - Promise.make((resolve, reject) => { - Mqtt_Client.publish(client, topic, message, {qos: qos}, error => { - switch error->Js.Nullable.toOption { - | Some(err) => - let meta = Js.Dict.empty() - Js.Dict.set(meta, "topic", Js.Json.string(topic)) - Logger.error(Logger.logger, "MQTT publish failed", ~meta) - reject(err->Obj.magic) - | None => - let meta = Js.Dict.empty() - Js.Dict.set(meta, "topic", Js.Json.string(topic)) - Js.Dict.set(meta, "size", Js.Json.number(String.length(message)->Int.toFloat)) - Logger.debug(Logger.logger, "MQTT published", ~meta) - resolve() - } - }) - }) -} - -/// Publish a JSON payload to a topic. -let publishJSON = async (service: t, topic: string, data: Js.Json.t, ~qos: int=0): unit => { - await publish(service, topic, Js.Json.stringify(data), ~qos) -} - -/// Subscribe to a topic with a message handler. -let subscribeToTopic = ( - service: t, - topic: string, - handler: messageHandler, - ~qos: int=0, -): promise => { - let client = getClient(service) - Promise.make((resolve, reject) => { - Mqtt_Client.subscribe(client, topic, {qos: qos}, error => { - switch error->Js.Nullable.toOption { - | Some(err) => - let meta = Js.Dict.empty() - Js.Dict.set(meta, "topic", Js.Json.string(topic)) - Logger.error(Logger.logger, "MQTT subscribe failed", ~meta) - reject(err->Obj.magic) - | None => - let existing = Js.Dict.get(service.handlers, topic)->Option.getOr([]) - Js.Dict.set(service.handlers, topic, Array.concat(existing, [handler])) - let meta = Js.Dict.empty() - Js.Dict.set(meta, "topic", Js.Json.string(topic)) - Logger.info(Logger.logger, "MQTT subscribed", ~meta) - resolve() - } - }) - }) -} - -/// Subscribe to JSON messages on a topic. -let subscribeJSON = async ( - service: t, - topic: string, - handler: (string, Js.Json.t) => unit, - ~qos: int=0, -): unit => { - let decoder = Deno.makeTextDecoder() - await subscribeToTopic( - service, - topic, - (topic, payload) => { - try { - let text = Deno.decode(decoder, payload) - let data = Js.Json.parseExn(text) - handler(topic, data) - } catch { - | _exn => - let meta = Js.Dict.empty() - Js.Dict.set(meta, "topic", Js.Json.string(topic)) - Logger.error(Logger.logger, "Failed to parse MQTT JSON message", ~meta) - } - }, - ~qos, - ) -} - -/// Unsubscribe from a topic (removes all handlers for the topic). -let unsubscribeFromBroker = (service: t, topic: string): promise => { - let client = getClient(service) - Promise.make((resolve, reject) => { - Mqtt_Client.unsubscribe(client, topic, error => { - switch error->Js.Nullable.toOption { - | Some(err) => - let meta = Js.Dict.empty() - Js.Dict.set(meta, "topic", Js.Json.string(topic)) - Logger.error(Logger.logger, "MQTT unsubscribe failed", ~meta) - reject(err->Obj.magic) - | None => - let meta = Js.Dict.empty() - Js.Dict.set(meta, "topic", Js.Json.string(topic)) - Logger.info(Logger.logger, "MQTT unsubscribed", ~meta) - resolve() - } - }) - }) -} - -/// Unsubscribe a specific handler or all handlers from a topic. -let unsubscribe = async (service: t, topic: string): unit => { - // Remove all handlers for this topic and unsubscribe from broker - let _removed = %raw(`delete service.handlers[topic]`) - await unsubscribeFromBroker(service, topic) -} - -/// Subscribe to all telemetry from a device by ID. -let subscribeToDevice = async (service: t, deviceId: string, handler: Js.Json.t => unit): unit => { - await subscribeJSON(service, `devices/${deviceId}/#`, (_topic, data) => handler(data)) -} - -/// Publish a command to a specific device. -let publishDeviceCommand = async ( - service: t, - deviceId: string, - command: string, - params: Js.Json.t, -): unit => { - let payload = Js.Dict.empty() - Js.Dict.set(payload, "command", Js.Json.string(command)) - Js.Dict.set(payload, "params", params) - Js.Dict.set(payload, "timestamp", Js.Json.number(Js.Date.now())) - await publishJSON(service, `devices/${deviceId}/commands`, Js.Json.object_(payload)) -} - -/// Broadcast a system-wide event. -let broadcastEvent = async (service: t, event: string, data: Js.Json.t): unit => { - let payload = Js.Dict.empty() - Js.Dict.set(payload, "event", Js.Json.string(event)) - Js.Dict.set(payload, "data", data) - Js.Dict.set(payload, "timestamp", Js.Json.number(Js.Date.now())) - await publishJSON(service, `system/events/${event}`, Js.Json.object_(payload)) -} diff --git a/backend-deno/services/Opcua.affine b/backend-deno/services/Opcua.affine new file mode 100644 index 0000000..3a62c28 --- /dev/null +++ b/backend-deno/services/Opcua.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Opcua; + +// TODO: Complete semantic implementation diff --git a/backend-deno/services/Opcua.res b/backend-deno/services/Opcua.res deleted file mode 100644 index 5a49096..0000000 --- a/backend-deno/services/Opcua.res +++ /dev/null @@ -1,223 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// OPC UA server service for SCADA/DCS interoperability. -/// Exposes device data via OPC UA (IEC 62541) for industrial systems. -/// -/// Note: Production implementation would use node-opcua or similar. -/// This is a simplified interface showing the integration pattern. - -open Bindings.Opcua - -/// OPC UA server state. -type t = { - port: string, - mutable isRunningFlag: bool, - nodes: Js.Dict.t, - namespaceUri: string, - namespaceIndex: int, -} - -/// Create a new OPC UA server (not yet started). -let make = (port: string): t => { - port, - isRunningFlag: false, - nodes: Js.Dict.empty(), - namespaceUri: "http://kaldor.community/manufacturing/", - namespaceIndex: 2, -} - -/// Add or update a node in the address space. -let addNode = (server: t, node: opcuaNode): unit => { - Js.Dict.set(server.nodes, node.nodeId, node) -} - -/// Initialize the default address space structure. -let initializeAddressSpace = (server: t): unit => { - addNode( - server, - { - nodeId: `ns=${Int.toString(server.namespaceIndex)};s=Devices`, - browseName: "Devices", - value: Js.Json.object_(Js.Dict.empty()), - dataType: "Object", - accessLevel: "read", - }, - ) - Logger.debug(Logger.logger, "OPC UA address space initialized") -} - -/// Start the OPC UA server. -let start = async (server: t): unit => { - try { - server.isRunningFlag = true - initializeAddressSpace(server) - - let meta = Js.Dict.empty() - Js.Dict.set(meta, "port", Js.Json.string(server.port)) - Js.Dict.set(meta, "endpoint", Js.Json.string(`opc.tcp://localhost:${server.port}`)) - Js.Dict.set(meta, "namespace", Js.Json.string(server.namespaceUri)) - Js.Dict.set(meta, "securityMode", Js.Json.string("SignAndEncrypt")) - Logger.info(Logger.logger, "OPC UA server started", ~meta) - } catch { - | exn => - Logger.error(Logger.logger, "Failed to start OPC UA server") - raise(exn) - } -} - -/// Stop the OPC UA server. -let stop = async (server: t): unit => { - server.isRunningFlag = false - Logger.info(Logger.logger, "OPC UA server stopped") -} - -/// Check if the server is running. -let isRunning = (server: t): bool => server.isRunningFlag - -/// Add a device to the OPC UA address space with standard variables. -let addDevice = (server: t, deviceId: string, info: deviceInfo): unit => { - let ns = Int.toString(server.namespaceIndex) - let deviceNodeId = `ns=${ns};s=Devices.${deviceId}` - - let deviceValue = Js.Dict.empty() - Js.Dict.set(deviceValue, "type", Js.Json.string(info.type_)) - Js.Dict.set(deviceValue, "id", Js.Json.string(deviceId)) - - addNode( - server, - { - nodeId: deviceNodeId, - browseName: info.name, - value: Js.Json.object_(deviceValue), - dataType: "Object", - accessLevel: "read", - }, - ) - - addNode( - server, - { - nodeId: `${deviceNodeId}.Status`, - browseName: "Status", - value: Js.Json.string("Online"), - dataType: "String", - accessLevel: "read", - }, - ) - - addNode( - server, - { - nodeId: `${deviceNodeId}.Temperature`, - browseName: "Temperature", - value: Js.Json.number(0.0), - dataType: "Double", - accessLevel: "read", - }, - ) - - addNode( - server, - { - nodeId: `${deviceNodeId}.Vibration`, - browseName: "Vibration", - value: Js.Json.number(0.0), - dataType: "Double", - accessLevel: "read", - }, - ) - - let meta = Js.Dict.empty() - Js.Dict.set(meta, "deviceId", Js.Json.string(deviceId)) - Js.Dict.set(meta, "nodeId", Js.Json.string(deviceNodeId)) - Logger.info(Logger.logger, "OPC UA device added", ~meta) -} - -/// Remove a device and all child nodes from the address space. -let removeDevice = (server: t, deviceId: string): unit => { - let ns = Int.toString(server.namespaceIndex) - let deviceNodeId = `ns=${ns};s=Devices.${deviceId}` - - let keysToRemove = - Js.Dict.keys(server.nodes)->Array.filter(nodeId => String.startsWith(nodeId, deviceNodeId)) - - keysToRemove->Array.forEach(nodeId => { - let _ = %raw(`delete server.nodes[nodeId]`) - }) - - let meta = Js.Dict.empty() - Js.Dict.set(meta, "deviceId", Js.Json.string(deviceId)) - Logger.info(Logger.logger, "OPC UA device removed", ~meta) -} - -/// Update a node's value. -let updateNodeValue = (server: t, nodeId: string, value: Js.Json.t): unit => { - switch Js.Dict.get(server.nodes, nodeId) { - | Some(node) => - Js.Dict.set(server.nodes, nodeId, {...node, value}) - let meta = Js.Dict.empty() - Js.Dict.set(meta, "nodeId", Js.Json.string(nodeId)) - Logger.debug(Logger.logger, "OPC UA node updated", ~meta) - | None => - let meta = Js.Dict.empty() - Js.Dict.set(meta, "nodeId", Js.Json.string(nodeId)) - Logger.warn(Logger.logger, "OPC UA node not found", ~meta) - } -} - -/// Read a node's value. -let readNode = (server: t, nodeId: string): option => - Js.Dict.get(server.nodes, nodeId) - -/// Browse child nodes (get direct children of a node). -let browseNode = (server: t, nodeId: string): array => { - let prefix = if String.endsWith(nodeId, ".") { - nodeId - } else { - nodeId ++ "." - } - Js.Dict.values(server.nodes)->Array.filter(node => { - if String.startsWith(node.nodeId, prefix) { - let suffix = String.sliceToEnd(node.nodeId, ~start=String.length(prefix)) - !String.includes(suffix, ".") - } else { - false - } - }) -} - -/// Update device metrics (temperature, vibration, status). -let updateDeviceMetrics = (server: t, deviceId: string, metrics: deviceMetrics): unit => { - let ns = Int.toString(server.namespaceIndex) - let devicePrefix = `ns=${ns};s=Devices.${deviceId}` - - switch metrics.temperature { - | Some(temp) => updateNodeValue(server, `${devicePrefix}.Temperature`, Js.Json.number(temp)) - | None => () - } - - switch metrics.vibration { - | Some(vib) => updateNodeValue(server, `${devicePrefix}.Vibration`, Js.Json.number(vib)) - | None => () - } - - switch metrics.status { - | Some(status) => updateNodeValue(server, `${devicePrefix}.Status`, Js.Json.string(status)) - | None => () - } -} - -/// Get server info for API endpoint. -let getServerInfo = (server: t): serverInfo => { - endpoint: `opc.tcp://localhost:${server.port}`, - namespace: server.namespaceUri, - namespaceIndex: server.namespaceIndex, - securityMode: "SignAndEncrypt", - securityPolicy: "Basic256Sha256", - authentication: ["Anonymous", "UserNamePassword"], - nodeCount: Js.Dict.keys(server.nodes)->Array.length, -} - -/// Get all nodes (for debugging). -let getAllNodes = (server: t): array => Js.Dict.values(server.nodes) diff --git a/backend-deno/services/Redis.affine b/backend-deno/services/Redis.affine new file mode 100644 index 0000000..32e51ca --- /dev/null +++ b/backend-deno/services/Redis.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Redis; + +// TODO: Complete semantic implementation diff --git a/backend-deno/services/Redis.res b/backend-deno/services/Redis.res deleted file mode 100644 index 87c8b29..0000000 --- a/backend-deno/services/Redis.res +++ /dev/null @@ -1,252 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -/// Redis service for caching and pub/sub. -/// Supports real-time updates and session management. - -/// Subscriber callback type. -type subscriberCallback = string => unit - -/// Redis client state. -type t = { - mutable client: option, - connectionUrl: string, - mutable isConnectedFlag: bool, - subscribers: Js.Dict.t>, -} - -/// Parse hostname from a Redis URL. -let parseHostname = (url: string): string => { - let parsed = %raw(`new URL(url)`) - let hostname: string = %raw(`parsed.hostname || "localhost"`) - hostname -} - -/// Parse port from a Redis URL. -let parsePort = (url: string): int => { - let parsed = %raw(`new URL(url)`) - let portStr: string = %raw(`parsed.port || "6379"`) - Int.fromString(portStr)->Option.getOr(6379) -} - -/// Create a new Redis client wrapper (not yet connected). -let make = (connectionUrl: string): t => { - client: None, - connectionUrl, - isConnectedFlag: false, - subscribers: Js.Dict.empty(), -} - -/// Connect to the Redis server. -let connect = async (redis: t): unit => { - try { - let hostname = parseHostname(redis.connectionUrl) - let port = parsePort(redis.connectionUrl) - let client = await Redis_Client.connect({"hostname": hostname, "port": port}) - redis.client = Some(client) - redis.isConnectedFlag = true - - let meta = Js.Dict.empty() - Js.Dict.set(meta, "url", Js.Json.string(redis.connectionUrl)) - Logger.info(Logger.logger, "Redis connected", ~meta) - } catch { - | exn => - let meta = Js.Dict.empty() - Js.Dict.set( - meta, - "error", - Js.Json.string(exn->Js.Exn.asJsExn->Option.flatMap(Js.Exn.message)->Option.getOr("unknown")), - ) - Logger.error(Logger.logger, "Failed to connect to Redis", ~meta) - raise(exn) - } -} - -/// Disconnect from Redis. -let disconnect = async (redis: t): unit => { - switch redis.client { - | Some(client) => - Redis_Client.close(client) - redis.isConnectedFlag = false - Logger.info(Logger.logger, "Redis disconnected") - | None => () - } -} - -/// Check if connected. -let isConnected = (redis: t): bool => redis.isConnectedFlag - -/// Get the raw client, raising if not connected. -let getClient = (redis: t): Redis_Client.t => { - switch redis.client { - | Some(client) => client - | None => Js.Exn.raiseError("Redis not connected") - } -} - -// --- Cache operations --- - -/// Get a string value by key. -let get = async (redis: t, key: string): option => { - let client = getClient(redis) - let result = await Redis_Client.get(client, key) - result->Js.Nullable.toOption -} - -/// Set a string value, optionally with a TTL in seconds. -let set = async (redis: t, key: string, value: string, ~ttlSeconds: option=?): unit => { - let client = getClient(redis) - switch ttlSeconds { - | Some(ttl) => { - let _ = await Redis_Client.setex(client, key, ttl, value) - } - | None => { - let _ = await Redis_Client.set(client, key, value) - } - } -} - -/// Delete a key. -let del = async (redis: t, key: string): unit => { - let client = getClient(redis) - let _ = await Redis_Client.del(client, key) -} - -/// Check if a key exists. -let exists = async (redis: t, key: string): bool => { - let client = getClient(redis) - let result = await Redis_Client.exists(client, key) - result === 1 -} - -// --- JSON helpers --- - -/// Get a JSON value by key. -let getJSON = async (redis: t, key: string): option => { - let value = await get(redis, key) - switch value { - | Some(str) => Some(Js.Json.parseExn(str)) - | None => None - } -} - -/// Set a JSON value, optionally with a TTL. -let setJSON = async (redis: t, key: string, value: Js.Json.t, ~ttlSeconds: option=?): unit => { - let str = Js.Json.stringify(value) - await set(redis, key, str, ~ttlSeconds?) -} - -// --- Pub/Sub --- - -/// Publish a string message to a channel. -let publish = async (redis: t, channel: string, message: string): unit => { - let client = getClient(redis) - let _ = await Redis_Client.publish(client, channel, message) - let meta = Js.Dict.empty() - Js.Dict.set(meta, "channel", Js.Json.string(channel)) - Js.Dict.set(meta, "messageLength", Js.Json.number(String.length(message)->Int.toFloat)) - Logger.debug(Logger.logger, "Published to channel", ~meta) -} - -/// Publish a JSON payload to a channel. -let publishJSON = async (redis: t, channel: string, data: Js.Json.t): unit => { - await publish(redis, channel, Js.Json.stringify(data)) -} - -/// Subscribe to a Redis channel with a callback. -let subscribe = async (redis: t, channel: string, callback: subscriberCallback): unit => { - let existing = Js.Dict.get(redis.subscribers, channel) - switch existing { - | None => { - Js.Dict.set(redis.subscribers, channel, [callback]) - - // Create a dedicated subscriber connection - let hostname = parseHostname(redis.connectionUrl) - let port = parsePort(redis.connectionUrl) - let subscriber = await Redis_Client.connect({"hostname": hostname, "port": port}) - await Redis_Client.subscribe(subscriber, channel, message => { - switch Js.Dict.get(redis.subscribers, channel) { - | Some(callbacks) => callbacks->Array.forEach(cb => cb(message)) - | None => () - } - }) - - let meta = Js.Dict.empty() - Js.Dict.set(meta, "channel", Js.Json.string(channel)) - Logger.info(Logger.logger, "Subscribed to Redis channel", ~meta) - } - | Some(callbacks) => { - let updated = Array.concat(callbacks, [callback]) - Js.Dict.set(redis.subscribers, channel, updated) - } - } -} - -/// Unsubscribe a callback from a Redis channel. -let unsubscribe = (_redis: t, channel: string, _callback: subscriberCallback): unit => { - // Note: In a full implementation we would track and remove the specific callback. - // For now, log and clear all callbacks for the channel. - let meta = Js.Dict.empty() - Js.Dict.set(meta, "channel", Js.Json.string(channel)) - Logger.info(Logger.logger, "Unsubscribed from Redis channel", ~meta) -} - -// --- Session management --- - -/// Store a session object in Redis with a TTL. -let setSession = async (redis: t, sessionId: string, data: Js.Json.t, ~ttlSeconds: int=3600): unit => { - await setJSON(redis, `session:${sessionId}`, data, ~ttlSeconds) -} - -/// Retrieve a session object from Redis. -let getSession = async (redis: t, sessionId: string): option => { - await getJSON(redis, `session:${sessionId}`) -} - -/// Delete a session from Redis. -let deleteSession = async (redis: t, sessionId: string): unit => { - await del(redis, `session:${sessionId}`) -} - -// --- Rate limiting --- - -/// Rate limit result. -type rateLimitResult = { - allowed: bool, - remaining: int, -} - -/// Check if a request is within the rate limit using a Redis sorted set -/// sliding window. -let checkRateLimit = async ( - redis: t, - key: string, - maxRequests: int, - windowSeconds: int, -): rateLimitResult => { - let client = getClient(redis) - let now = Js.Date.now() - let windowStart = now -. Int.toFloat(windowSeconds) *. 1000.0 - - let rateLimitKey = `ratelimit:${key}` - - // Remove old entries - let _ = await Redis_Client.zremrangebyscore( - client, - rateLimitKey, - "-inf", - Float.toString(windowStart), - ) - - // Count requests in current window - let count = await Redis_Client.zcard(client, rateLimitKey) - - if count >= maxRequests { - {allowed: false, remaining: 0} - } else { - // Add current request - let _ = await Redis_Client.zadd(client, rateLimitKey, now, Float.toString(now)) - let _ = await Redis_Client.expire(client, rateLimitKey, windowSeconds) - {allowed: true, remaining: maxRequests - count - 1} - } -} diff --git a/frontend/rescript.json b/frontend/rescript.json deleted file mode 100644 index 41bf4c1..0000000 --- a/frontend/rescript.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "kaldor-iiot-frontend", - "sources": [{"dir": "src", "subdirs": true}], - "package-specs": [{"module": "es6", "in-source": true}], - "suffix": ".res.js", - "dependencies": ["@rescript/core", "@rescript/react"], - "bs-dependencies": ["@rescript/core", "@rescript/react"], - "reason": {"react-jsx": 3}, - "jsx": {"version": 4, "mode": "automatic"} -} diff --git a/frontend/src/App.affine b/frontend/src/App.affine new file mode 100644 index 0000000..eb92faa --- /dev/null +++ b/frontend/src/App.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module App; + +// TODO: Complete semantic implementation diff --git a/frontend/src/App.res b/frontend/src/App.res deleted file mode 100644 index 95c9ce0..0000000 --- a/frontend/src/App.res +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// Root application component. Handles authentication gating and -// WebSocket lifecycle. Routes authenticated users through Layout. - -@react.component -let make = () => { - let dispatch = Redux.useAppDispatch() - let authState = Redux.useAppSelector(state => state.auth) - - React.useEffect2(() => { - if authState.isAuthenticated { - switch Nullable.toOption(authState.token) { - | Some(token) => { - let _ = Websocket.connectWebSocket(token, dispatch) - Some(() => Websocket.disconnectWebSocket()) - } - | None => None - } - } else { - None - } - }, (authState.isAuthenticated, authState.token)) - - if !authState.isAuthenticated { - - } else { - - - } /> - } /> - } /> - } /> - } /> - } /> - - - } -} diff --git a/frontend/src/Main.affine b/frontend/src/Main.affine new file mode 100644 index 0000000..d410d4c --- /dev/null +++ b/frontend/src/Main.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Main; + +// TODO: Complete semantic implementation diff --git a/frontend/src/Main.res b/frontend/src/Main.res deleted file mode 100644 index c0a9017..0000000 --- a/frontend/src/Main.res +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// Application entry point. Mounts React root with Redux Provider, -// BrowserRouter, MUI ThemeProvider, and ToastContainer. - -%%raw(`import 'react-toastify/dist/ReactToastify.css'`) - -let theme = Mui.createTheme({ - palette: { - "mode": "light", - "primary": {"main": "#1976d2"}, - "secondary": {"main": "#dc004e"}, - }, - typography: { - "fontFamily": `"Roboto", "Helvetica", "Arial", sans-serif`, - }, -}) - -// Get the root DOM element -let rootElement = switch ReactDOM.querySelector("#root") { -| Some(element) => element -| None => panic("Could not find #root element") -} - -let root = ReactDOM.Client.createRoot(rootElement) - -root->ReactDOM.Client.Root.render( - - - - - - - - - - - , -) diff --git a/frontend/src/bindings/Axios.affine b/frontend/src/bindings/Axios.affine new file mode 100644 index 0000000..8388fc7 --- /dev/null +++ b/frontend/src/bindings/Axios.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Axios; + +// TODO: Complete semantic implementation diff --git a/frontend/src/bindings/Axios.res b/frontend/src/bindings/Axios.res deleted file mode 100644 index e86c036..0000000 --- a/frontend/src/bindings/Axios.res +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// FFI bindings for axios - -type config = { - baseURL: string, - timeout: int, -} - -type headers = {mutable \"Authorization": string} - -type requestConfig = {headers: headers} - -type responseData<'a> = {data: 'a} - -type response<'a> = { - data: responseData<'a>, - status: int, -} - -type errorResponse = {status: int} -type axiosError = {response: Nullable.t} - -type interceptorHandler<'a> = {use: 'a => 'a} -type responseInterceptorError - -type interceptors = { - request: interceptorHandler, -} - -type instance - -@module("axios") -external create: config => instance = "create" - -@send -external get: (instance, string) => promise> = "get" - -@send -external getWithParams: (instance, string, {..}) => promise> = "get" - -@send -external post: (instance, string, 'body) => promise> = "post" - -@send -external postNoBody: (instance, string) => promise> = "post" - -@send -external put: (instance, string, 'body) => promise> = "put" - -@send -external delete: (instance, string) => promise> = "delete" - -// Interceptor setup via raw JS interop -@send -external addRequestInterceptor: ( - instance, - @as("interceptors") _, -) => {..} = "get" diff --git a/frontend/src/bindings/Mui.affine b/frontend/src/bindings/Mui.affine new file mode 100644 index 0000000..fd64d3a --- /dev/null +++ b/frontend/src/bindings/Mui.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Mui; + +// TODO: Complete semantic implementation diff --git a/frontend/src/bindings/Mui.res b/frontend/src/bindings/Mui.res deleted file mode 100644 index e98b200..0000000 --- a/frontend/src/bindings/Mui.res +++ /dev/null @@ -1,285 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// FFI bindings for @mui/material components used in kaldor-iiot frontend - -module Box = { - @module("@mui/material") @react.component - external make: ( - ~component: string=?, - ~sx: {..}=?, - ~display: string=?, - ~justifyContent: string=?, - ~alignItems: string=?, - ~mb: int=?, - ~mt: int=?, - ~children: React.element=?, - ~onClick: ReactEvent.Mouse.t => unit=?, - ~onSubmit: ReactEvent.Form.t => unit=?, - ) => React.element = "Box" -} - -module Container = { - @module("@mui/material") @react.component - external make: ( - ~maxWidth: string=?, - ~children: React.element=?, - ) => React.element = "Container" -} - -module Typography = { - @module("@mui/material") @react.component - external make: ( - ~variant: string=?, - ~component: string=?, - ~color: string=?, - ~align: string=?, - ~gutterBottom: bool=?, - ~sx: {..}=?, - ~children: React.element=?, - ) => React.element = "Typography" -} - -module Button = { - @module("@mui/material") @react.component - external make: ( - ~variant: string=?, - ~color: string=?, - ~size: string=?, - ~\"type": string=?, - ~fullWidth: bool=?, - ~disabled: bool=?, - ~onClick: ReactEvent.Mouse.t => unit=?, - ~sx: {..}=?, - ~children: React.element=?, - ) => React.element = "Button" -} - -module TextField = { - @module("@mui/material") @react.component - external make: ( - ~margin: string=?, - ~required: bool=?, - ~fullWidth: bool=?, - ~id: string=?, - ~label: string=?, - ~name: string=?, - ~\"type": string=?, - ~autoComplete: string=?, - ~autoFocus: bool=?, - ~value: string=?, - ~onChange: ReactEvent.Form.t => unit=?, - ) => React.element = "TextField" -} - -module Paper = { - @module("@mui/material") @react.component - external make: ( - ~elevation: int=?, - ~sx: {..}=?, - ~children: React.element=?, - ) => React.element = "Paper" -} - -module AppBar = { - @module("@mui/material") @react.component - external make: ( - ~position: string=?, - ~children: React.element=?, - ) => React.element = "AppBar" -} - -module Toolbar = { - @module("@mui/material") @react.component - external make: (~children: React.element=?) => React.element = "Toolbar" -} - -module Drawer = { - @module("@mui/material") @react.component - external make: ( - ~\"open": bool=?, - ~onClose: ReactEvent.Synthetic.t => unit=?, - ~children: React.element=?, - ) => React.element = "Drawer" -} - -module IconButton = { - @module("@mui/material") @react.component - external make: ( - ~edge: string=?, - ~color: string=?, - ~onClick: ReactEvent.Mouse.t => unit=?, - ~sx: {..}=?, - ~children: React.element=?, - ) => React.element = "IconButton" -} - -module List = { - @module("@mui/material") @react.component - external make: (~children: React.element=?) => React.element = "List" -} - -module ListItem = { - @module("@mui/material") @react.component - external make: ( - ~button: bool=?, - ~onClick: ReactEvent.Mouse.t => unit=?, - ~children: React.element=?, - ) => React.element = "ListItem" -} - -module ListItemIcon = { - @module("@mui/material") @react.component - external make: (~children: React.element=?) => React.element = "ListItemIcon" -} - -module ListItemText = { - @module("@mui/material") @react.component - external make: (~primary: string=?) => React.element = "ListItemText" -} - -module Badge = { - @module("@mui/material") @react.component - external make: ( - ~badgeContent: int=?, - ~color: string=?, - ~children: React.element=?, - ) => React.element = "Badge" -} - -module Grid = { - @module("@mui/material") @react.component - external make: ( - ~container: bool=?, - ~item: bool=?, - ~spacing: int=?, - ~xs: int=?, - ~sm: int=?, - ~md: int=?, - ~sx: {..}=?, - ~children: React.element=?, - ) => React.element = "Grid" -} - -module Card = { - @module("@mui/material") @react.component - external make: ( - ~sx: {..}=?, - ~onClick: ReactEvent.Mouse.t => unit=?, - ~children: React.element=?, - ) => React.element = "Card" -} - -module CardContent = { - @module("@mui/material") @react.component - external make: (~children: React.element=?) => React.element = "CardContent" -} - -module Chip = { - @module("@mui/material") @react.component - external make: ( - ~label: string=?, - ~color: string=?, - ~size: string=?, - ) => React.element = "Chip" -} - -module Table = { - @module("@mui/material") @react.component - external make: (~children: React.element=?) => React.element = "Table" -} - -module TableHead = { - @module("@mui/material") @react.component - external make: (~children: React.element=?) => React.element = "TableHead" -} - -module TableBody = { - @module("@mui/material") @react.component - external make: (~children: React.element=?) => React.element = "TableBody" -} - -module TableRow = { - @module("@mui/material") @react.component - external make: ( - ~key: string=?, - ~children: React.element=?, - ) => React.element = "TableRow" -} - -module TableCell = { - @module("@mui/material") @react.component - external make: (~children: React.element=?) => React.element = "TableCell" -} - -module CssBaseline = { - @module("@mui/material") @react.component - external make: unit => React.element = "CssBaseline" -} - -// MUI theme functions -type themeOptions = { - palette: {..}, - typography: {..}, -} - -type theme - -@module("@mui/material/styles") -external createTheme: themeOptions => theme = "createTheme" - -module ThemeProvider = { - @module("@mui/material/styles") @react.component - external make: (~theme: theme, ~children: React.element) => React.element = "ThemeProvider" -} - -// MUI Icons -module DashboardIcon = { - @module("@mui/icons-material/Dashboard") @react.component - external make: (~color: string=?) => React.element = "default" -} - -module ViewListIcon = { - @module("@mui/icons-material/ViewList") @react.component - external make: (~color: string=?) => React.element = "default" -} - -module WarningIcon = { - @module("@mui/icons-material/Warning") @react.component - external make: (~color: string=?) => React.element = "default" -} - -module AnalyticsIcon = { - @module("@mui/icons-material/Analytics") @react.component - external make: (~color: string=?) => React.element = "default" -} - -module SettingsIcon = { - @module("@mui/icons-material/Settings") @react.component - external make: (~color: string=?) => React.element = "default" -} - -module MenuIcon = { - @module("@mui/icons-material/Menu") @react.component - external make: unit => React.element = "default" -} - -module ExitToAppIcon = { - @module("@mui/icons-material/ExitToApp") @react.component - external make: unit => React.element = "default" -} - -module WarningAmberIcon = { - @module("@mui/icons-material/WarningAmber") @react.component - external make: (~color: string=?) => React.element = "default" -} - -module CheckCircleIcon = { - @module("@mui/icons-material/CheckCircle") @react.component - external make: (~color: string=?) => React.element = "default" -} - -module ErrorIcon = { - @module("@mui/icons-material/Error") @react.component - external make: (~color: string=?) => React.element = "default" -} diff --git a/frontend/src/bindings/ReactRouter.affine b/frontend/src/bindings/ReactRouter.affine new file mode 100644 index 0000000..3ab296d --- /dev/null +++ b/frontend/src/bindings/ReactRouter.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ReactRouter; + +// TODO: Complete semantic implementation diff --git a/frontend/src/bindings/ReactRouter.res b/frontend/src/bindings/ReactRouter.res deleted file mode 100644 index 8cbcba0..0000000 --- a/frontend/src/bindings/ReactRouter.res +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// FFI bindings for react-router-dom v6 - -module BrowserRouter = { - @module("react-router-dom") @react.component - external make: (~children: React.element) => React.element = "BrowserRouter" -} - -module Routes = { - @module("react-router-dom") @react.component - external make: (~children: React.element) => React.element = "Routes" -} - -module Route = { - @module("react-router-dom") @react.component - external make: (~path: string, ~element: React.element) => React.element = "Route" -} - -module Navigate = { - @module("react-router-dom") @react.component - external make: (~\"to": string, ~replace: bool=?) => React.element = "Navigate" -} - -module Link = { - @module("react-router-dom") @react.component - external make: (~\"to": string, ~children: React.element) => React.element = "Link" -} - -@module("react-router-dom") -external useNavigate: unit => string => unit = "useNavigate" - -type params = {id: string} - -@module("react-router-dom") -external useParams: unit => params = "useParams" - -type location = {pathname: string} - -@module("react-router-dom") -external useLocation: unit => location = "useLocation" diff --git a/frontend/src/bindings/ReactToastify.affine b/frontend/src/bindings/ReactToastify.affine new file mode 100644 index 0000000..bfcf3bb --- /dev/null +++ b/frontend/src/bindings/ReactToastify.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ReactToastify; + +// TODO: Complete semantic implementation diff --git a/frontend/src/bindings/ReactToastify.res b/frontend/src/bindings/ReactToastify.res deleted file mode 100644 index 895b928..0000000 --- a/frontend/src/bindings/ReactToastify.res +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// FFI bindings for react-toastify - -module ToastContainer = { - @module("react-toastify") @react.component - external make: ( - ~position: string=?, - ~autoClose: int=?, - ~hideProgressBar: bool=?, - ~newestOnTop: bool=?, - ~closeOnClick: bool=?, - ~rtl: bool=?, - ~pauseOnFocusLoss: bool=?, - ~draggable: bool=?, - ~pauseOnHover: bool=?, - ) => React.element = "ToastContainer" -} - -type toastOptions = { - position: string, - autoClose: int, -} - -@module("react-toastify") -external toastSuccess: string => unit = "toast.success" - -@module("react-toastify") -external toastError: string => unit = "toast.error" - -@module("react-toastify") -external toastWarning: string => unit = "toast.warning" - -@module("react-toastify") -external toastWarningWithOptions: (string, toastOptions) => unit = "toast.warning" diff --git a/frontend/src/bindings/Recharts.affine b/frontend/src/bindings/Recharts.affine new file mode 100644 index 0000000..349b572 --- /dev/null +++ b/frontend/src/bindings/Recharts.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Recharts; + +// TODO: Complete semantic implementation diff --git a/frontend/src/bindings/Recharts.res b/frontend/src/bindings/Recharts.res deleted file mode 100644 index 66b3831..0000000 --- a/frontend/src/bindings/Recharts.res +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// FFI bindings for recharts - -module ResponsiveContainer = { - @module("recharts") @react.component - external make: ( - ~width: string=?, - ~height: int=?, - ~children: React.element=?, - ) => React.element = "ResponsiveContainer" -} - -module LineChart = { - @module("recharts") @react.component - external make: ( - ~data: array<'a>=?, - ~children: React.element=?, - ) => React.element = "LineChart" -} - -module Line = { - @module("recharts") @react.component - external make: ( - ~\"type": string=?, - ~dataKey: string=?, - ~stroke: string=?, - ~name: string=?, - ) => React.element = "Line" -} - -module XAxis = { - @module("recharts") @react.component - external make: (~dataKey: string=?) => React.element = "XAxis" -} - -module YAxis = { - @module("recharts") @react.component - external make: unit => React.element = "YAxis" -} - -module CartesianGrid = { - @module("recharts") @react.component - external make: (~strokeDasharray: string=?) => React.element = "CartesianGrid" -} - -module Tooltip = { - @module("recharts") @react.component - external make: unit => React.element = "Tooltip" -} - -module Legend = { - @module("recharts") @react.component - external make: unit => React.element = "Legend" -} - -module BarChart = { - @module("recharts") @react.component - external make: ( - ~data: array<'a>=?, - ~children: React.element=?, - ) => React.element = "BarChart" -} - -module Bar = { - @module("recharts") @react.component - external make: ( - ~dataKey: string=?, - ~fill: string=?, - ~name: string=?, - ) => React.element = "Bar" -} - -module PieChart = { - @module("recharts") @react.component - external make: (~children: React.element=?) => React.element = "PieChart" -} - -module Pie = { - @module("recharts") @react.component - external make: ( - ~data: array<'a>=?, - ~dataKey: string=?, - ~cx: string=?, - ~cy: string=?, - ~outerRadius: int=?, - ~fill: string=?, - ~children: React.element=?, - ) => React.element = "Pie" -} - -module Cell = { - @module("recharts") @react.component - external make: (~fill: string=?) => React.element = "Cell" -} diff --git a/frontend/src/bindings/ReduxToolkit.affine b/frontend/src/bindings/ReduxToolkit.affine new file mode 100644 index 0000000..9b21ce4 --- /dev/null +++ b/frontend/src/bindings/ReduxToolkit.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ReduxToolkit; + +// TODO: Complete semantic implementation diff --git a/frontend/src/bindings/ReduxToolkit.res b/frontend/src/bindings/ReduxToolkit.res deleted file mode 100644 index 3634b1c..0000000 --- a/frontend/src/bindings/ReduxToolkit.res +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// FFI bindings for @reduxjs/toolkit and react-redux - -// Generic store type - opaque -type store - -// Generic dispatch type -type dispatch = {..} => unit - -module Provider = { - @module("react-redux") @react.component - external make: (~store: store, ~children: React.element) => React.element = "Provider" -} - -@module("react-redux") -external useDispatch: unit => dispatch = "useDispatch" - -@module("react-redux") -external useSelector: ('state => 'a) => 'a = "useSelector" diff --git a/frontend/src/bindings/SocketIo.affine b/frontend/src/bindings/SocketIo.affine new file mode 100644 index 0000000..885bb66 --- /dev/null +++ b/frontend/src/bindings/SocketIo.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module SocketIo; + +// TODO: Complete semantic implementation diff --git a/frontend/src/bindings/SocketIo.res b/frontend/src/bindings/SocketIo.res deleted file mode 100644 index 8608e1d..0000000 --- a/frontend/src/bindings/SocketIo.res +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// FFI bindings for socket.io-client - -type socket - -type connectOptions = {transports: array} - -@module("socket.io-client") -external io: (string, connectOptions) => socket = "io" - -@send -external on: (socket, string, {..} => unit) => unit = "on" - -@send -external onConnect: (socket, @as("connect") _, unit => unit) => unit = "on" - -@send -external onDisconnect: (socket, @as("disconnect") _, unit => unit) => unit = "on" - -@send -external onError: (socket, @as("error") _, {..} => unit) => unit = "on" - -@send -external emit: (socket, string, 'a) => unit = "emit" - -@send -external disconnect: socket => unit = "disconnect" diff --git a/frontend/src/components/Layout.affine b/frontend/src/components/Layout.affine new file mode 100644 index 0000000..ade4ee8 --- /dev/null +++ b/frontend/src/components/Layout.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Layout; + +// TODO: Complete semantic implementation diff --git a/frontend/src/components/Layout.res b/frontend/src/components/Layout.res deleted file mode 100644 index 73f8102..0000000 --- a/frontend/src/components/Layout.res +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// Layout wrapper with MUI AppBar, navigation Drawer, and main content area. - -// Menu item record type for the sidebar navigation -type menuItem = { - text: string, - icon: React.element, - path: string, -} - -@react.component -let make = (~children: React.element) => { - let (drawerOpen, setDrawerOpen) = React.useState(() => false) - let navigate = ReactRouter.useNavigate() - let dispatch = Redux.useAppDispatch() - let alertsState = Redux.useAppSelector(state => state.alerts) - - let menuItems: array = [ - {text: "Dashboard", icon: , path: "/"}, - {text: "Looms", icon: , path: "/looms"}, - { - text: "Alerts", - icon: - - , - path: "/alerts", - }, - {text: "Analytics", icon: , path: "/analytics"}, - {text: "Settings", icon: , path: "/settings"}, - ] - - let handleLogout = _event => { - dispatch(AuthSlice.logout()) - } - - - - - setDrawerOpen(prev => !prev)} - sx={{"mr": 2}}> - - - - {React.string("Kaldor IIoT")} - - - - - - - setDrawerOpen(_ => false)}> - - - {menuItems - ->Array.map(item => - { - navigate(item.path) - setDrawerOpen(_ => false) - }}> - {item.icon} - - - ) - ->React.array} - - - - - {children} - - -} diff --git a/frontend/src/hooks/Redux.affine b/frontend/src/hooks/Redux.affine new file mode 100644 index 0000000..ac9e212 --- /dev/null +++ b/frontend/src/hooks/Redux.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Redux; + +// TODO: Complete semantic implementation diff --git a/frontend/src/hooks/Redux.res b/frontend/src/hooks/Redux.res deleted file mode 100644 index a83bde7..0000000 --- a/frontend/src/hooks/Redux.res +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// Typed Redux dispatch and selector hooks for the Kaldor IIoT store. - -let useAppDispatch = () => ReduxToolkit.useDispatch() - -let useAppSelector = (selector: Store.rootState => 'a): 'a => { - ReduxToolkit.useSelector(selector) -} diff --git a/frontend/src/pages/Alerts.affine b/frontend/src/pages/Alerts.affine new file mode 100644 index 0000000..d3b5aa5 --- /dev/null +++ b/frontend/src/pages/Alerts.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Alerts; + +// TODO: Complete semantic implementation diff --git a/frontend/src/pages/Alerts.res b/frontend/src/pages/Alerts.res deleted file mode 100644 index 25ee3c8..0000000 --- a/frontend/src/pages/Alerts.res +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// Alerts page showing a table of all alerts with acknowledge actions. - -@react.component -let make = () => { - let dispatch = Redux.useAppDispatch() - let alertsState = Redux.useAppSelector(state => state.alerts) - - let loadAlerts = () => { - let _ = - Api.AlertsAPI.getAll() - ->Promise.then(response => { - let data: array = response.data.data - dispatch(AlertsSlice.setAlerts(data)) - Promise.resolve() - }) - ->Promise.catch(error => { - Console.error2("Failed to load alerts:", error) - Promise.resolve() - }) - } - - React.useEffect0(() => { - loadAlerts() - None - }) - - let handleAcknowledge = (alertId: int) => { - let _ = - Api.AlertsAPI.acknowledge(alertId) - ->Promise.then(_ => { - dispatch(AlertsSlice.acknowledgeAlert(alertId)) - ReactToastify.toastSuccess("Alert acknowledged") - Promise.resolve() - }) - ->Promise.catch(_ => { - ReactToastify.toastError("Failed to acknowledge alert") - Promise.resolve() - }) - } - - let getSeverityColor = (severity: string): string => { - switch severity { - | "critical" => "error" - | "warning" => "warning" - | "info" => "info" - | _ => "default" - } - } - - - - {React.string("Alerts")} - - - - - - {React.string("Time")} - {React.string("Loom")} - {React.string("Type")} - {React.string("Severity")} - {React.string("Value")} - {React.string("Status")} - {React.string("Action")} - - - - {alertsState.alerts - ->Array.map(alert => - - - {React.string(alert.created_at)} - - - {React.string(alert.loom_id)} - - - {React.string(alert.alert_type)} - - - - - - {React.string(Float.toString(alert.value))} - - - {if alert.acknowledged { - - } else { - - }} - - - {if !alert.acknowledged { - handleAcknowledge(alert.id)}> - {React.string("Acknowledge")} - - } else { - React.null - }} - - - ) - ->React.array} - - - - -} diff --git a/frontend/src/pages/Analytics.affine b/frontend/src/pages/Analytics.affine new file mode 100644 index 0000000..e380ff0 --- /dev/null +++ b/frontend/src/pages/Analytics.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Analytics; + +// TODO: Complete semantic implementation diff --git a/frontend/src/pages/Analytics.res b/frontend/src/pages/Analytics.res deleted file mode 100644 index 8f40fb5..0000000 --- a/frontend/src/pages/Analytics.res +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// Analytics page placeholder for future predictive maintenance features. - -@react.component -let make = () => { - - - {React.string("Analytics")} - - - {React.string("Advanced analytics and predictive maintenance features coming soon...")} - - -} diff --git a/frontend/src/pages/Dashboard.affine b/frontend/src/pages/Dashboard.affine new file mode 100644 index 0000000..cfd242c --- /dev/null +++ b/frontend/src/pages/Dashboard.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Dashboard; + +// TODO: Complete semantic implementation diff --git a/frontend/src/pages/Dashboard.res b/frontend/src/pages/Dashboard.res deleted file mode 100644 index 4055b6f..0000000 --- a/frontend/src/pages/Dashboard.res +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// Dashboard page showing loom summary cards and status overview. - -@react.component -let make = () => { - let dispatch = Redux.useAppDispatch() - let navigate = ReactRouter.useNavigate() - let loomsState = Redux.useAppSelector(state => state.looms) - let alertsState = Redux.useAppSelector(state => state.alerts) - - let loadLooms = () => { - dispatch(LoomsSlice.setLoading(true)) - let _ = - Api.LoomsAPI.getAll() - ->Promise.then(response => { - let data: array = response.data.data - dispatch(LoomsSlice.setLooms(data)) - Promise.resolve() - }) - ->Promise.catch(error => { - Console.error2("Failed to load looms:", error) - Promise.resolve() - }) - } - - React.useEffect0(() => { - loadLooms() - None - }) - - let getStatusIcon = (status: string) => { - switch status { - | "online" => - | "warning" => - | "error" => - | _ => - } - } - - let activeCount = - loomsState.looms->Array.filter(l => l.status === "active")->Array.length - - - - {React.string("Dashboard")} - - - - - - {React.string("Total Looms")} - - - {React.string(Int.toString(Array.length(loomsState.looms)))} - - - - - - - {React.string("Active Looms")} - - - {React.string(Int.toString(activeCount))} - - - - - - - {React.string("Alerts")} - - - {React.string(Int.toString(alertsState.unacknowledgedCount))} - - - - - - - {React.string("System Health")} - - - {React.string("98%")} - - - - - - {React.string("Looms")} - - - {loomsState.looms - ->Array.map(loom => - - navigate(`/loom/${loom.id}`)}> - - - - {React.string(loom.name)} - - {getStatusIcon(loom.status)} - - - {React.string(loom.location)} - - - {React.string(loom.model)} - - - - - - - - ) - ->React.array} - - -} diff --git a/frontend/src/pages/Login.affine b/frontend/src/pages/Login.affine new file mode 100644 index 0000000..44336c7 --- /dev/null +++ b/frontend/src/pages/Login.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Login; + +// TODO: Complete semantic implementation diff --git a/frontend/src/pages/Login.res b/frontend/src/pages/Login.res deleted file mode 100644 index 69ec7c2..0000000 --- a/frontend/src/pages/Login.res +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// Login page with username/password form and Redux auth integration. - -// Helper to extract target value from form event -@get -external formEventTargetValue: ReactEvent.Form.t => string = "target.value" - -@react.component -let make = () => { - let dispatch = Redux.useAppDispatch() - let (username, setUsername) = React.useState(() => "") - let (password, setPassword) = React.useState(() => "") - let (loading, setLoading) = React.useState(() => false) - - let handleSubmit = event => { - ReactEvent.Form.preventDefault(event) - setLoading(_ => true) - dispatch(AuthSlice.loginStart()) - - let _ = Api.AuthAPI.login(username, password) - ->Promise.then(response => { - let data = response.data.data - dispatch( - AuthSlice.loginSuccess({ - token: data["token"], - user: { - id: data["user"]["id"], - username: data["user"]["username"], - email: data["user"]["email"], - role: data["user"]["role"], - }, - }), - ) - ReactToastify.toastSuccess("Login successful!") - Promise.resolve() - }) - ->Promise.catch(error => { - let message = "Login failed" - dispatch(AuthSlice.loginFailure(message)) - ReactToastify.toastError(message) - setLoading(_ => false) - let _ = error - Promise.resolve() - }) - } - - - - - - {React.string("Kaldor IIoT")} - - - {React.string("Loom Monitoring System")} - - - setUsername(_ => formEventTargetValue(e))} - /> - setPassword(_ => formEventTargetValue(e))} - /> - - {React.string(loading ? "Logging in..." : "Sign In")} - - - - {React.string("Default credentials: admin / admin123")} - - - - -} diff --git a/frontend/src/pages/LoomDetail.affine b/frontend/src/pages/LoomDetail.affine new file mode 100644 index 0000000..68863a7 --- /dev/null +++ b/frontend/src/pages/LoomDetail.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module LoomDetail; + +// TODO: Complete semantic implementation diff --git a/frontend/src/pages/LoomDetail.res b/frontend/src/pages/LoomDetail.res deleted file mode 100644 index 6e625e9..0000000 --- a/frontend/src/pages/LoomDetail.res +++ /dev/null @@ -1,136 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// Loom detail page showing real-time measurements and BBW trend chart. - -// Helper to safely format a nullable float to fixed decimal string -let formatFloat = (value: option, decimals: int): string => { - switch value { - | Some(v) => Float.toFixedWithPrecision(v, ~digits=decimals) - | None => "--" - } -} - -@react.component -let make = () => { - let params = ReactRouter.useParams() - let id = params.id - let (historicalData, setHistoricalData) = React.useState(() => []) - let measurementsState = Redux.useAppSelector(state => state.measurements) - let realTimeData = Dict.get(measurementsState.realTimeData, id) - - let loadHistoricalData = () => { - let _ = - Api.MeasurementsAPI.get(id) - ->Promise.then(response => { - let data: array = response.data.data - setHistoricalData(_ => data) - Promise.resolve() - }) - ->Promise.catch(error => { - Console.error2("Failed to load data:", error) - Promise.resolve() - }) - } - - React.useEffect1(() => { - Websocket.subscribeToLoom(id) - loadHistoricalData() - Some(() => Websocket.unsubscribeFromLoom(id)) - }, [id]) - - let bbwDisplay = switch realTimeData { - | Some(d) => formatFloat(Some(d.bbw_avg), 2) - | None => "--" - } - - let tempDisplay = switch realTimeData { - | Some(d) => formatFloat(Some(d.temperature), 1) - | None => "--" - } - - let vibDisplay = switch realTimeData { - | Some(d) => formatFloat(Some(d.vibration), 2) - | None => "--" - } - - let qualDisplay = switch realTimeData { - | Some(d) => Float.toFixedWithPrecision(d.quality, ~digits=0) - | None => "--" - } - - - - {React.string(`Loom ${id}`)} - - - - - - - {React.string("BBW (Current)")} - - - {React.string(`${bbwDisplay} mm`)} - - - - - - - - - {React.string("Temperature")} - - - {React.string(`${tempDisplay} °C`)} - - - - - - - - - {React.string("Vibration")} - - - {React.string(`${vibDisplay} g`)} - - - - - - - - - {React.string("Quality")} - - - {React.string(`${qualDisplay}%`)} - - - - - - - - {React.string("BBW Trend (Last 24 Hours)")} - - - - - - - - - - - - - - - - - -} diff --git a/frontend/src/pages/Settings.affine b/frontend/src/pages/Settings.affine new file mode 100644 index 0000000..56d93d7 --- /dev/null +++ b/frontend/src/pages/Settings.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Settings; + +// TODO: Complete semantic implementation diff --git a/frontend/src/pages/Settings.res b/frontend/src/pages/Settings.res deleted file mode 100644 index 63f2e41..0000000 --- a/frontend/src/pages/Settings.res +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// Settings page placeholder for system configuration. - -@react.component -let make = () => { - - - {React.string("Settings")} - - - {React.string("System configuration and preferences...")} - - -} diff --git a/frontend/src/services/Api.affine b/frontend/src/services/Api.affine new file mode 100644 index 0000000..a2a8ffc --- /dev/null +++ b/frontend/src/services/Api.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Api; + +// TODO: Complete semantic implementation diff --git a/frontend/src/services/Api.res b/frontend/src/services/Api.res deleted file mode 100644 index d772506..0000000 --- a/frontend/src/services/Api.res +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// Axios API client with interceptors for the Kaldor IIoT backend. -// Interceptors are set up via a JS helper since they require mutation -// and complex callback patterns. - -@module("./api.js") -external api: Axios.instance = "default" - -// Auth API -module AuthAPI = { - @module("./api.js") @scope("authAPI") - external login: (string, string) => promise> = "login" - - @module("./api.js") @scope("authAPI") - external register: (string, string, string) => promise> = "register" -} - -// Looms API -module LoomsAPI = { - @module("./api.js") @scope("loomsAPI") - external getAll: unit => promise> = "getAll" - - @module("./api.js") @scope("loomsAPI") - external getById: string => promise> = "getById" - - @module("./api.js") @scope("loomsAPI") - external updateConfig: (string, {..}) => promise> = "updateConfig" -} - -// Measurements API -module MeasurementsAPI = { - @module("./api.js") @scope("measurementsAPI") - external get: (string, ~params: {..}=?) => promise> = "get" -} - -// Alerts API -module AlertsAPI = { - @module("./api.js") @scope("alertsAPI") - external getAll: (~params: {..}=?) => promise> = "getAll" - - @module("./api.js") @scope("alertsAPI") - external acknowledge: int => promise> = "acknowledge" -} - -// Analytics API -module AnalyticsAPI = { - @module("./api.js") @scope("analyticsAPI") - external getSummary: string => promise> = "getSummary" -} diff --git a/frontend/src/services/Websocket.affine b/frontend/src/services/Websocket.affine new file mode 100644 index 0000000..c1afa4d --- /dev/null +++ b/frontend/src/services/Websocket.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Websocket; + +// TODO: Complete semantic implementation diff --git a/frontend/src/services/Websocket.res b/frontend/src/services/Websocket.res deleted file mode 100644 index a948237..0000000 --- a/frontend/src/services/Websocket.res +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// WebSocket (Socket.io) client for real-time loom data. -// FIX: original TS had `subscribeTo Loom` (space) -- corrected to `subscribeToLoom`. - -// Import connect/disconnect/subscribe functions from the JS helper -// since socket.io relies on mutable module-level state. - -@module("./websocket.js") -external connectWebSocket: (string, ReduxToolkit.dispatch) => SocketIo.socket = "connectWebSocket" - -@module("./websocket.js") -external disconnectWebSocket: unit => unit = "disconnectWebSocket" - -@module("./websocket.js") -external subscribeToLoom: string => unit = "subscribeToLoom" - -@module("./websocket.js") -external unsubscribeFromLoom: string => unit = "unsubscribeFromLoom" diff --git a/frontend/src/store/Store.affine b/frontend/src/store/Store.affine new file mode 100644 index 0000000..fa4cb46 --- /dev/null +++ b/frontend/src/store/Store.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Store; + +// TODO: Complete semantic implementation diff --git a/frontend/src/store/Store.res b/frontend/src/store/Store.res deleted file mode 100644 index fac0244..0000000 --- a/frontend/src/store/Store.res +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// Redux store configuration. -// Uses a thin JS helper for configureStore since it requires dynamic reducer map. - -// Root state type matching all slice states -type rootState = { - auth: AuthSlice.authState, - looms: LoomsSlice.loomsState, - alerts: AlertsSlice.alertsState, - measurements: MeasurementsSlice.measurementsState, -} - -// Re-export store from JS helper -@module("./store.js") -external store: ReduxToolkit.store = "store" diff --git a/frontend/src/store/slices/AlertsSlice.affine b/frontend/src/store/slices/AlertsSlice.affine new file mode 100644 index 0000000..aac3263 --- /dev/null +++ b/frontend/src/store/slices/AlertsSlice.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AlertsSlice; + +// TODO: Complete semantic implementation diff --git a/frontend/src/store/slices/AlertsSlice.res b/frontend/src/store/slices/AlertsSlice.res deleted file mode 100644 index e9f805f..0000000 --- a/frontend/src/store/slices/AlertsSlice.res +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// Typed ReScript interface for the alerts Redux slice. -// The actual reducer logic lives in alertsSlice.js (Immer-based). - -type alert = { - id: int, - loom_id: string, - device_id: string, - alert_type: string, - severity: string, - value: float, - message: string, - acknowledged: bool, - created_at: string, -} - -type alertsState = { - alerts: array, - unacknowledgedCount: int, - loading: bool, -} - -// Action creators imported from the JS wrapper -@module("./alertsSlice.js") -external setAlerts: array => {..} = "setAlerts" - -@module("./alertsSlice.js") -external addAlert: alert => {..} = "addAlert" - -@module("./alertsSlice.js") -external acknowledgeAlert: int => {..} = "acknowledgeAlert" - -@module("./alertsSlice.js") -external setLoading: bool => {..} = "setLoading" - -// Default export is the reducer -@module("./alertsSlice.js") -external reducer: (alertsState, {..}) => alertsState = "default" diff --git a/frontend/src/store/slices/AuthSlice.affine b/frontend/src/store/slices/AuthSlice.affine new file mode 100644 index 0000000..c8236d7 --- /dev/null +++ b/frontend/src/store/slices/AuthSlice.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AuthSlice; + +// TODO: Complete semantic implementation diff --git a/frontend/src/store/slices/AuthSlice.res b/frontend/src/store/slices/AuthSlice.res deleted file mode 100644 index ae982d4..0000000 --- a/frontend/src/store/slices/AuthSlice.res +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// Typed ReScript interface for the auth Redux slice. -// The actual reducer logic lives in authSlice.js (Immer-based). - -type user = { - id: int, - username: string, - email: string, - role: string, -} - -type authState = { - isAuthenticated: bool, - token: Nullable.t, - user: Nullable.t, - loading: bool, - error: Nullable.t, -} - -type loginPayload = { - token: string, - user: user, -} - -// Action creators imported from the JS wrapper -@module("./authSlice.js") -external loginStart: unit => {..} = "loginStart" - -@module("./authSlice.js") -external loginSuccess: loginPayload => {..} = "loginSuccess" - -@module("./authSlice.js") -external loginFailure: string => {..} = "loginFailure" - -@module("./authSlice.js") -external logout: unit => {..} = "logout" - -// Default export is the reducer -@module("./authSlice.js") -external reducer: (authState, {..}) => authState = "default" diff --git a/frontend/src/store/slices/LoomsSlice.affine b/frontend/src/store/slices/LoomsSlice.affine new file mode 100644 index 0000000..265d50a --- /dev/null +++ b/frontend/src/store/slices/LoomsSlice.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module LoomsSlice; + +// TODO: Complete semantic implementation diff --git a/frontend/src/store/slices/LoomsSlice.res b/frontend/src/store/slices/LoomsSlice.res deleted file mode 100644 index 9a67fb6..0000000 --- a/frontend/src/store/slices/LoomsSlice.res +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// Typed ReScript interface for the looms Redux slice. -// The actual reducer logic lives in loomsSlice.js (Immer-based). - -type loom = { - id: string, - name: string, - description: string, - location: string, - model: string, - status: string, - configuration: {..}, -} - -type loomsState = { - looms: array, - selectedLoom: Nullable.t, - loading: bool, - error: Nullable.t, -} - -type statusUpdate = { - id: string, - status: string, -} - -// Action creators imported from the JS wrapper -@module("./loomsSlice.js") -external setLooms: array => {..} = "setLooms" - -@module("./loomsSlice.js") -external setSelectedLoom: loom => {..} = "setSelectedLoom" - -@module("./loomsSlice.js") -external updateLoomStatus: statusUpdate => {..} = "updateLoomStatus" - -@module("./loomsSlice.js") -external setLoading: bool => {..} = "setLoading" - -@module("./loomsSlice.js") -external setError: string => {..} = "setError" - -// Default export is the reducer -@module("./loomsSlice.js") -external reducer: (loomsState, {..}) => loomsState = "default" diff --git a/frontend/src/store/slices/MeasurementsSlice.affine b/frontend/src/store/slices/MeasurementsSlice.affine new file mode 100644 index 0000000..38bef51 --- /dev/null +++ b/frontend/src/store/slices/MeasurementsSlice.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MeasurementsSlice; + +// TODO: Complete semantic implementation diff --git a/frontend/src/store/slices/MeasurementsSlice.res b/frontend/src/store/slices/MeasurementsSlice.res deleted file mode 100644 index 2f201dd..0000000 --- a/frontend/src/store/slices/MeasurementsSlice.res +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Kaldor Community Manufacturing Platform Contributors - -// Typed ReScript interface for the measurements Redux slice. -// The actual reducer logic lives in measurementsSlice.js (Immer-based). - -type measurement = { - time: string, - loom_id: string, - bbw_avg: float, - bbw_min: float, - bbw_max: float, - bbw_stddev: float, - temperature: float, - vibration: float, - quality: float, -} - -type measurementsState = { - measurements: Dict.t>, - realTimeData: Dict.t, - loading: bool, -} - -type measurementsPayload = { - loomId: string, - data: array, -} - -type realTimePayload = { - loomId: string, - data: measurement, -} - -// Action creators imported from the JS wrapper -@module("./measurementsSlice.js") -external setMeasurements: measurementsPayload => {..} = "setMeasurements" - -@module("./measurementsSlice.js") -external updateRealTimeData: realTimePayload => {..} = "updateRealTimeData" - -@module("./measurementsSlice.js") -external setLoading: bool => {..} = "setLoading" - -// Default export is the reducer -@module("./measurementsSlice.js") -external reducer: (measurementsState, {..}) => measurementsState = "default"