@sukramj/fabric-ui 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,11 +43,20 @@ Pages: `NodeStatus`, `NodePair`, `NodeFabrics` (with maintenance),
43
43
  context; none names a host concept. The selection page is driven by the
44
44
  provider's schema (see the repository's `docs/provider.md`).
45
45
 
46
+ Controller pages (v0.2.0): `ControllerNodes`, `ControllerCommission`,
47
+ `ControllerSettings`, and `ControllerNode` with the props `nodeId` (the
48
+ 16-digit hex `node_id_hex`) and an optional first `tab` (`overview`,
49
+ `endpoints`, `attributes`, `acl`, `bindings`, `window`, `icd`, `fabrics`,
50
+ `updates`). They read `ui.controller` (`createControllerStore`); the
51
+ routes `controllerNodes`, `controllerCommission`, `controllerSettings` and
52
+ the prefix `controllerNode` link them. `ui.controller.loadStatus()` sets
53
+ `present` to false on a 404: the host serves no controller.
54
+
46
55
  ### Translator
47
56
 
48
57
  `t(key, vars)` is the host's. `createTranslator(locale, extra)` builds one
49
58
  over the package's catalogues (`catalogs.en`, `catalogs.de`, keys
50
- `node.*`, `common.*`) plus the host's own; a `locale` getter that reads
59
+ `node.*`, `controller.*`, `shell.*`, `common.*`) plus the host's own; a `locale` getter that reads
51
60
  reactive state re-renders the pages on a language switch.
52
61
 
53
62
  ### Stylesheet
@@ -80,6 +89,10 @@ createFabricUI({ client, subscribe: (onEvent) => hub.on("fabric-ui", onEvent) })
80
89
  const close = subscribeStream(client.streamUrl(), (ev) => hub.publish(ev.type, ev.data));
