toga-ai 1.0.605 → 1.0.607
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/knowledge/1.0/apps/test/INDEX.md +1 -0
- package/knowledge/1.0/apps/test/features/compass-retrofix2-sql-generator.md +102 -0
- package/knowledge/1.0/apps/tools/INDEX.md +2 -2
- package/knowledge/1.0/apps/tools/architecture.md +36 -17
- package/knowledge/1.0/apps/tools/features/clickup-sprint-dashboard.md +69 -12
- package/knowledge/1.0/apps/tools/features/oneuptime-monitor-status-panel.md +235 -235
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/features/email-send-pipeline.md +22 -1
- package/knowledge/2.0/apps/_underscore/features/error-reporting-issue-event.md +16 -1
- package/knowledge/2.0/apps/api2/INDEX.md +1 -1
- package/knowledge/2.0/apps/api2/features/tableview-field-metadata.md +40 -0
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +14 -1
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +1 -1
- package/knowledge/2.0/apps/toga25-supply/features/surface-frontend.md +38 -1
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/compass-usa/workflows/order-lifecycle-and-data-integrity.md +41 -7
- package/knowledge/clients/quad/profile.md +9 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: OneUptime Monitor Status Panel & Outage Alerting (tools /
|
|
2
|
+
title: OneUptime Monitor Status Panel & Outage Alerting (tools /developer)
|
|
3
3
|
framework: "1.0"
|
|
4
4
|
repo: tools
|
|
5
5
|
project: Tools
|
|
@@ -10,7 +10,9 @@ updated: 2026-08-18
|
|
|
10
10
|
owners: [kyalamarthi, jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- tools/assets/clickup/sprint-dashboard.html
|
|
13
|
-
- tools/
|
|
13
|
+
- tools/_/app/clickup/monitors.php
|
|
14
|
+
- tools/v2/monitors/status/index.php
|
|
15
|
+
- tools/mvc/developer/get.php
|
|
14
16
|
related:
|
|
15
17
|
- ./clickup-sprint-dashboard.md
|
|
16
18
|
- ../architecture.md
|
|
@@ -20,273 +22,271 @@ related:
|
|
|
20
22
|
|
|
21
23
|
## Summary
|
|
22
24
|
|
|
23
|
-
A live **operational monitor column** on the wall-display dashboard at `/
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
A live **operational monitor column** on the wall-display dashboard at `/developer` (the route
|
|
26
|
+
was renamed from `/clickup/react` — the internal asset path `assets/clickup/sprint-dashboard.html`
|
|
27
|
+
and the `App_Clickup_*` class names are deliberately unchanged). The page is a two-column split —
|
|
28
|
+
the ClickUp sprint board at 75%, the monitor column filling the rest.
|
|
29
|
+
|
|
30
|
+
The panel is **state-adaptive: calm when healthy, loud when broken.**
|
|
31
|
+
|
|
32
|
+
- **Healthy** = one calm full-height green hero ("ALL SYSTEMS OPERATIONAL" + monitor count), a
|
|
33
|
+
soft glow on a dark panel — **not** a saturated fill. In the green state the hero is
|
|
34
|
+
**full-bleed flush** to the pane edges (no panel header, no inner padding, no hero border —
|
|
35
|
+
clipped by the pane's `overflow:hidden` + rounded corners).
|
|
36
|
+
- **Bad** = a saturated DEGRADED (amber) / OUTAGE (red) band plus the affected-monitor list as
|
|
37
|
+
cards that **grow to share the column height** (min-height floor, scroll when many). Each
|
|
38
|
+
affected card puts its icon centered **above** the text, larger, with a role-colored full
|
|
39
|
+
border; monitor names **wrap** (they used to truncate on the TV). Non-green states keep the
|
|
40
|
+
panel header + padded card layout.
|
|
41
|
+
- **Unknown** = a neutral grey "STATUS UNKNOWN — Monitor data unavailable" when there is no
|
|
42
|
+
usable data — this closes the old "0 monitors parsed looks like all-green" gap.
|
|
43
|
+
|
|
44
|
+
The display is read **from across a room**: a monitor changing to a bad state **beeps, pops up
|
|
45
|
+
and blinks**, and for as long as anything is down the **screen edges hold a standing colour
|
|
46
|
+
tint**.
|
|
47
|
+
|
|
48
|
+
The monitor data now comes from a **server-side, same-origin proxy** (`/v2/monitors/status/`,
|
|
49
|
+
`App_Clickup_Monitors`) — the browser no longer POSTs cross-origin to OneUptime. This is the
|
|
50
|
+
**read/consumer** side of OneUptime, unrelated to the push-heartbeat monitors the worker tiers
|
|
51
|
+
report *into* — see
|
|
36
52
|
[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
|
-
|
|
53
|
+
and [OneUptime external uptime monitoring for 1.0 workers](../../worker/features/oneuptime-worker-uptime-monitoring.md).
|
|
54
|
+
Those docs define what *creates* the statuses this panel renders.
|
|
39
55
|
|
|
40
56
|
## How it works
|
|
41
57
|
|
|
42
58
|
### One document, not a nested iframe
|
|
43
59
|
|
|
44
|
-
The panel is implemented as React components **in the same `sprint-dashboard.html` document**
|
|
45
|
-
|
|
46
|
-
already iframes this page, so
|
|
47
|
-
|
|
60
|
+
The panel is implemented as React components **in the same `sprint-dashboard.html` document** as
|
|
61
|
+
the sprint board (via CDN React, no build step). It is deliberately **not** a nested iframe:
|
|
62
|
+
`mvc/developer/get.php` already iframes this page, so a second frame would load a second React
|
|
63
|
+
runtime and a duplicate poll. The sprint board component is `SprintBoard`, wrapped by a
|
|
64
|
+
`Dashboard` that owns the monitor poll and passes it down as props, so the status panel and the
|
|
65
|
+
alert watcher share **one** fetch loop.
|
|
66
|
+
|
|
67
|
+
Only the monitor fetch runs at `MONITOR_REFRESH_MS`; sprint data stays on the page's 5-minute
|
|
68
|
+
`AUTO_REFRESH_MS`.
|
|
69
|
+
|
|
70
|
+
### Data source — server-side proxy `/v2/monitors/status/` (`App_Clickup_Monitors`)
|
|
71
|
+
|
|
72
|
+
The client `useMonitors` hook now does a **same-origin GET** to `/v2/monitors/status/` and
|
|
73
|
+
unwraps `.data` from the api2 envelope — the SSO session cookie authenticates it like every other
|
|
74
|
+
`/v2/*` call. The old cross-origin browser POST to `oneuptime.com`, the client-side
|
|
75
|
+
`buildMonitorModel` / `monRole` / `monVal` parsers, and the hardcoded `MONITOR_STATUS_PAGE_ID`
|
|
76
|
+
were **removed from the HTML**.
|
|
48
77
|
|
|
49
|
-
|
|
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.
|
|
78
|
+
`App_Clickup_Monitors::serve()` (`_/app/clickup/monitors.php`) mirrors `App_Clickup_Sprint`:
|
|
53
79
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
80
|
+
- Same **SSO / persona JSON auth gate** (`App_Auth::isLoggedIn()` + persona check, JSON 401/403
|
|
81
|
+
on failure), same **api2 `{success, data, errors}` envelope**, uuid4 transaction id.
|
|
82
|
+
- Server-side **curl POST** to the OneUptime public status-page overview API, reducing the
|
|
83
|
+
**~250 KB** upstream payload to a **~1 KB** model:
|
|
84
|
+
`{total, buckets, rows[{key, name, group, statusName, role, since-epoch-ms}], overallName,
|
|
85
|
+
overallRole}`.
|
|
86
|
+
- Caches ~5s via **APCu**, so one upstream fetch is shared across all open display tabs and every
|
|
87
|
+
tab shows one consistent fresh snapshot.
|
|
88
|
+
- Status-page ID is a server constant `DEFAULT_STATUS_PAGE_ID`, overridable via
|
|
89
|
+
`[oneuptime] status_page_id` in config (location only — no value recorded here).
|
|
57
90
|
|
|
58
|
-
|
|
91
|
+
This eliminated the app's **only** cross-origin browser dependency and its CORS / edge-cache /
|
|
92
|
+
mixed-context fragility (the direct browser poll was silently not re-polling), and cut traffic
|
|
93
|
+
from ~1 MB/min per tab to ~1 KB per poll.
|
|
59
94
|
|
|
60
|
-
|
|
95
|
+
### Upstream contract — OneUptime public status-page overview API (now consumed server-side)
|
|
96
|
+
|
|
97
|
+
Reusable for any future status display; the parsing now lives in PHP, not the browser. Endpoint:
|
|
61
98
|
|
|
62
99
|
```
|
|
63
100
|
POST https://oneuptime.com/status-page-api/overview/<statusPageId>
|
|
64
101
|
body: {}
|
|
65
102
|
```
|
|
66
103
|
|
|
67
|
-
- **It is a POST, not a GET**, and
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
so the browser can call it cross-origin with **no proxy and no credentials**. This is the one
|
|
72
|
-
cross-origin call on a page whose every other fetch is same-origin `/v2/*`.
|
|
73
|
-
- The response is **~250 KB**.
|
|
74
|
-
- Values arrive as **typed envelopes** — `{_type: "ObjectID" | "DateTime" | "Color", value: …}`
|
|
75
|
-
— so read `.value`, never the object. Dates are ISO-8601 UTC with a trailing `Z`.
|
|
104
|
+
- **It is a POST, not a GET**, and returns **raw JSON** — **not** the api2 envelope. The payload
|
|
105
|
+
**is** the data.
|
|
106
|
+
- Values arrive as **typed envelopes** — `{_type: "ObjectID" | "DateTime" | "Color", value: …}` —
|
|
107
|
+
so read `.value`. Dates are ISO-8601 UTC (`Z`).
|
|
76
108
|
- A monitor's **current** state is `statusPageResources[].monitor.currentMonitorStatusId`,
|
|
77
|
-
resolved against
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
-
|
|
90
|
-
|
|
91
|
-
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
resolved against `monitorStatuses[]`; the open timeline interval (`endsAt: null`) is when the
|
|
110
|
+
current state began ("in this state for X").
|
|
111
|
+
- **Discovery — the id-shape asymmetry.** In the overview payload a monitor status's own `_id` is
|
|
112
|
+
a **plain string**, but the resource's `currentMonitorStatusId` is a **typed envelope**
|
|
113
|
+
(`{_type:'ObjectID', value}`). Unwrap the resource id; the status `_id` is already plain. (This
|
|
114
|
+
corrected an earlier mis-diagnosis that the client parser was broken — it was fine; the point is
|
|
115
|
+
now moot since parsing is server-side.)
|
|
116
|
+
|
|
117
|
+
Status bucketing is **derived, not hardcoded**: `isOperationalState` → good; worst-priority
|
|
118
|
+
non-operational → offline/critical; anything between → degraded. A renamed or new status still
|
|
119
|
+
buckets correctly.
|
|
120
|
+
|
|
121
|
+
### Panel presentation — state-adaptive (supersedes the solid status circle)
|
|
122
|
+
|
|
123
|
+
The old design (a solid `MonitorCircle`, a per-status legend with counts + percentages, and a
|
|
124
|
+
redundant status pill) was **removed**, and the panel header renamed **"Monitor status" →
|
|
125
|
+
"Status"**. The governing decision (developer's "Option 2"): **calm when healthy, loud when
|
|
126
|
+
broken.**
|
|
127
|
+
|
|
128
|
+
- **`healthy` now requires `total > 0 && rows.length === 0`.** A green hero can no longer show on
|
|
129
|
+
an empty/malformed response — that renders **Unknown** instead (see below).
|
|
130
|
+
- **Green** — a single calm full-height green hero (soft glow, dark panel — not a saturated fill),
|
|
131
|
+
full-bleed flush to the pane edges (no header, no inner padding, no hero border; clipped by the
|
|
132
|
+
pane's `overflow:hidden` + rounded corners).
|
|
133
|
+
- **Degraded / outage** — a saturated amber/red band + the affected-monitor cards, which grow to
|
|
134
|
+
share the column height (min-height floor, scroll when many). Icon centered above the text,
|
|
135
|
+
larger, role-colored full border, wrapping names. Each card uses a **dark, role-tinted
|
|
136
|
+
background with a soft radial glow** (amber for degraded, red for offline) rather than a
|
|
137
|
+
near-black fill, so a single card that grows to fill the column reads as amber/red — not a
|
|
138
|
+
black void — while the saturated icon + status text stay legible on the tint.
|
|
139
|
+
|
|
140
|
+
**Tuned for a dark wall TV.** The incident-card tints were brightened for a dark display (glow
|
|
141
|
+
~0.34–0.38, lifted base fills), and the green "operational" hero and grey "Unknown" panel were
|
|
142
|
+
brightened to match. Card fonts + icon, and the green hero's check / title / subtitle, were
|
|
143
|
+
enlarged for across-the-room readability.
|
|
144
|
+
|
|
145
|
+
### Unknown state — closes the false-green gap
|
|
146
|
+
|
|
147
|
+
An empty/malformed OneUptime response (`total === 0`) or a fetch error with **no prior good
|
|
148
|
+
model** now renders a neutral grey **"STATUS UNKNOWN — Monitor data unavailable"**, never the
|
|
149
|
+
green hero. The server proxy leaves `total: 0` as-is for the client to treat as Unknown. This is
|
|
150
|
+
the durable fix for the previously-documented "0 monitors parsed = reassuring all-green" known
|
|
151
|
+
issue: a total loss of visibility no longer clears a standing red screen to green, and the mass
|
|
152
|
+
row drop-out is no longer mistaken for a full recovery.
|
|
110
153
|
|
|
111
154
|
### Alerting — edge-triggered events over a level-triggered state
|
|
112
155
|
|
|
113
|
-
|
|
114
|
-
them up is the bug:
|
|
156
|
+
Two separate mechanisms; mixing them up is the bug:
|
|
115
157
|
|
|
116
158
|
| | Trigger | Drives | Why |
|
|
117
159
|
|---|---|---|---|
|
|
118
|
-
| **Event** | **Edge** —
|
|
119
|
-
| **State** | **Level** —
|
|
120
|
-
|
|
121
|
-
**
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
recovery fires no popup.
|
|
151
|
-
- **Blink** — the tint element gains `.is-blinking` for the 5s pulse, then the colour **holds**.
|
|
152
|
-
**Bad-state events only** — recovery fires no blink and no tint.
|
|
153
|
-
|
|
154
|
-
**Recovery event — audio-only, by design.** `useMonitorAlerts` also computes `recovered` = keys
|
|
155
|
-
that were non-operational in the previous poll and have **dropped out of `model.rows`** this poll,
|
|
156
|
-
and calls `playBeeps('recovered')` (the rising chime) for them. It deliberately fires **no popup
|
|
157
|
-
and no screen tint**: the level-triggered standing tint already clears itself when a row leaves
|
|
158
|
-
the set, and a green "all clear" popup/flash would fight the "the board only shouts when something
|
|
159
|
-
is wrong" design. So recovery is silent **visually** but audible. Two edge rules: the **first
|
|
160
|
-
poll is still the baseline** (opening the page mid-outage does not chime), and if a bad-state
|
|
161
|
-
change and a recovery land in the **same 15s poll the bad-state alert wins** that tick, avoiding
|
|
162
|
-
overlapping sounds (simultaneous transitions are rare).
|
|
160
|
+
| **Event** | **Edge** — on the transition | beeps, popup, blink | A level-triggered event re-alarms every poll for a monitor down an hour → an unsilenceable siren. |
|
|
161
|
+
| **State** | **Level** — current state | standing edge tint | An edge-triggered state would show a clean screen when you open the page mid-outage. **The board must never look healthy while it isn't.** |
|
|
162
|
+
|
|
163
|
+
**Alert hold — popup and blink both 15s.** `useMonitorAlerts` returns `{alert, blink}` on
|
|
164
|
+
separate timers, but both now hold for **15000ms**: the centre popup (`ALERT_POPUP_MS`) and the
|
|
165
|
+
screen-edge blink (`ALERT_BLINK_MS`) each stay up a full 15s so a fresh alert is readable and
|
|
166
|
+
visible across the room. (`prefers-reduced-motion` still kills the fast edge pulse — see Gotchas.)
|
|
167
|
+
Previously both shared one 5s `ALERT_HOLD_MS`.
|
|
168
|
+
|
|
169
|
+
**Event — `useMonitorAlerts`** compares each poll's non-operational rows against the previous:
|
|
170
|
+
|
|
171
|
+
- The **first successful poll is the baseline**, so opening the page mid-outage is silent (the
|
|
172
|
+
standing tint still shows immediately, being level-triggered).
|
|
173
|
+
- Fires on **any change of non-operational state** (`r.role !== previous.get(r.key)`), so
|
|
174
|
+
good→degraded, good→offline, degraded→offline **and offline→degraded** all alert.
|
|
175
|
+
- It remembers each monitor's **full previous-poll row** (key→row), so recovered monitors can be
|
|
176
|
+
named by the recovery dialog below.
|
|
177
|
+
- **Sound** — Web Audio beeps synthesised at runtime (no audio asset). A per-role `ALERT_SOUND`
|
|
178
|
+
table (count / freq / per-beep `step` / duration / gap / volume / `wave`) read by
|
|
179
|
+
`playBeeps(role)`. Current tuning: warning 3× square @ 720 Hz (flat), critical 4× square @
|
|
180
|
+
410 Hz (flat), recovered 3× square rising chime 620 → 800 → 980 Hz (freq 620, step 180).
|
|
181
|
+
- **Popup + blink** on **bad-state** events.
|
|
182
|
+
|
|
183
|
+
**Recovery event — dialog + chime, but no edge flash.** Keys that were non-operational last poll
|
|
184
|
+
and dropped out of `model.rows` this poll play the rising recovery chime **and** now show the
|
|
185
|
+
**centre dialog for 15s**: a green, check-icon "N monitor(s) OPERATIONAL" / "Back to operational"
|
|
186
|
+
that lists each recovered monitor as "Operational" (named from the remembered previous-poll rows).
|
|
187
|
+
The recovery branch sets a `role:'good'` alert (`AlertOverlay` has an `alert.role === 'good'`
|
|
188
|
+
variant) but deliberately calls **no `setBlink`** — recovery fires **no screen-edge flash/tint**,
|
|
189
|
+
because a green all-clear must not strobe the room and the level-triggered standing tint already
|
|
190
|
+
self-clears when a row leaves the set. First poll is still the baseline; if a bad-state change and
|
|
191
|
+
a recovery land in the same poll, the bad-state alert wins that tick.
|
|
163
192
|
|
|
164
193
|
**State — `standingRole(model)`** returns `critical` if any row is offline, else `warning` if any
|
|
165
|
-
is degraded, else `null
|
|
166
|
-
|
|
194
|
+
is degraded, else `null`. The tint element renders only while `standingRole` is non-null, so full
|
|
195
|
+
recovery clears it. The tint (worst-current) and the popup (the event) can legitimately differ:
|
|
196
|
+
a degraded-event popup can sit on a red screen because something else is still offline — correct,
|
|
197
|
+
not a bug.
|
|
167
198
|
|
|
168
|
-
|
|
169
|
-
**worst-current** state while the popup reflects **the event**, so a degraded-event popup can sit
|
|
170
|
-
on a red screen because something else is still offline. That is correct, not a bug.
|
|
199
|
+
### Two colour pairs: meaning vs. legibility
|
|
171
200
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
joined (priority) → amber when offline cleared → no tint on full recovery, with the blink
|
|
177
|
-
stopping after 5s and the colour held. Circle verified against the DOM: `#0ca30c` all-clear,
|
|
178
|
-
`#fab219` degraded only, `#d03b3b` with 1 degraded + 1 offline, and `textNodesInSvg = 0`.
|
|
201
|
+
`MON_COLOR` is the **validated status palette** and drives everything that **carries meaning**.
|
|
202
|
+
`MON_COLOR_BRIGHT` (`good:#25d825, warning:#ffd633, critical:#ff4d4d`) is used **only** for the
|
|
203
|
+
full-screen edge rim, where the only job is legibility from across a room. Do not let the bright
|
|
204
|
+
pair leak into anything semantic.
|
|
179
205
|
|
|
180
|
-
|
|
206
|
+
Both reach the overlay as CSS custom properties (`--alert-color`, `--alert-bright`). The rim is a
|
|
207
|
+
three-layer `box-shadow` (solid hot rim + tight bright bloom + a deep haze that stays on the base
|
|
208
|
+
hue so the dashboard stays readable), each bright layer written
|
|
209
|
+
`var(--alert-bright, var(--alert-color))` so it degrades to the base colour.
|
|
210
|
+
|
|
211
|
+
Standing and blinking differ in intensity **and tempo**, keeping "still broken" distinct from
|
|
212
|
+
"something just changed":
|
|
213
|
+
|
|
214
|
+
- `.alert-flash` (standing) — 12px rim, **slowly breathes** opacity 0→1→0 on a **4-second**
|
|
215
|
+
ease-in-out loop (`@keyframes alert-breathe`, 2s fade-in / 2s fade-out).
|
|
216
|
+
- `.alert-flash.is-blinking` (event) — 20px rim, `opacity: 1`, plus the fast **0.7s** pulse. Its
|
|
217
|
+
compound selector (0,2,0) out-specifies the standing breathe (0,1,0).
|
|
181
218
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
(`good:#25d825, warning:#ffd633, critical:#ff4d4d`) is used **only** for the full-screen edge rim,
|
|
185
|
-
where nothing depends on its exact value and the only job is legibility from across a room. Do
|
|
186
|
-
not let the bright pair leak into anything semantic.
|
|
187
|
-
|
|
188
|
-
Both reach the overlay as CSS custom properties — `--alert-color` and `--alert-bright`. The rim is
|
|
189
|
-
a **three-layer `box-shadow`**: solid hot rim + tight bright bloom + a deep haze that deliberately
|
|
190
|
-
stays on the **base** hue, so the dashboard underneath stays readable instead of washing out. Each
|
|
191
|
-
bright layer is written `var(--alert-bright, var(--alert-color))` so it **degrades to the base
|
|
192
|
-
colour** if the property never reaches the element.
|
|
193
|
-
|
|
194
|
-
Standing and blinking differ in intensity **and in animation tempo**, which is what keeps
|
|
195
|
-
"still broken" distinguishable from "something just changed":
|
|
196
|
-
|
|
197
|
-
- `.alert-flash` (standing) — 12px rim, **slowly breathes** its opacity 0 → 1 → 0 on a
|
|
198
|
-
**4-second ease-in-out loop** (`@keyframes alert-breathe { 0%,100% { opacity: 0 } 50% {
|
|
199
|
-
opacity: 1 } }`, applied `animation: alert-breathe 4s ease-in-out infinite` — a 4s cycle =
|
|
200
|
-
2s fade-in / 2s fade-out). It may sit on screen for hours, so the gentle breathe draws a
|
|
201
|
-
distant eye without reading as urgent. The tempo was tuned with the developer.
|
|
202
|
-
- `.alert-flash.is-blinking` (event) — 20px rim, `opacity: 1`, plus the fast **0.7s** pulse.
|
|
203
|
-
Its compound selector (0,2,0) out-specifies the standing breathe (0,1,0), so a just-changed
|
|
204
|
-
monitor shows the urgent fast blink, not the slow breathe.
|
|
205
|
-
|
|
206
|
-
The base `.alert-flash { opacity: .85 }` is the **reduced-motion fallback**, not the animated
|
|
207
|
-
default — see the `prefers-reduced-motion` gotcha. It is deliberately non-zero so a
|
|
208
|
-
reduced-motion viewer never lands on a fully-invisible (opacity 0) glow.
|
|
219
|
+
The base `.alert-flash { opacity: .85 }` is the **reduced-motion fallback**, deliberately non-zero
|
|
220
|
+
so a reduced-motion viewer never lands on a fully-invisible glow.
|
|
209
221
|
|
|
210
222
|
## Gotchas
|
|
211
223
|
|
|
224
|
+
- **Never call `curl_close()` in App_ code — the strict handler turns its PHP 8.5 deprecation into
|
|
225
|
+
a fatal (prod incident CV-2).** `App_Clickup_Monitors` originally called `curl_close($ch)`. On
|
|
226
|
+
the production Elastic Beanstalk PHP (8.5) `curl_close()` is **deprecated**, and the App_
|
|
227
|
+
framework's strict error handler promotes that `E_DEPRECATED` to a **fatal**, so
|
|
228
|
+
`/v2/monitors/status/` 500'd with an HTML error page (Sentry id CV-2). Fix: remove the call
|
|
229
|
+
entirely — `curl_close()` has been a **no-op since PHP 8.0** (the handle is a GC-freed
|
|
230
|
+
`CurlHandle`); use `unset($ch)` if you want to drop the reference. Same fatal-from-strict-handler
|
|
231
|
+
family as the E_ALL / `buildArrayOfRows`-by-ref gotcha. The endpoint's final catch was also
|
|
232
|
+
broadened from `catch (Exception)` to **`catch (\Throwable)`** so any catchable `Error` degrades
|
|
233
|
+
to the JSON "Status unknown" envelope instead of a 500 page.
|
|
234
|
+
- **`crypto.randomUUID()` is undefined on a non-secure origin — it crashed React to a black
|
|
235
|
+
screen.** The page sets a per-request `transactionId` header using `crypto.randomUUID()`, which
|
|
236
|
+
exists **only in a secure context** (https, or http on `localhost`/`127.0.0.1`). Served over
|
|
237
|
+
plain HTTP by hostname / LAN IP (e.g. `http://tools/…`, "Not secure"), it is `undefined` and
|
|
238
|
+
throws **before React mounts** → a black screen. Fixed with a `transactionId()` helper:
|
|
239
|
+
`crypto.randomUUID` if present → else `crypto.getRandomValues` (a real v4) → else a non-crypto
|
|
240
|
+
id. **Reusable for any browser code that must run over a non-secure origin** — never call
|
|
241
|
+
`crypto.randomUUID`/`crypto.subtle` unguarded.
|
|
212
242
|
- **The embedding iframe needs `allow="autoplay"` or the alert sound is silently blocked.**
|
|
213
|
-
Browser autoplay policy is scoped **per-iframe**. `mvc/
|
|
214
|
-
|
|
215
|
-
beeps would never have sounded in the Tools app — with no error. It is now
|
|
216
|
-
`allow="fullscreen; autoplay"`. Any future iframed page that makes sound needs the same.
|
|
243
|
+
Browser autoplay policy is scoped **per-iframe**. `mvc/developer/get.php` embeds the dashboard
|
|
244
|
+
with `allow="fullscreen; autoplay"`. Any future iframed page that makes sound needs the same.
|
|
217
245
|
- **Permission is not enough — browsers still require one user gesture per session.** The page
|
|
218
|
-
unlocks the `AudioContext` on the first click/keypress
|
|
219
|
-
|
|
220
|
-
|
|
246
|
+
unlocks the `AudioContext` on the first click/keypress; if an alert fires while audio is still
|
|
247
|
+
blocked the popup says so rather than failing silently. On an **unattended wall display nobody
|
|
248
|
+
clicks**, so alerts stay visual-only. For kiosk use launch Chrome with
|
|
221
249
|
`--autoplay-policy=no-user-gesture-required`.
|
|
222
|
-
- **The `prefers-reduced-motion` override needs the compound selector.**
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
future state/modifier CSS split has the same trap. Because it already names `.alert-flash`,
|
|
228
|
-
this same rule **also covers the new slow `alert-breathe` breathe** — a reduced-motion viewer
|
|
229
|
-
falls back to the static `opacity: .85` glow with **no separate rule needed**.
|
|
230
|
-
- **A parse failure currently looks calmer than an outage** (known issue, not fixed). If the
|
|
231
|
-
OneUptime call returns HTTP 200 with a malformed or empty body, `buildMonitorModel` yields
|
|
232
|
-
total 0, empty buckets and empty rows. The status pill correctly degrades to "Unknown", but
|
|
233
|
-
the affected-list card still reads **"All 0 monitors are operational"**, and any
|
|
234
|
-
previously-degraded monitor silently disappears — and, worse now that recovery is audible,
|
|
235
|
-
the mass drop-out is **indistinguishable from a full recovery**, so the panel plays the happy
|
|
236
|
-
**recovery chime** for every vanished monitor on a parse failure. Since the standing
|
|
237
|
-
tint and the status circle are both driven by that same empty row set, a parse failure also
|
|
238
|
-
**clears a standing red screen back to green** — the most reassuring possible display of a
|
|
239
|
-
total loss of visibility. Recommended fix: treat "zero resources parsed" as an **error
|
|
240
|
-
state**, not a healthy one.
|
|
241
|
-
|
|
242
|
-
## Recommended next step (advisory — not built)
|
|
243
|
-
|
|
244
|
-
Replace the direct browser → OneUptime poll with a small **server-side proxy endpoint** in the
|
|
245
|
-
Tools app (e.g. `/v2/monitors/status/`) that fetches upstream, reduces the payload to the
|
|
246
|
-
summary, and caches ~5s. Rationale:
|
|
247
|
-
|
|
248
|
-
- The browser currently pulls **~250 KB every 15s (~1 MB/min per open tab)** to display about
|
|
249
|
-
six numbers; a proxy makes it ~1 KB.
|
|
250
|
-
- One upstream fetch is shared across **all** open displays instead of one per tab.
|
|
251
|
-
- It removes the cross-origin dependency, and therefore the CORS/autoplay coupling above.
|
|
252
|
-
- The status page ID moves into **config** instead of hardcoded HTML.
|
|
253
|
-
- It gives a natural home for **server-side transition history**, which is what edge-triggered
|
|
254
|
-
alerting really wants (today the baseline resets every time a tab reloads).
|
|
250
|
+
- **The `prefers-reduced-motion` override needs the compound selector.** Once the blink moved to
|
|
251
|
+
`.alert-flash.is-blinking` (0,2,0), a reduced-motion rule on `.alert-flash` alone (0,1,0) could
|
|
252
|
+
no longer override it. The rule must list both:
|
|
253
|
+
`.alert-flash, .alert-flash.is-blinking { animation: none; }`. Because it already names
|
|
254
|
+
`.alert-flash`, this same rule also covers the slow `alert-breathe` breathe.
|
|
255
255
|
|
|
256
256
|
## Change history
|
|
257
|
-
- 2026-08-18 —
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
`
|
|
265
|
-
`
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
257
|
+
- 2026-08-18 — **State-adaptive redesign + server-side proxy + Unknown state + /developer rename.**
|
|
258
|
+
Replaced the solid `MonitorCircle` / legend / percentages / status pill with a calm full-height
|
|
259
|
+
green hero (full-bleed flush in the green state) vs. a saturated amber/red band with growing,
|
|
260
|
+
wrapping affected-monitor cards on a dark, role-tinted radial-glow background (brightened for a
|
|
261
|
+
dark TV, larger type); renamed the panel header to **"Status"** and widened the monitor column
|
|
262
|
+
(sprint board 80%→75%). Added a neutral grey **Unknown** state (`healthy` now requires
|
|
263
|
+
`total > 0 && rows.length === 0`), closing the old "0 parsed = all-green" gap. Both the centre
|
|
264
|
+
popup (`ALERT_POPUP_MS`) and the screen-edge blink (`ALERT_BLINK_MS`) now hold **15s**
|
|
265
|
+
(`useMonitorAlerts` returns `{alert, blink}` on separate timers). **Recovery** now shows a green
|
|
266
|
+
check-icon dialog for 15s naming the recovered monitors (a `role:'good'` `AlertOverlay` variant,
|
|
267
|
+
driven by remembered previous-poll rows) in addition to the rising chime, but still fires **no
|
|
268
|
+
edge flash/tint**. Monitor data now comes from a **server-side same-origin proxy**
|
|
269
|
+
`/v2/monitors/status/` (`App_Clickup_Monitors`, APCu ~5s cache, ~250 KB→~1 KB, status-page ID in
|
|
270
|
+
config), removing the client's cross-origin OneUptime POST and its parsers; recorded the
|
|
271
|
+
OneUptime id-shape asymmetry (resource `currentMonitorStatusId` is a typed
|
|
272
|
+
`{_type:'ObjectID',value}` envelope, the status `_id` is plain). Fixed prod incident **CV-2**:
|
|
273
|
+
removed `curl_close()` (deprecated in PHP 8.5 → fatal under the App_ strict handler) and
|
|
274
|
+
broadened the endpoint catch to `\Throwable`. Route renamed `/clickup/react` → `/developer`.
|
|
275
|
+
Added the `crypto.randomUUID()` non-secure-origin black-screen gotcha. (jcardinal)
|
|
276
|
+
- 2026-08-18 — Wall-display cadence tuning. Standing edge-tint breathe cycle 5s→**4s**;
|
|
277
|
+
`MONITOR_REFRESH_MS` 5s→**15s** (outage detection now reacts within ~15s). Corrected prior doc
|
|
278
|
+
drift (breathe had been documented as 7s but code was 5s). (jcardinal)
|
|
279
|
+
- 2026-08-14 — Generalised alert-sound synthesis and added a **recovery chime** (per-role
|
|
280
|
+
`ALERT_SOUND` table, retuned degraded 3×720 / offline 4×410 / recovered rising 620→800→980);
|
|
281
|
+
recovery is audio-only by design (no popup/tint). First poll stays the silent baseline;
|
|
282
|
+
bad-state change wins over a same-tick recovery. (jcardinal)
|
|
283
|
+
- 2026-08-14 — Standing edge tint slowly breathes opacity 0→1→0 (`@keyframes alert-breathe`);
|
|
284
|
+
fast 0.7s `.is-blinking` event pulse out-specifies it; reduced-motion falls back to static
|
|
285
|
+
`opacity: .85`. (jcardinal)
|
|
286
|
+
- 2026-08-14 — Built the monitor status column (split layout, solid status circle + legend +
|
|
287
|
+
degraded/offline card) as components in the same document sharing one poll; documented the
|
|
288
|
+
OneUptime public status-page overview API contract and edge-vs-level alerting; added the
|
|
289
|
+
`MON_COLOR_BRIGHT` rim palette and the iframe autoplay / user-gesture / reduced-motion gotchas.
|
|
290
|
+
(kyalamarthi)
|
|
291
|
+
</content>
|
|
292
|
+
</invoke>
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
| [_Component_*/_Model_* project-namespace registration (autoloader) & backslash-qualify traps](features/component-model-namespace-registration.md) | Every **project-local** `_Component_*` and `_Model_*` class in a 2.0 app **must declare the project namespace** at the top of the file: ```php namespace <NAMESP | _underscore/Loader.php, worker2/_.php, api2/_.php, worker2/Component/Forecast/Db/Db.php, worker2/Component/Forecast/SaleImport/SaleImport.php, worker2/Component/Api/Oneuptime/Oneuptime.php, api2/Component/Api/Netsuite/Netsuite.php, _underscore/Component/Api/Paypal/Paypal.php |
|
|
18
18
|
| [_Config group access — the two-argument "optional" form does NOT rescue a missing GROUP](features/config-group-access.md) | `_Config::<group>('<property>')` reads a value out of the active `Config/<ENVIRONMENT>.ini`, and **it throws when the requested group is absent from the ini.** | _underscore/Config.php, _underscore/Component/Api/Paypal/Paypal.php, _underscore/Worker.php, api2/Config/beta.ini, api2/Config/sandbox-dev.ini |
|
|
19
19
|
| [Re-pointing a DB alias mid-request (_Database::register park/restore)](features/database-alias-repointing.md) | `_Database` keys **all live per-database runtime state by the connection ALIAS** (`Client` / `_underscore::DB_CLIENT`, `ClientLogs`, `Archive`), **not** by the | _underscore/Database.php, _underscore/Query.php, api2/Component/Api/V2/V2.php, api2/Component/Api/CrossClient/CrossClient.php |
|
|
20
|
-
| [2.0 Email Send Pipeline (queue + Send worker)](features/email-send-pipeline.md) | In 2.0, `_Email::send()` **does not transmit** — it queues the message. | _underscore/Email.php, _underscore/String.php, worker2/Worker/Infrastructure/Email/Send.php |
|
|
20
|
+
| [2.0 Email Send Pipeline (queue + Send worker)](features/email-send-pipeline.md) | In 2.0, `_Email::send()` **does not transmit** — it queues the message. | _underscore/Email.php, _underscore/String.php, worker2/Worker/Infrastructure/Email/Send.php, _underscore/Model/Client/Logs/Email.php, _underscore/Model/Client/Logs/EmailAttachment.php |
|
|
21
21
|
| [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
|
|
22
22
|
| [Error Reporting — Issue/Event Capture, Fingerprinting & Aggregation](features/error-reporting-issue-event.md) | Platform-wide error reporting for TOGA 2.0, built on an **Issue / Event** aggregation model in the **shared Core Logs DB**. | _underscore/Error.php, _underscore/Database.php, _underscore/Exception/Business.php, api2/Controller/Index.php, worker2/Controller/Index.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, _underscore/Model/Core/Logs/IssueFingerprint.php, _underscore/Model/Core/Logs/IssueClickupTask.php, _underscore/Model/Core/Logs/IssueEmailAddress.php, _underscore/Model/Core/Logs/IssueAreaOwner.php, dbchanges2/Logs/2026-07-30a - Error reporting Issues and Events.sql, dbchanges2/Logs/2026-08-01a - Rename tables to singular.sql, dbchanges2/Logs_Client/2026-08-01a - Drop Error table.sql, dbchanges2/Core/2026-07-30a - Error escalation cron job.sql |
|
|
23
23
|
| [Record-Changed Event Publishing (_Event::publish to SQS)](features/event-publish-sqs.md) | `_Event::publish()` (in `_underscore/Event.php`) is the PHP side of the real-time event pipeline. | _underscore/Event.php |
|