@scalio-cloud/ui 0.18.1 → 0.19.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.0
4
+ Settings card "Scalio Beacon" for the apps (Beacon phase 3, scalio-docs `concepts/beacon-suite.md`).
5
+ - `BeaconConnection` with `peersApi(api)`: pair the installation with a Beacon instance (address + one-time code from
6
+ Beacon), show the pairing (address, version, last contact, queue), test it and disconnect. Routes from
7
+ scalio-connect 0.8.0 (`scalio_connect.peers.web.admin_router`).
8
+ - Labels `beacon*` in German and English.
9
+ - **Visible change:** none until an app shows the card. No token changes.
10
+
3
11
  ## 0.18.1
4
12
  Page actions after Markus' feedback (2026-10-31, UISPEC section 2 "Page actions"). 0.18.0 was released before these
5
13
  changes were merged; apps use 0.18.1.
@@ -36,7 +44,7 @@ Same profile page in every app, reference Scalio Nexus (Markus 2026-10-31, scali
36
44
  - **Visible change:** none until an app uses the new parts. No token changes.
37
45
 
38
46
  ## 0.15.0
39
- New app Scalio Beacon (service desk, scalio-docs ADR 0043).
47
+ New app Scalio Beacon (service desk, scalio-docs ADR 0044).
40
48
  - Product switcher: entry "Beacon" (icon `RadioTower`) between Citadel and Pulse, available like the others.
41
49
  - `manualUrl("beacon")`: docs.scaliobeacon.com / devdocs.scaliobeacon.com.
42
50
  - **Visible change:** every app shows "Beacon" in the switcher after its upgrade (greyed out until the instance
package/README.md CHANGED
@@ -117,6 +117,10 @@ With `client` the drawer has the tabs "Neue Anfrage" (category, subject, descrip
117
117
  axios instance. The settings card `<SupportConnection client={supportApi(api)} lang={lang} />` connects the
118
118
  installation with a one-time code (admins). Without `client` the drawer keeps the old single form with `onSubmit`.
119
119
 
120
+ The settings card `<BeaconConnection client={peersApi(api)} lang={lang} />` pairs the app with Scalio Beacon
121
+ (address and one-time code from Beacon, Einstellungen -> Verbundene Apps); findings then go to Beacon. Backend:
122
+ scalio-connect 0.8.0 `peers.web.admin_router`.
123
+
120
124
  `link` is the router's `NavLink` (it gets `to`, `end`, a `className` function and `onClick`); without it the
121
125
  sidebar renders plain links and marks the item for `activePath`. Item test ids default to `nav-<path>`
122
126
  (`nav-overview` for `/`), or set `testId`. Page blocks: `KpiTile` (tone `error` = red number),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalio-cloud/ui",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
4
4
  "description": "Scalio Control Room design tokens, Tailwind preset, self-hosted fonts, the shared app header, sign-in layout, sidebar and page blocks",
5
5
  "license": "SEE LICENSE IN README.md",
6
6
  "private": false,
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ // Settings card "Scalio Beacon" (Beacon phase 3, scalio-docs concepts/beacon-suite.md): an app (Citadel, Rosetta,
3
+ // Foundry, Nexus) pairs with a Beacon instance using the address and one-time code from Beacon (Einstellungen ->
4
+ // Verbundene Apps); findings then go to Beacon and become tickets. Backend routes from scalio-connect
5
+ // (`scalio_connect.peers.web.admin_router`); peersApi(api) maps them for an axios-like client.
6
+ const React = require("react");
7
+ const h = React.createElement;
8
+ const { labelsFor } = require("./labels");
9
+
10
+ function errorText(err) {
11
+ const d = err && err.response && err.response.data && err.response.data.detail;
12
+ if (typeof d === "string") return d;
13
+ if (d && typeof d.message === "string") return d.message;
14
+ return (err && err.message) || String(err);
15
+ }
16
+
17
+ function fmtDate(value, lang) {
18
+ if (!value) return "";
19
+ const d = new Date(value);
20
+ if (isNaN(d.getTime())) return String(value);
21
+ return d.toLocaleString(lang === "en" ? "en-GB" : "de-CH", { dateStyle: "medium", timeStyle: "short" });
22
+ }
23
+
24
+ // api: axios instance with the app's base URL (/api). Every method resolves to the response body.
25
+ function peersApi(api) {
26
+ const data = (p) => p.then((r) => r.data);
27
+ const enc = encodeURIComponent;
28
+ return {
29
+ list: () => data(api.get("/peers")),
30
+ connect: (url, code) => data(api.post("/peers", { url, code })),
31
+ check: (id) => data(api.post(`/peers/${enc(id)}/check`)),
32
+ remove: (id) => data(api.delete(`/peers/${enc(id)}`)),
33
+ };
34
+ }
35
+
36
+ function BeaconConnection({ client, lang, labels, onChange }) {
37
+ const L = labelsFor(lang, labels);
38
+ const [st, setSt] = React.useState(null);
39
+ const [url, setUrl] = React.useState("");
40
+ const [code, setCode] = React.useState("");
41
+ const [busy, setBusy] = React.useState(false);
42
+ const [error, setError] = React.useState("");
43
+ const [note, setNote] = React.useState("");
44
+ const load = React.useCallback(() => client.list().then((d) => setSt(d)).catch((err) => (setError(errorText(err)), setSt(false))), [client]);
45
+ React.useEffect(() => { load(); }, [load]);
46
+ const run = async (fn, ok) => {
47
+ setBusy(true);
48
+ setError("");
49
+ setNote("");
50
+ try {
51
+ const d = await fn();
52
+ if (d && d.ok === false) throw new Error(L.beaconCheckFailed.replace("{detail}", d.detail || d.status));
53
+ setCode("");
54
+ setNote(typeof ok === "function" ? ok(d) : ok);
55
+ await load();
56
+ if (onChange) onChange(d);
57
+ } catch (err) {
58
+ setError(errorText(err));
59
+ } finally {
60
+ setBusy(false);
61
+ }
62
+ };
63
+ if (st === null) return h("div", { className: "scalio-support-meta" }, L.loading);
64
+ if (st === false) return h("div", { className: "scalio-notice is-error", role: "alert" }, error);
65
+ const beacons = (st.peers || []).filter((p) => p.app === "beacon");
66
+ return h(
67
+ "div",
68
+ { className: "scalio-support-conn", "data-testid": "beacon-connection" },
69
+ h("p", { className: "scalio-support-meta" }, beacons.length ? L.beaconHelpConnected : L.beaconHelp),
70
+ beacons.map((p) => {
71
+ const q = p.queue || {};
72
+ return h(
73
+ "div",
74
+ { key: p.id, className: "scalio-support-conn", "data-testid": "beacon-peer" },
75
+ h("div", { className: "scalio-support-conn-head" },
76
+ h("span", { className: "scalio-badge " + (p.usable ? "is-ok" : "is-error") }, p.usable ? L.beaconConnected : L.beaconKeyLost)),
77
+ h("dl", { className: "scalio-support-conn-facts" },
78
+ h("dt", null, L.beaconAddress), h("dd", null, p.url),
79
+ p.version ? h(React.Fragment, null, h("dt", null, L.version), h("dd", null, p.version)) : null,
80
+ h("dt", null, L.supportSince), h("dd", null, fmtDate(p.paired_at, lang)),
81
+ h("dt", null, L.beaconLastContact), h("dd", null, fmtDate(p.last_seen, lang))),
82
+ q.pending || q.failed
83
+ ? h("div", { className: "scalio-support-meta", "data-testid": "beacon-queue" },
84
+ L.supportOutbox.replace("{pending}", q.pending || 0).replace("{failed}", q.failed || 0) + (q.last_error ? ` (${q.last_error})` : ""))
85
+ : null,
86
+ h("div", { className: "scalio-support-conn-actions" },
87
+ h("button", { type: "button", className: "scalio-btn", disabled: busy, "data-testid": "beacon-check",
88
+ onClick: () => run(() => client.check(p.id), (d) => L.beaconChecked.replace("{version}", (d && d.version) || "")) }, L.supportCheck),
89
+ h("button", { type: "button", className: "scalio-btn scalio-btn-danger", disabled: busy, "data-testid": "beacon-remove",
90
+ onClick: () => (typeof window === "undefined" || window.confirm(L.beaconRemoveConfirm)) && run(() => client.remove(p.id), L.supportRemoved) },
91
+ L.supportRemove))
92
+ );
93
+ }),
94
+ note ? h("div", { className: "scalio-notice is-ok", "data-testid": "beacon-note" }, note) : null,
95
+ error ? h("div", { className: "scalio-notice is-error", role: "alert", "data-testid": "beacon-error" }, error) : null,
96
+ beacons.length
97
+ ? null
98
+ : h(
99
+ "form",
100
+ { className: "scalio-support-form", onSubmit: (e) => { e.preventDefault(); run(() => client.connect(url.trim(), code.trim()), L.beaconPaired); } },
101
+ h("label", { className: "scalio-field-label", htmlFor: "beacon-url" }, L.beaconAddress),
102
+ h("input", { id: "beacon-url", className: "scalio-input", placeholder: "https://beacon.example.com", value: url, required: true,
103
+ onChange: (e) => setUrl(e.target.value), "data-testid": "beacon-url-input" }),
104
+ h("label", { className: "scalio-field-label", htmlFor: "beacon-code" }, L.beaconCode),
105
+ h("input", { id: "beacon-code", className: "scalio-input", placeholder: "bpc_…", value: code, required: true,
106
+ onChange: (e) => setCode(e.target.value), "data-testid": "beacon-code-input" }),
107
+ h("button", { type: "submit", className: "scalio-btn-primary", disabled: busy, "data-testid": "beacon-connect-btn" }, L.supportConnect)
108
+ )
109
+ );
110
+ }
111
+
112
+ module.exports = { peersApi, BeaconConnection };
package/react/index.js CHANGED
@@ -378,6 +378,6 @@ module.exports = {
378
378
  TEST_IDS,
379
379
  initials,
380
380
  };
381
- Object.assign(module.exports, Nav, Blocks, require("./settings"), require("./support"), require("./security"), require("./license"), require("./palette"), require("./unify"), require("./profile"), require("./actions"));
381
+ Object.assign(module.exports, Nav, Blocks, require("./settings"), require("./support"), require("./beacon"), require("./security"), require("./license"), require("./palette"), require("./unify"), require("./profile"), require("./actions"));
382
382
  // After the exports above: auth.js uses LanguageToggle and ThemeToggle from this module.
383
383
  Object.assign(module.exports, require("./auth"));
package/react/labels.js CHANGED
@@ -140,6 +140,19 @@ const LABELS = {
140
140
  supportRemove: "Trennen",
141
141
  supportRemoveConfirm: "Verbindung zum Support trennen? Neue Tickets warten dann, bis wieder verbunden ist.",
142
142
  supportRemoved: "Verbindung getrennt.",
143
+ // Scalio Beacon (connected apps, Beacon phase 3)
144
+ beaconTitle: "Scalio Beacon",
145
+ beaconHelp: "Befunde dieser Installation gehen an Scalio Beacon und werden dort Tickets. Adresse und Kopplungscode erhalten Sie in Beacon unter Einstellungen -> Verbundene Apps.",
146
+ beaconHelpConnected: "Befunde dieser Installation gehen an Scalio Beacon und werden dort Tickets.",
147
+ beaconAddress: "Beacon-Adresse",
148
+ beaconCode: "Kopplungscode",
149
+ beaconConnected: "Verbunden",
150
+ beaconKeyLost: "Schlüssel nicht lesbar, bitte neu verbinden",
151
+ beaconLastContact: "Letzter Kontakt",
152
+ beaconPaired: "Mit Beacon verbunden.",
153
+ beaconChecked: "Verbindung funktioniert (Beacon {version}).",
154
+ beaconCheckFailed: "Keine Verbindung: {detail}",
155
+ beaconRemoveConfirm: "Verbindung zu Beacon trennen? Befunde gehen dann nicht mehr an Beacon; die Tickets dort bleiben.",
143
156
  // Two-factor card (SecurityFactors) and license panel (LicensePanel)
144
157
  mfa: {
145
158
  backupTitle: "Ihre Backup-Codes",
@@ -384,6 +397,19 @@ const LABELS = {
384
397
  supportRemove: "Disconnect",
385
398
  supportRemoveConfirm: "Disconnect from support? New tickets then wait until the instance is connected again.",
386
399
  supportRemoved: "Disconnected.",
400
+ // Scalio Beacon (connected apps, Beacon phase 3)
401
+ beaconTitle: "Scalio Beacon",
402
+ beaconHelp: "Findings of this installation go to Scalio Beacon and become tickets there. You get the address and the pairing code in Beacon under Settings -> Connected apps.",
403
+ beaconHelpConnected: "Findings of this installation go to Scalio Beacon and become tickets there.",
404
+ beaconAddress: "Beacon address",
405
+ beaconCode: "Pairing code",
406
+ beaconConnected: "Connected",
407
+ beaconKeyLost: "Key cannot be read, please connect again",
408
+ beaconLastContact: "Last contact",
409
+ beaconPaired: "Connected with Beacon.",
410
+ beaconChecked: "Connection works (Beacon {version}).",
411
+ beaconCheckFailed: "No connection: {detail}",
412
+ beaconRemoveConfirm: "Disconnect from Beacon? Findings no longer go to Beacon; the tickets there stay.",
387
413
  mfa: {
388
414
  backupTitle: "Your backup codes",
389
415
  backupHint: "Each code works once, if you do not have your phone. Keep them in a safe place, for example your password manager. They are shown only now.",