81
90
  ```
82
91
 
92
+ `ui.controller.connect()` opens `GET /controller/stream` the same way;
93
+ `subscribeControllerStream` and the `subscribeController` option of
94
+ `createFabricUI` are its twins.
95
+
83
96
  ## Developing
84
97
 
85
98
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sukramj/fabric-ui",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "The web interface of go-fabric nodes: Svelte 5 pages, UI kit, design tokens with a Home Assistant theme bridge, and the generated API client.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "SukramJ",
@@ -1,4 +1,4 @@
1
- // The client of the node routes, typed from contract/openapi.yaml
1
+ // The client of the node and controller routes, typed from contract/openapi.yaml
2
2
  // (schema.gen.ts). It is the TypeScript half of the contract: the tests in
3
3
  // client.test.ts read the same golden fixtures as the Go handler tests.
4
4
  // Mirrors the matter* functions of openccu-loom
@@ -40,6 +40,49 @@ export type FabricEvent = S["FabricEvent"];
40
40
  export type WindowEvent = S["WindowEvent"];
41
41
  export type ExposureEvent = S["ExposureEvent"];
42
42
 
43
+ // The controller family (contract 0.2.0).
44
+ export type ControllerStatus = S["ControllerStatus"];
45
+ export type ControllerCapabilities = S["ControllerCapabilities"];
46
+ export type ConnectionState = S["ConnectionState"];
47
+ export type ControllerNodeSummary = S["ControllerNodeSummary"];
48
+ export type ControllerNodeList = S["ControllerNodeList"];
49
+ export type ControllerNodeDetail = S["ControllerNodeDetail"];
50
+ export type NodeRecord = S["NodeRecord"];
51
+ export type NodeProperties = S["NodeProperties"];
52
+ export type NodeEndpoint = S["NodeEndpoint"];
53
+ export type NodeCluster = S["NodeCluster"];
54
+ export type NodeDeviceType = S["NodeDeviceType"];
55
+ export type NodeWindow = S["NodeWindow"];
56
+ export type AttributePath = S["AttributePath"];
57
+ export type AttributeValue = S["AttributeValue"];
58
+ export type AttributeValueList = S["AttributeValueList"];
59
+ export type WriteRequest = S["WriteRequest"];
60
+ export type WriteResult = S["WriteResult"];
61
+ export type InvokeRequest = S["InvokeRequest"];
62
+ export type InvokeResult = S["InvokeResult"];
63
+ export type AclEntry = S["AclEntry"];
64
+ export type AclTarget = S["AclTarget"];
65
+ export type BindingTarget = S["BindingTarget"];
66
+ export type EndpointBindings = S["EndpointBindings"];
67
+ export type BindingList = S["BindingList"];
68
+ export type IcdState = S["IcdState"];
69
+ export type NodeFabric = S["NodeFabric"];
70
+ export type NodeFabricList = S["NodeFabricList"];
71
+ export type CommissionRequest = S["CommissionRequest"];
72
+ export type Discovered = S["Discovered"];
73
+ export type DiscoveredList = S["DiscoveredList"];
74
+ export type Credential = S["Credential"];
75
+ export type CredentialList = S["CredentialList"];
76
+ export type CredentialRequest = S["CredentialRequest"];
77
+ export type PendingUpdate = S["PendingUpdate"];
78
+ export type UpdateQueue = S["UpdateQueue"];
79
+ export type UpdateConsentRequest = S["UpdateConsentRequest"];
80
+ export type AvailableUpdate = S["AvailableUpdate"];
81
+ export type AvailableUpdateList = S["AvailableUpdateList"];
82
+ export type ControllerEvent = S["ControllerEvent"];
83
+ /** What GET /controller/discovery selects by. */
84
+ export type DiscoverFilter = { filter?: "any" | "long_discriminator" | "short_discriminator" | "vendor" | "device_type"; value?: number; timeout?: number };
85
+
43
86
  /** The header the admin token travels in (token mode). */
44
87
  export const TOKEN_HEADER = "X-Fabric-UI-Token";
45
88
 
@@ -83,6 +126,42 @@ export interface FabricClient {
83
126
  factoryReset(): Promise<void>;
84
127
  /** The URL of the event stream. */
85
128
  streamUrl(): string;
129
+
130
+ // The controller family. Every node id is the 16-digit hex of node_id_hex.
131
+ /** The controller's status; it sets the admin token from csrf_token, as status() does. 404 when the host serves no controller. */
132
+ controllerStatus(): Promise<ControllerStatus>;
133
+ controllerNodes(): Promise<ControllerNodeList>;
134
+ controllerNode(id: string): Promise<ControllerNodeDetail>;
135
+ decommissionNode(id: string): Promise<void>;
136
+ interviewNode(id: string): Promise<void>;
137
+ enableNode(id: string): Promise<void>;
138
+ disableNode(id: string): Promise<void>;
139
+ setNodeAutoSubscribe(id: string, enabled: boolean): Promise<void>;
140
+ openNodeWindow(id: string, timeoutSeconds?: number): Promise<NodeWindow>;
141
+ closeNodeWindow(id: string): Promise<void>;
142
+ readAttributes(id: string, paths: AttributePath[], fabricFiltered?: boolean): Promise<AttributeValueList>;
143
+ writeAttribute(id: string, req: WriteRequest): Promise<WriteResult>;
144
+ invokeCommand(id: string, req: InvokeRequest): Promise<InvokeResult>;
145
+ nodeAcl(id: string): Promise<{ entries: AclEntry[] }>;
146
+ setNodeAcl(id: string, entries: AclEntry[]): Promise<WriteResult>;
147
+ nodeBindings(id: string): Promise<BindingList>;
148
+ setNodeBindings(id: string, endpoint: number, bindings: BindingTarget[]): Promise<WriteResult>;
149
+ nodeIcd(id: string): Promise<IcdState>;
150
+ registerNodeIcd(id: string, allowMultiAdmin?: boolean): Promise<IcdState>;
151
+ unregisterNodeIcd(id: string, force?: boolean): Promise<IcdState>;
152
+ nodeFabrics(id: string): Promise<NodeFabricList>;
153
+ removeNodeFabric(id: string, fabricIndex: number): Promise<void>;
154
+ commission(req: CommissionRequest): Promise<ControllerNodeDetail>;
155
+ discover(filter?: DiscoverFilter): Promise<DiscoveredList>;
156
+ credentials(): Promise<CredentialList>;
157
+ setCredential(req: CredentialRequest): Promise<void>;
158
+ removeCredential(kind: Credential["kind"], id: string): Promise<void>;
159
+ setFabricLabel(label: string): Promise<void>;
160
+ updates(): Promise<UpdateQueue>;
161
+ consentUpdate(req: UpdateConsentRequest): Promise<void>;
162
+ checkUpdates(): Promise<AvailableUpdateList>;
163
+ /** The URL of the controller's event stream. */
164
+ controllerStreamUrl(): string;
86
165
  }
87
166
 
88
167
  type Fetch = (input: string, init?: RequestInit) => Promise<Response>;
@@ -147,9 +226,63 @@ export function createClient(baseUrl: string, fetchImpl: Fetch = (i, init) => fe
147
226
  factoryReset: () =>
148
227
  call("POST", "/node/maintenance/factory-reset", { admin: true, body: { confirm: FACTORY_RESET_CONFIRMATION } }),
149
228
  streamUrl: () => base + "/node/stream",
229
+
230
+ async controllerStatus() {
231
+ const s = await call<ControllerStatus>("GET", "/controller/status");
232
+ if (s.csrf_token) token = s.csrf_token;
233
+ return s;
234
+ },
235
+ controllerNodes: () => call("GET", "/controller/nodes"),
236
+ controllerNode: (id) => call("GET", node(id)),
237
+ decommissionNode: (id) => call("DELETE", node(id), { admin: true }),
238
+ interviewNode: (id) => call("POST", node(id) + "/interview", { admin: true }),
239
+ enableNode: (id) => call("POST", node(id) + "/enable", { admin: true }),
240
+ disableNode: (id) => call("POST", node(id) + "/disable", { admin: true }),
241
+ setNodeAutoSubscribe: (id, enabled) => call("POST", node(id) + "/auto-subscribe", { admin: true, body: { enabled } }),
242
+ openNodeWindow: (id, timeoutSeconds) =>
243
+ call("POST", node(id) + "/window", { admin: true, body: timeoutSeconds === undefined ? {} : { timeout_seconds: timeoutSeconds } }),
244
+ closeNodeWindow: (id) => call("POST", node(id) + "/window/close", { admin: true }),
245
+ readAttributes: (id, paths, fabricFiltered) =>
246
+ call("POST", node(id) + "/attributes", { body: fabricFiltered === undefined ? { paths } : { paths, fabric_filtered: fabricFiltered } }),
247
+ writeAttribute: (id, req) => call("PUT", node(id) + "/attributes", { admin: true, body: req }),
248
+ invokeCommand: (id, req) => call("POST", node(id) + "/commands", { admin: true, body: req }),
249
+ nodeAcl: (id) => call("GET", node(id) + "/acl"),
250
+ setNodeAcl: (id, entries) => call("PUT", node(id) + "/acl", { admin: true, body: { entries } }),
251
+ nodeBindings: (id) => call("GET", node(id) + "/bindings"),
252
+ setNodeBindings: (id, endpoint, bindings) => call("PUT", node(id) + "/bindings", { admin: true, body: { endpoint, bindings } }),
253
+ nodeIcd: (id) => call("GET", node(id) + "/icd"),
254
+ registerNodeIcd: (id, allowMultiAdmin) =>
255
+ call("POST", node(id) + "/icd/register", { admin: true, body: allowMultiAdmin === undefined ? {} : { allow_multi_admin: allowMultiAdmin } }),
256
+ unregisterNodeIcd: (id, force) =>
257
+ call("POST", node(id) + "/icd/unregister", { admin: true, body: force === undefined ? {} : { force } }),
258
+ nodeFabrics: (id) => call("GET", node(id) + "/fabrics"),
259
+ removeNodeFabric: (id, i) => call("DELETE", `${node(id)}/fabrics/${encodeURIComponent(String(i))}`, { admin: true }),
260
+ commission: (req) => call("POST", "/controller/commission", { admin: true, body: req }),
261
+ discover(f = {}) {
262
+ const q = new URLSearchParams();
263
+ if (f.filter) q.set("filter", f.filter);
264
+ if (f.value !== undefined) q.set("value", String(f.value));
265
+ if (f.timeout !== undefined) q.set("timeout", String(f.timeout));
266
+ const qs = q.toString();
267
+ return call("GET", "/controller/discovery" + (qs ? "?" + qs : ""), { admin: true });
268
+ },
269
+ credentials: () => call("GET", "/controller/credentials"),
270
+ setCredential: (req) => call("PUT", "/controller/credentials", { admin: true, body: req }),
271
+ removeCredential: (kind, id) =>
272
+ call("DELETE", `/controller/credentials?${new URLSearchParams({ kind, id }).toString()}`, { admin: true }),
273
+ setFabricLabel: (label) => call("PUT", "/controller/fabric/label", { admin: true, body: { label } }),
274
+ updates: () => call("GET", "/controller/updates"),
275
+ consentUpdate: (req) => call("POST", "/controller/updates/consent", { admin: true, body: req }),
276
+ checkUpdates: () => call("POST", "/controller/updates/check", { admin: true }),
277
+ controllerStreamUrl: () => base + "/controller/stream",
150
278
  };
151
279
  }
152
280
 
281
+ /** The path of a controller node. */
282
+ function node(id: string): string {
283
+ return `/controller/nodes/${encodeURIComponent(id)}`;
284
+ }
285
+
153
286
  function isProblem(v: unknown): v is Problem {
154
287
  return typeof v === "object" && v !== null && "code" in v && "title" in v;
155
288
  }