toga-ai 1.0.577 → 1.0.579

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.
@@ -43,6 +43,11 @@
43
43
  "name": "harness-audit",
44
44
  "command": "/harness-audit",
45
45
  "description": "Audit knowledge base and harness health, score 0-100."
46
+ },
47
+ {
48
+ "name": "migrate-github-org",
49
+ "command": "/migrate-github-org",
50
+ "description": "Repoint local git remotes from the old GitHub org to the new org across every repo in the developer's workspace."
46
51
  }
47
52
  ],
48
53
  "agents": [
@@ -10,6 +10,7 @@
10
10
  | [/errors Curation Console (Tools → shared Core Logs DB)](features/errors-curation-console.md) | Internal-only triage/curation screen for the 2.0 Issue/Event error-reporting pipeline, built as a 1.0 Tools MVC page reading the **shared Core Logs DB** through | tools/mvc/errors/get.php, tools/mvc/errors/post.php, tools/mvc/errors/issue/get.php, tools/assets/css/style.css, tools/_/app/nav.php, tools/config.production.ini |
11
11
  | [Legacy Email Notifier (tools /email-migration/notify)](features/legacy-email-notifier.md) | An SSO-gated admin tool at **`/email-migration/notify`** (nav group **Email Migration** > **Legacy Notifier**, personas `['TOGa Technology','Development Team']` | tools/mvc/email-migration/notify/get.php, tools/mvc/email-migration/notify/post.php, tools/_/app/nav.php |
12
12
  | [Tools MVC — Routing, CSRF & App_Database Access Patterns](features/mvc-data-access-patterns.md) | The load-bearing 1.0 (`App_`) framework conventions a developer needs when adding a page to the Tools app — URL routing, CSRF, and DB access through `App_Databa | tools/_/app/nav.php, tools/mvc/get.php, library/app/error.php, library/app/database.php |
13
+ | [OneUptime Monitor Status Panel & Outage Alerting (tools /clickup/react)](features/oneuptime-monitor-status-panel.md) | A live **operational monitor column** on the wall-display dashboard at `/clickup/react`. | tools/assets/clickup/sprint-dashboard.html, tools/mvc/clickup/react/get.php |
13
14
  | [Tools Persona-Gated Navigation (App_Nav)](features/persona-gated-navigation.md) | `App_Nav` is the Tools app's two-level, **persona-gated** navigation. | tools/_/app/nav.php, tools/mvc/get.php |
14
15
  | [Tools SAML SSO Consumer & Persona-Gated Auth (App_Auth)](features/saml-sso-auth.md) | `App_Auth` is the Tools app's authentication layer: it consumes the SAML gateway `?saml=` handoff (see the 2.0 SAML downstream integration contract), establishe | tools/_/app/auth.php, tools/mvc/sso/initiate/get.php, tools/mvc/sso/get.php, tools/mvc/login/get.php, tools/mvc/login/post.php, tools/mvc/logout/get.php, tools/mvc/get.php, tools/config.production.ini, tools/config.local.ini |
15
16
  | [Talos Knowledge Base Admin UI (KB Documents + Vocabulary)](features/talos-kb-documents-admin.md) | > **PER-AI-MODEL, DATA-DRIVEN SCOPING (2026-07-29).** The KB-documents and Vocabulary admin > UIs were refactored from a single hard-coded **"development-team"* | tools/mvc/talos/kb-documents/get.php, tools/mvc/talos/kb-documents/post.php, tools/mvc/talos/knowledge-bases/get.php, tools/mvc/talos/knowledge-bases/post.php, tools/mvc/talos/vocabulary/get.php, tools/mvc/talos/vocabulary/post.php, tools/_/app/talos/s3.php, tools/_/app/talos/bedrock.php, tools/_/app/pg.php, tools/_/app/model/true/aimodels.php, tools/_/app/model/true/vectorindexes.php, tools/_/app/model/true/aimodels_vectorindexes.php, tools/_/app/worker.php, tools/_/app/nav.php, tools/config.production.ini, tools/config.alpha.ini, tools/.platform/httpd/conf.d/timeouts.conf, tools/.platform/hooks/prebuild/01-install-php-pgsql.sh, tools/.platform/hooks/postdeploy/01-restart-php.sh |
@@ -6,8 +6,8 @@ project: Tools
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-08-04
10
- owners: [jcardinal]
9
+ updated: 2026-08-14
10
+ owners: [jcardinal, kyalamarthi]
11
11
  files:
12
12
  - tools/index.php
13
13
  - tools/_/app/framework.php
@@ -30,6 +30,7 @@ related:
30
30
  - ./features/saml-sso-auth.md
31
31
  - ./features/persona-gated-navigation.md
32
32
  - ./features/developer-tools.md
33
+ - ./features/oneuptime-monitor-status-panel.md
33
34
  - ../library/architecture.md
34
35
  - ../library/features/mvc-page-pattern-and-app-skeleton.md
35
36
  ---
@@ -97,6 +98,24 @@ routes to `/login` (one-step SSO sign-in — no intermediate welcome card). A pe
97
98
  authenticated user gets the dashboard's explicit empty-state (see
98
99
  `features/persona-gated-navigation.md`).
99
100
 
101
+ ## Wall-display routes & third-party browser dependencies
102
+
103
+ Tools is no longer only a click-through internal-tool shell: `/clickup/react` is an
104
+ **unattended wall display** that holds a full-screen colour state **indefinitely** — an ambient,
105
+ always-on status surface rather than a page someone visits (see
106
+ `features/clickup-sprint-dashboard.md` and `features/oneuptime-monitor-status-panel.md`). Two
107
+ architectural consequences:
108
+
109
+ - **One cross-origin browser dependency.** Every other fetch in this app is same-origin and
110
+ authenticated by the SSO session cookie. The monitor column is the sole exception: the browser
111
+ POSTs directly to the OneUptime **public** status-page API on another host (CORS-open, no
112
+ credentials, raw JSON — not the api2 envelope). Nothing proxies it server-side today, so an
113
+ upstream CORS or availability change breaks that panel in the browser with no server-side signal.
114
+ - **Iframed pages carry their own permission policy.** `mvc/clickup/react/get.php` embeds its
115
+ asset in an iframe, and browser autoplay policy is scoped **per-iframe** — an iframed page that
116
+ must play audio needs `allow="… autoplay"` on the frame or it is blocked silently. Unattended
117
+ displays additionally never receive the user gesture browsers require before audio plays.
118
+
100
119
  ## Adding a tool
101
120
 
102
121
  Create `mvc/<folder>/<tool>/get.php` (copy `mvc/_TEMPLATE/get.php`), add one entry to
@@ -115,6 +134,11 @@ Create `mvc/<folder>/<tool>/get.php` (copy `mvc/_TEMPLATE/get.php`), add one ent
115
134
  **Action still owed:** rotate every secret in `config.production.ini` — the file was
116
135
  reachable, so treat all of it as compromised. (Location only — no values recorded here.)
117
136
  - **SSO initiation + replay defense are open items** — see `features/saml-sso-auth.md`.
137
+ - **Unproxied third-party polling from the browser (`/clickup/react`).** The monitor column pulls
138
+ ~250 KB from OneUptime every 5s — ~3 MB/min **per open tab** — to render about six numbers, and
139
+ hardcodes the status-page ID in the HTML asset. Recommended fix: a small server-side endpoint
140
+ (e.g. `/v2/monitors/status/`) that fetches, reduces and caches ~5s, moving the ID into config and
141
+ removing the cross-origin dependency. Advisory, not scheduled.
118
142
  - **Framework-level Sentry error reporting is an open gap.** Prod printed "Sentry is not
119
143
  installed" because `sentry/sentry` was missing from `composer.json` (added `^4.10`; sibling
120
144
  apps use `^4.10`/`^4.19`). But even with Sentry in `vendor/`, the 1.0 framework does **not**
@@ -123,6 +147,11 @@ Create `mvc/<folder>/<tool>/get.php` (copy `mvc/_TEMPLATE/get.php`), add one ent
123
147
  composer package must `require_once` the autoloader itself.
124
148
 
125
149
  ## Change history
150
+ - 2026-08-14 — Recorded that `/clickup/react` is now an unattended wall display holding a
151
+ full-screen colour state indefinitely, carrying the app's only cross-origin browser dependency
152
+ (OneUptime public status-page API) plus per-iframe permission policy
153
+ (`allow="fullscreen; autoplay"`), and logged the unproxied ~3 MB/min poll + hardcoded
154
+ status-page ID as a known issue with a server-side-proxy fix. (kyalamarthi)
126
155
  - 2026-08-04 — Added the app-wide light/dark/auto theme: `assets/css/theme.css` token layer
127
156
  registered first in `App_FrameworkIndex_Tools`, pre-paint `data-theme` stamping via the
128
157
  one-and-only `setHeaderCode()` hook, `localStorage` preference (no DB column), nav/public
@@ -6,7 +6,7 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-04
9
+ updated: 2026-08-14
10
10
  owners: [kyalamarthi, jcardinal]
11
11
  files:
12
12
  - tools/_/app/clickup/sprint.php
@@ -21,6 +21,7 @@ files:
21
21
  - tools/mvc/login/get.php
22
22
  - tools/_/app/nav.php
23
23
  related:
24
+ - ./oneuptime-monitor-status-panel.md
24
25
  - ./persona-gated-navigation.md
25
26
  - ./saml-sso-auth.md
26
27
  - ./mvc-data-access-patterns.md
@@ -36,6 +37,12 @@ built for a wall-mounted **TV display** ("TOGa IQ" internal sprint metrics). It
36
37
  standalone Node/Express prototype from a prior session, folding the dashboard into `tools`
37
38
  so it runs on the existing App\_ framework + SSO with no separate service.
38
39
 
40
+ The page is now a **two-column split**: this sprint board at 80%, and an operational
41
+ **monitor status column** filling the rest — documented separately in
42
+ [OneUptime Monitor Status Panel](./oneuptime-monitor-status-panel.md). The sprint board itself
43
+ was left byte-for-byte unchanged when that landed (its component was renamed `SprintBoard` and
44
+ wrapped in a two-line `Dashboard`).
45
+
39
46
  It reads the **same Team DB** and reproduces the **same Power BI-parity metric definitions**
40
47
  already documented for worker2 — see
41
48
  [team-sprint-management](../../../2.0/apps/worker2/features/team-sprint-management.md). This
@@ -80,11 +87,39 @@ Babel-standalone from CDN, JSX inside a `<script type="text/babel">`. **No build
80
87
  npm, no bundler.** It fetches `/v2/sprints/*` **same-origin**, so the SSO session cookie
81
88
  authenticates the calls automatically. Do not mistake this for a bundled React app.
82
89
 
90
+ ### Retiring a work type or a departed developer — filter client-side, not in the mock
91
+
92
+ Two `ALL_CAPS` constants near the top of the HTML drop rows from the **live** API responses,
93
+ both matched case-insensitively and trimmed:
94
+
95
+ - **`RETIRED_WORK_TYPES`** — applied to the `worktype-breakdown` response. Currently `STRETCH`,
96
+ retired because the team no longer plans stretch work. The STRETCH **KPI tile** (which called
97
+ `/v2/sprints/tile/?category=stretch`), its colour mapping and its Work Type slice were removed
98
+ outright.
99
+ - **`FORMER_DEVS`** — applied to the `points-by-dev` response, to keep a departed developer off
100
+ "Sprint Points By Dev".
101
+
102
+ **Editing the `USE_MOCK` fixtures does nothing.** `USE_MOCK` is `false` and the live API drives
103
+ the page, so a name or category only disappears when it is filtered out of the **response**.
104
+ Verified against live sprint 84: the departed developer was still returned by
105
+ `/v2/sprints/points-by-dev/` with 33 points (In Review 29, Complete 4), while the API had
106
+ already stopped returning STRETCH — so that filter now matters only for historical sprints.
107
+ See [Gotchas](#gotchas) for the historical-sprint caveat these filters carry.
108
+
109
+ ### Sprint Burndown series
110
+
111
+ Burndown is **Target** (grey dashed) + **Committed** (red) only. The blue **"All"** series was
112
+ dropped alongside STRETCH — with that category retired it tracked "Committed" too closely to
113
+ earn a second line. The legend derives from the `BURNDOWN_SERIES` constant, so it updated
114
+ itself; add or remove a line there rather than editing the legend.
115
+
83
116
  ### Embed page — `mvc/clickup/react/get.php`
84
117
 
85
- A PHP MVC view that `requireAuth`s, then embeds `sprint-dashboard.html` in an
86
- `<iframe allowfullscreen>` (cache-busted by `filemtime`). Heading "Clickup Dashboard",
87
- subtitle notes the data is "…refreshed every 5 min."
118
+ A PHP MVC view that `requireAuth`s, then embeds `sprint-dashboard.html` in an iframe
119
+ (cache-busted by `filemtime`). Heading "Clickup Dashboard", subtitle notes the data is
120
+ "…refreshed every 5 min." The frame is `allow="fullscreen; autoplay"` — **autoplay is
121
+ load-bearing**, not decorative: it is what lets the monitor column's outage beeps sound (see
122
+ [the monitor panel doc](./oneuptime-monitor-status-panel.md)).
88
123
 
89
124
  ### Nav — `_/app/nav.php`
90
125
 
@@ -121,6 +156,16 @@ the React dashboard supersedes it.
121
156
  `cd tools && ENVIRONMENT=dev-local php -d "include_path=.;C:/www/library" -S localhost:8000 router.php`.
122
157
  The `App_` framework + `_.php` bootstrap load from the separate `library` repo via
123
158
  `include_path`; `router.php` reproduces the `.htaccess` rewrite for PHP's built-in server.
159
+ The dev server binds to **localhost only**.
160
+ - **Reach the dashboard through `/clickup/react`, never the raw asset.** The `/v2/sprints/*`
161
+ endpoints are same-origin PHP authenticated by the Tools **SSO session cookie**. Opening
162
+ `/assets/clickup/sprint-dashboard.html` directly in an unauthenticated tab makes **every**
163
+ `/v2` call return **HTTP 401** and the page renders empty — the route, not the file, is what
164
+ carries the session.
165
+ - **Expected local-only console noise.** Twelve `ERR_CONNECTION_REFUSED` for
166
+ `http://localhost/resources/*` (the shared 1.0 jquery / fontawesome / `library.css` /
167
+ `library.js` are expected on port 80) and a resulting `preloaderExit is not defined`. Both are
168
+ pre-existing and local-only — not symptoms of anything in this feature.
124
169
 
125
170
  > Credentials note: DB creds and the `dev_mode` / `dev_autologin` flags live in the
126
171
  > git-ignored `config.dev-local.ini` — never commit values.
@@ -138,6 +183,17 @@ the React dashboard supersedes it.
138
183
  `buildArrayOfRows(App_Database::query($sql,$db), false)` raises "Only variables should be
139
184
  passed by reference", which the E_ALL error handler promotes to a fatal 500. Assign first:
140
185
  `$res = App_Database::query(...); buildArrayOfRows($res, false);`.
186
+ - **`RETIRED_WORK_TYPES` / `FORMER_DEVS` are client-side and unconditional, so historical
187
+ sprints under-report.** Browsing a past sprint that genuinely had stretch work, or that
188
+ developer's points, silently shows lower numbers than the API returned — the filter does not
189
+ know which sprint is on screen. Acceptable for a wall display pinned to the current sprint;
190
+ the correct long-term fix is to filter in `App_Clickup_Sprint` (server-side) or to scope the
191
+ filter to the current sprint only.
192
+ - **`StatusChart`'s `USE_MOCK` fixture is broken (dev-only, unfixed).** The fixture supplies
193
+ `{label, count}` but the code reads `b.points`, so every value is `undefined`, gets filtered
194
+ out, and the Status panel renders **blank** whenever `USE_MOCK = true`. Production is
195
+ unaffected — the live API returns `points`. One-word fix (`count:` → `points:`) if anyone
196
+ needs the mock; deliberately left alone as out of scope.
141
197
 
142
198
  - **The old Power BI embed page (`mvc/clickup/dashboard/get.php`) keeps ~6 fixed colour
143
199
  literals on purpose — do NOT theme them.** Its card/frame colour `#0b1020` is matched to the
@@ -147,6 +203,14 @@ the React dashboard supersedes it.
147
203
  [App-Wide Colour Theme](./theme-light-dark.md).
148
204
 
149
205
  ## Change history
206
+ - 2026-08-14 — Retired the **STRETCH** work type (KPI tile, colour, Work Type slice) and removed
207
+ the blue **"All"** burndown series, leaving Target + Committed; added the client-side
208
+ `RETIRED_WORK_TYPES` / `FORMER_DEVS` response filters (editing the `USE_MOCK` fixtures does
209
+ nothing — the live API drives the page) and recorded their historical-sprint under-reporting
210
+ caveat. Embed iframe is now `allow="fullscreen; autoplay"` for the new
211
+ [monitor status column](./oneuptime-monitor-status-panel.md), which shares this document.
212
+ Documented the raw-asset **401** trap and expected local console noise, plus the broken
213
+ `StatusChart` mock fixture. (kyalamarthi)
150
214
  - 2026-08-04 — Tokenised the `/clickup/dashboard` page-local styles for the app-wide light/dark
151
215
  theme, keeping ~6 deliberate fixed literals (incl. the `#0b1020` frame that matches the
152
216
  embedded Power BI canvas so the iframe load does not flash white). Not committed. (jcardinal)
@@ -0,0 +1,232 @@
1
+ ---
2
+ title: OneUptime Monitor Status Panel & Outage Alerting (tools /clickup/react)
3
+ framework: "1.0"
4
+ repo: tools
5
+ project: Tools
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-14
10
+ owners: [kyalamarthi]
11
+ files:
12
+ - tools/assets/clickup/sprint-dashboard.html
13
+ - tools/mvc/clickup/react/get.php
14
+ related:
15
+ - ./clickup-sprint-dashboard.md
16
+ - ../architecture.md
17
+ - ../../worker/features/oneuptime-worker-uptime-monitoring.md
18
+ - ../../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
19
+ ---
20
+
21
+ ## Summary
22
+
23
+ A live **operational monitor column** on the wall-display dashboard at `/clickup/react`. The
24
+ page is a two-column split — the ClickUp sprint board at 80%, the monitor column filling the
25
+ rest, each a bordered rounded card. The column shows an overall status pill, a **single solid
26
+ status circle** carrying the whole system's state, a per-status legend (count + %), and a
27
+ "Degraded and offline" card listing name / group / status / how long it has been in that state.
28
+
29
+ The display is designed to be read **from across a room**: when a monitor changes to a bad state
30
+ it **beeps, pops up and blinks**, and for as long as anything is still down the **screen edges
31
+ hold a standing colour tint**.
32
+
33
+ It reads the **OneUptime public status-page overview API** directly from the browser. This is
34
+ the **read/consumer** side of OneUptime and is unrelated to the push-heartbeat monitors the
35
+ worker tiers report *into* — see
36
+ [OneUptime push-metric monitors for 2.0 workers](../../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md)
37
+ and [OneUptime external uptime monitoring for 1.0 workers](../../worker/features/oneuptime-worker-uptime-monitoring.md)
38
+ for that side. Those docs define what *creates* the statuses this panel renders.
39
+
40
+ ## How it works
41
+
42
+ ### One document, not a nested iframe
43
+
44
+ The panel is implemented as React components **in the same `sprint-dashboard.html` document**
45
+ as the sprint board. It is deliberately **not** a nested iframe: `mvc/clickup/react/get.php`
46
+ already iframes this page, so nesting a second frame would load a **second React runtime** and
47
+ run a **duplicate ~250 KB poll**.
48
+
49
+ The existing sprint board was left byte-for-byte unchanged — its component was renamed
50
+ `SprintBoard` and a two-line `Dashboard` wrapper added around it plus the monitor column. The
51
+ monitor poll is lifted into `Dashboard` and passed **down as props**, so the status panel and
52
+ the alert watcher share **one** fetch loop rather than polling twice.
53
+
54
+ Only the monitor fetch runs at `MONITOR_REFRESH_MS` (5s); sprint data stays on the page's
55
+ existing 5-minute `AUTO_REFRESH_MS`.
56
+
57
+ ### Upstream contract — OneUptime public status-page overview API
58
+
59
+ Reusable for any future status display. Endpoint:
60
+
61
+ ```
62
+ POST https://oneuptime.com/status-page-api/overview/<statusPageId>
63
+ body: {}
64
+ ```
65
+
66
+ - **It is a POST, not a GET**, and it returns **raw JSON** — **not** the api2
67
+ `{isSuccess, data, messages}` envelope used everywhere else in TOGA. The payload **is** the
68
+ data. Do not unwrap a `.data` that isn't there.
69
+ - A **public** status page needs **no auth** and answers with `Access-Control-Allow-Origin: *`,
70
+ so the browser can call it cross-origin with **no proxy and no credentials**. This is the one
71
+ cross-origin call on a page whose every other fetch is same-origin `/v2/*`.
72
+ - The response is **~250 KB**.
73
+ - Values arrive as **typed envelopes** — `{_type: "ObjectID" | "DateTime" | "Color", value: …}`
74
+ — so read `.value`, never the object. Dates are ISO-8601 UTC with a trailing `Z`.
75
+ - A monitor's **current** state is `statusPageResources[].monitor.currentMonitorStatusId`,
76
+ resolved against the `monitorStatuses[]` list.
77
+ - In `monitorStatusTimelines[]`, the entry with **`endsAt: null`** is the **open** interval —
78
+ i.e. when the current state began. That is what drives "in this state for X".
79
+
80
+ The status page ID is currently a hardcoded constant (`MONITOR_STATUS_PAGE_ID`) in the HTML;
81
+ moving it to config is part of the recommended next step below. (Identifier location only — no
82
+ value recorded here.)
83
+
84
+ ### Status bucketing is derived, never hardcoded
85
+
86
+ Buckets are computed from the status list itself, not from a hardcoded set of status names:
87
+
88
+ - `isOperationalState` → **good**
89
+ - the **worst-priority non-operational** status → **offline / critical**
90
+ - anything in between → **degraded**
91
+
92
+ So a renamed status, or a newly added one, still lands in the right bucket without a code
93
+ change.
94
+
95
+ ### The status circle — the colour is the message
96
+
97
+ `MonitorCircle` is **one filled circle** whose colour is the whole system's state: green
98
+ all-clear, amber if anything is degraded, red if anything is offline, **red winning when both**.
99
+ It draws **nothing inside it** — no total, no label.
100
+
101
+ That is deliberate, and it replaced a `MonitorDonut` (a segmented ring showing per-status
102
+ proportions with the total in the centre) which was **deleted**. From across a room the colour is
103
+ what carries; the legend directly beneath still gives the exact per-status counts, so no
104
+ information is lost. The SVG keeps an `aria-label` listing the counts, so screen readers still
105
+ get the numbers that came off the face of the circle.
106
+
107
+ The circle's colour comes from the **same `standingRole()`** that drives the screen-edge tint, so
108
+ the circle and the edges can never disagree about severity.
109
+
110
+ ### Alerting — edge-triggered events over a level-triggered state
111
+
112
+ **The distinction to carry to any future alerting surface.** Two separate mechanisms, and mixing
113
+ them up is the bug:
114
+
115
+ | | Trigger | Drives | Why |
116
+ |---|---|---|---|
117
+ | **Event** | **Edge** — fires on the transition | beeps, popup, 5s blink | A level-triggered *event* re-alarms every 5s poll for a monitor that has been offline an hour, and the wall display becomes a siren nobody can silence. |
118
+ | **State** | **Level** — follows current state | standing edge tint, status circle | An edge-triggered *state* would show a clean screen when you open the page during an existing outage. **The board must never look healthy while it isn't.** |
119
+
120
+ **Event — `useMonitorAlerts`** compares each poll's non-operational rows against the previous
121
+ poll's:
122
+
123
+ - The **first successful poll is the baseline**, so opening the page mid-outage is **silent**
124
+ (the standing tint, being level-triggered, still shows immediately).
125
+ - A state that simply persists never re-alarms.
126
+ - A full recovery to operational is **silent** (the monitor drops out of the row set entirely).
127
+ - It fires on **any change of non-operational state** — `r.role !== previous.get(r.key)`, where
128
+ the row set only ever holds non-operational monitors, so good→degraded, good→offline,
129
+ degraded→offline **and offline→degraded** all alert. An earlier version compared severity
130
+ numerically (`SEVERITY[new] > SEVERITY[old]`), which silently suppressed **offline → degraded**
131
+ because that is a severity *decrease*. That transition is news, so it is now announced and the
132
+ now-unused `SEVERITY` map was deleted.
133
+
134
+ The event presentation, held for `ALERT_HOLD_MS` (5s):
135
+
136
+ - **Sound** — Web Audio beeps, synthesised at runtime, so there is **no audio asset to ship**.
137
+ **1 beep @ 720 Hz** degraded, **2 beeps @ 990 Hz** offline. The beep count describes the state
138
+ the monitor is in **now**, so an offline → degraded change beeps **once**. Pitch differs too,
139
+ so severity is recognisable across a room without counting.
140
+ - **Popup** — a centre overlay naming each affected monitor.
141
+ - **Blink** — the tint element gains `.is-blinking` for the 5s pulse, then the colour **holds**.
142
+
143
+ **State — `standingRole(model)`** returns `critical` if any row is offline, else `warning` if any
144
+ is degraded, else `null` (no tint). The tint element only renders while `standingRole` is
145
+ non-null, so a full recovery clears it.
146
+
147
+ Note the two can legitimately show different colours at once: the tint always reflects
148
+ **worst-current** state while the popup reflects **the event**, so a degraded-event popup can sit
149
+ on a red screen because something else is still offline. That is correct, not a bug.
150
+
151
+ Verified across full cycles: beep sequence 720 / 990 990 / 720, silent on unchanged and on
152
+ recovery, popup held 4982 ms and 4955 ms against the 5s spec; amber standing → red when offline
153
+ joined (priority) → amber when offline cleared → no tint on full recovery, with the blink
154
+ stopping after 5s and the colour held. Circle verified against the DOM: `#0ca30c` all-clear,
155
+ `#fab219` degraded only, `#d03b3b` with 1 degraded + 1 offline, and `textNodesInSvg = 0`.
156
+
157
+ ### Two colour pairs: meaning vs. legibility
158
+
159
+ `MON_COLOR` is the **validated status palette** and drives everything that **carries meaning** —
160
+ icons, legend, popup border, status text, and the status circle. `MON_COLOR_BRIGHT`
161
+ (`good:#25d825, warning:#ffd633, critical:#ff4d4d`) is used **only** for the full-screen edge rim,
162
+ where nothing depends on its exact value and the only job is legibility from across a room. Do
163
+ not let the bright pair leak into anything semantic.
164
+
165
+ Both reach the overlay as CSS custom properties — `--alert-color` and `--alert-bright`. The rim is
166
+ a **three-layer `box-shadow`**: solid hot rim + tight bright bloom + a deep haze that deliberately
167
+ stays on the **base** hue, so the dashboard underneath stays readable instead of washing out. Each
168
+ bright layer is written `var(--alert-bright, var(--alert-color))` so it **degrades to the base
169
+ colour** if the property never reaches the element.
170
+
171
+ Standing and blinking differ only in intensity, which is what keeps "steady = still broken"
172
+ distinguishable from "pulsing = something just changed":
173
+
174
+ - `.alert-flash` (standing) — 12px rim, `opacity: .85`, no animation. It may sit on screen for
175
+ hours.
176
+ - `.alert-flash.is-blinking` (event) — 20px rim, `opacity: 1`, plus the 0.7s pulse.
177
+
178
+ ## Gotchas
179
+
180
+ - **The embedding iframe needs `allow="autoplay"` or the alert sound is silently blocked.**
181
+ Browser autoplay policy is scoped **per-iframe**. `mvc/clickup/react/get.php` embedded the
182
+ dashboard with `allow="fullscreen"`, so Web Audio inside the frame was policy-blocked and the
183
+ beeps would never have sounded in the Tools app — with no error. It is now
184
+ `allow="fullscreen; autoplay"`. Any future iframed page that makes sound needs the same.
185
+ - **Permission is not enough — browsers still require one user gesture per session.** The page
186
+ unlocks the `AudioContext` on the first click/keypress, and if an alert fires while audio is
187
+ still blocked the popup **says so** rather than failing silently. On an **unattended wall
188
+ display nobody ever clicks**, so alerts stay visual-only. For kiosk use, launch Chrome with
189
+ `--autoplay-policy=no-user-gesture-required`.
190
+ - **The `prefers-reduced-motion` override needs the compound selector.** Blinking is a
191
+ vestibular risk, so the reduced-motion rule kills the animation and holds the colour steady.
192
+ Once the blink moved to `.alert-flash.is-blinking` (specificity 0,2,0), a reduced-motion rule
193
+ on `.alert-flash` alone (0,1,0) **could no longer override it** and the screen kept pulsing.
194
+ The rule must list both: `.alert-flash, .alert-flash.is-blinking { animation: none; }`. Any
195
+ future state/modifier CSS split has the same trap.
196
+ - **A parse failure currently looks calmer than an outage** (known issue, not fixed). If the
197
+ OneUptime call returns HTTP 200 with a malformed or empty body, `buildMonitorModel` yields
198
+ total 0, empty buckets and empty rows. The status pill correctly degrades to "Unknown", but
199
+ the affected-list card still reads **"All 0 monitors are operational"**, and any
200
+ previously-degraded monitor silently disappears **with no alert** (recovery is silent by
201
+ design, and an empty row set is indistinguishable from a full recovery). Since the standing
202
+ tint and the status circle are both driven by that same empty row set, a parse failure also
203
+ **clears a standing red screen back to green** — the most reassuring possible display of a
204
+ total loss of visibility. Recommended fix: treat "zero resources parsed" as an **error
205
+ state**, not a healthy one.
206
+
207
+ ## Recommended next step (advisory — not built)
208
+
209
+ Replace the direct browser → OneUptime poll with a small **server-side proxy endpoint** in the
210
+ Tools app (e.g. `/v2/monitors/status/`) that fetches upstream, reduces the payload to the
211
+ summary, and caches ~5s. Rationale:
212
+
213
+ - The browser currently pulls **~250 KB every 5s (~3 MB/min per open tab)** to display about
214
+ six numbers; a proxy makes it ~1 KB.
215
+ - One upstream fetch is shared across **all** open displays instead of one per tab.
216
+ - It removes the cross-origin dependency, and therefore the CORS/autoplay coupling above.
217
+ - The status page ID moves into **config** instead of hardcoded HTML.
218
+ - It gives a natural home for **server-side transition history**, which is what edge-triggered
219
+ alerting really wants (today the baseline resets every time a tab reloads).
220
+
221
+ ## Change history
222
+ - 2026-08-14 — Built the monitor status column on `/clickup/react` (split layout, solid status
223
+ circle + legend + degraded/offline card) as components in the same document rather than a
224
+ nested iframe, sharing one 5s poll; documented the OneUptime public status-page overview API
225
+ contract (POST, raw JSON not the api2 envelope, CORS-open, typed `{_type,value}` envelopes,
226
+ `endsAt: null` = open interval) and derived status bucketing. Alerting splits **edge-triggered
227
+ events** (beeps / popup / 5s blink, now firing on any non-operational change incl.
228
+ offline → degraded) from a **level-triggered standing tint** driven by `standingRole()`, which
229
+ also colours the circle so the two can never disagree; added the `MON_COLOR_BRIGHT` rim-only
230
+ palette. Fixed the iframe `allow="fullscreen; autoplay"` autoplay block and recorded the
231
+ user-gesture/kiosk caveat, the `prefers-reduced-motion` specificity trap, the malformed-body
232
+ "all 0 monitors operational" gap, and the server-side-proxy recommendation. (kyalamarthi)
@@ -14,7 +14,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
14
14
  - **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
15
15
  - **test** (Test) — 14 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
16
16
  - **toga** (TOGa) — 3 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
17
- - **tools** (Tools) — 14 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
17
+ - **tools** (Tools) — 15 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
18
18
 
19
19
  ## 2.0 framework
20
20
 
@@ -39,7 +39,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
39
39
  - **togatech** (TOGA Technology Website) — 6 doc(s) → [standalone/apps/togatech/INDEX.md](standalone/apps/togatech/INDEX.md)
40
40
  - **websocket** (WebSocket Server) — 2 doc(s) → [standalone/apps/websocket/INDEX.md](standalone/apps/websocket/INDEX.md)
41
41
  - **forward** (Forwarder) — 3 doc(s) → [standalone/apps/forward/INDEX.md](standalone/apps/forward/INDEX.md)
42
- - **claude** (Claude Harness) — 7 doc(s) → [standalone/apps/claude/INDEX.md](standalone/apps/claude/INDEX.md)
42
+ - **claude** (Claude Harness) — 8 doc(s) → [standalone/apps/claude/INDEX.md](standalone/apps/claude/INDEX.md)
43
43
 
44
44
  ## Clients
45
45
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.577",
3
+ "version": "1.0.579",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",
@@ -0,0 +1,84 @@
1
+ ---
2
+ name: migrate-github-org
3
+ description: Repoints local git remotes from the old GitHub org (agilantsolutions) to the new org (togatech) across every repo in the developer's workspace, without touching SSH keys, tokens, or repo names. Trigger when the user says "migrate-github-org", "update my github remotes", "switch to the new github org", "point my repos at togatech", or asks how to update their local setup after the org transfer.
4
+ ---
5
+
6
+ # Migrate GitHub Org — repoint local remotes after the org transfer
7
+
8
+ TOGA is moving its repos from the `agilantsolutions` GitHub org to `togatech`. The
9
+ org-level transfer (repo Settings → Transfer ownership) is done once by an admin.
10
+ **This skill is the per-developer follow-up** — it updates each developer's local
11
+ git remotes so `git`/GitHub Desktop keep working against the new org.
12
+
13
+ ## Before running this
14
+
15
+ Confirm with the developer:
16
+ 1. **Has the org transfer already happened?** If repos don't exist at
17
+ `github.com/togatech/<repo>` yet, do not run this — the local remote would point
18
+ at a repo that doesn't resolve. Ask them to check with the admin first.
19
+ 2. **Is their GitHub account a member of `togatech`?** If not, pushes/pulls will
20
+ fail with a 403 or "repository not found" regardless of remote URL — this is
21
+ an org-membership issue, not something this script fixes.
22
+
23
+ If either is unconfirmed, tell the developer to check before proceeding, but still
24
+ offer to run the script if they want to prepare ahead of time — it's safe to run
25
+ against repos still on the old org (it just does nothing until the org changes).
26
+
27
+ ## Step 1 — Locate the script
28
+
29
+ The script lives next to this SKILL.md in the team repo:
30
+
31
+ ```bash
32
+ for d in claude ../claude; do [ -f "$d/skills/migrate-github-org/migrate-github-org.sh" ] && REPO="$d" && break; done
33
+ echo "${REPO:-NOT_FOUND}"
34
+ ```
35
+
36
+ If `NOT_FOUND`, tell the user to run `/sync-team-skills` first (or clone
37
+ `https://github.com/TOGATechnology/claude`), then retry.
38
+
39
+ ## Step 2 — Run it against their workspace
40
+
41
+ Run from the developer's actual project/workspace root (the folder containing
42
+ their repo checkouts as subfolders — e.g. `www`, `2.0`, whatever they call it):
43
+
44
+ ```bash
45
+ bash "$REPO/skills/migrate-github-org/migrate-github-org.sh" .
46
+ ```
47
+
48
+ If the user is running this from inside VS Code with the workspace folder already
49
+ open, `.` is correct — no path argument needed.
50
+
51
+ ## Step 3 — Report results
52
+
53
+ Relay the script's own summary output verbatim (updated / skipped / no-origin
54
+ counts and the list of repos changed) — do not re-summarize or drop detail.
55
+
56
+ Then tell the developer:
57
+ - If any repos were **updated**: restart GitHub Desktop (or close/reopen the repo
58
+ in it) so it picks up the new remote URL.
59
+ - The **first push or pull** after this may prompt for re-authentication if their
60
+ GitHub account doesn't yet have access to `togatech` — that's expected, not a
61
+ bug in the script. They should sign in / re-authorize when prompted.
62
+ - No SSH keys or manually-generated tokens are needed for this step — GitHub
63
+ Desktop handles HTTPS auth via its own sign-in. A token only matters for
64
+ someone with a manually configured PAT used outside Desktop (CI scripts,
65
+ `composer.json` VCS repos, deploy hooks) — those need separate re-scoping to
66
+ `togatech` and are out of scope for this skill.
67
+
68
+ ## What this does NOT do
69
+
70
+ - Does not transfer repos on GitHub itself (that's the admin's one-time step).
71
+ - Does not touch SSH remotes, deploy keys, CI secrets, or hardcoded org
72
+ references in code/config (ebextensions, composer.json, etc.) — those need
73
+ a separate sweep.
74
+ - Does not grant org membership or repo permissions.
75
+
76
+ ## Self-updating anomaly log
77
+
78
+ If you hit an error, permission issue, or unexpected behavior not already
79
+ documented above — even if you worked around it — record it here so the next
80
+ developer doesn't hit it blind.
81
+
82
+ ### Known anomalies
83
+
84
+ (none yet)
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # migrate-github-org.sh
4
+ #
5
+ # Scans a workspace directory for git repos and repoints any remote that
6
+ # points at OLD_ORG on github.com to NEW_ORG, keeping the repo name and
7
+ # the HTTPS protocol (what GitHub Desktop uses) unchanged.
8
+ #
9
+ # Usage:
10
+ # ./migrate-github-org.sh [workspace-dir]
11
+ #
12
+ # workspace-dir defaults to the current directory.
13
+ #
14
+ # Safe to re-run: repos already pointing at NEW_ORG, or at any org other
15
+ # than OLD_ORG, are left untouched and reported as skipped.
16
+
17
+ set -euo pipefail
18
+
19
+ OLD_ORG="agilantsolutions"
20
+ NEW_ORG="togatech"
21
+
22
+ WORKSPACE="${1:-.}"
23
+
24
+ if [ ! -d "$WORKSPACE" ]; then
25
+ echo "Error: workspace directory not found: $WORKSPACE" >&2
26
+ exit 1
27
+ fi
28
+
29
+ updated=0
30
+ skipped=0
31
+ not_git=0
32
+
33
+ echo "Scanning $WORKSPACE for git repositories..."
34
+ echo "Renaming remotes: github.com/$OLD_ORG/<repo> -> github.com/$NEW_ORG/<repo>"
35
+ echo
36
+
37
+ # Only descend one level into WORKSPACE — each subfolder is treated as a
38
+ # candidate repo. Adjust maxdepth if your repos are nested deeper.
39
+ while IFS= read -r -d '' gitdir; do
40
+ repo_path="$(dirname "$gitdir")"
41
+ repo_name="$(basename "$repo_path")"
42
+
43
+ remote_url="$(git -C "$repo_path" remote get-url origin 2>/dev/null || true)"
44
+
45
+ if [ -z "$remote_url" ]; then
46
+ echo " [no origin] $repo_name"
47
+ not_git=$((not_git + 1))
48
+ continue
49
+ fi
50
+
51
+ case "$remote_url" in
52
+ *"github.com/$OLD_ORG/"*|*"github.com:$OLD_ORG/"*)
53
+ new_url="${remote_url/$OLD_ORG/$NEW_ORG}"
54
+ git -C "$repo_path" remote set-url origin "$new_url"
55
+ echo " [updated] $repo_name"
56
+ echo " $remote_url"
57
+ echo " -> $new_url"
58
+ updated=$((updated + 1))
59
+ ;;
60
+ *)
61
+ echo " [skipped] $repo_name ($remote_url)"
62
+ skipped=$((skipped + 1))
63
+ ;;
64
+ esac
65
+ done < <(find "$WORKSPACE" -mindepth 2 -maxdepth 2 -type d -name ".git" -print0)
66
+
67
+ echo
68
+ echo "Done. Updated: $updated, skipped (not $OLD_ORG): $skipped, no origin: $not_git"
69
+
70
+ if [ "$updated" -gt 0 ]; then
71
+ echo
72
+ echo "Next steps for each updated repo:"
73
+ echo " - If you use GitHub Desktop, close and reopen the repo (or restart Desktop)"
74
+ echo " so it picks up the new remote URL."
75
+ echo " - The first push/pull against the new org may prompt you to re-authenticate"
76
+ echo " if your GitHub account/token doesn't already have access to $NEW_ORG."
77
+ fi