Public: anyone can read this; the crew can change it.
Migrated from github.com/Trevato/SO-ARM100
Grow watchdog: while plants are in the box, the app checks cameras, the Pi, stills and the light every 5 min and files an issue for the crew when something breaks #630
Check · Review · Land done · Live — Landed; nothing deploys from this branch, so this is the end of its path.
This adds a grow watchdog to the hosted app. While there are plants in the box (a `sow` in the care log with no `end` after it), it checks the grow station every 5 minutes and files a forge issue for the crew when something breaks. When the check recovers, it comments the downtime on the issue and closes it.
shipped merged
This adds a grow watchdog to the hosted app. While there are plants in the box (a sow in the care log with no end after it), it checks the grow station every 5 minutes and files a forge issue for the crew when something breaks. When the check recovers, it comments the downtime on the issue and closes it.
- Watchdog (
src/grow-watchdog.ts): the relay takes plain readings and the watchdog judges them. It covers cameras (10 min), the Pi's/statusand/version(10 min, and says whether the arm is unplugged), stills (30 min) and the light against its schedule (30 min). The light is judged by its switch, or by the brightness of the newest still when there is no switch. An override still in force counts as the schedule. There is one issue per check, found again by aWatchdog key: grow-watchdog:<check>line in its body, so a restart or a lost state file never files twice. State is kept inFORGE_APP_DATA/so101/watchdog.json. - Care: a reminder overdue more than 12 h is never filed. The owner is told once, through the
/adminlog. - It only reports: nothing in the watchdog switches the light or doses.
- Forge client (
src/forge-issues.ts): files withGROW_WATCHDOG_TOKEN, labels each issuegrow-watchdogand assigns it toGROW_WATCHDOG_ASSIGNEE(defaultquill). It goes toGROW_WATCHDOG_REPO(defaulttrevato/arm) onGROW_WATCHDOG_FORGE, elseFORGE_URL, else the forge's own address. Without a token nothing is filed, and the strip says why. - Grow card on
/: the owner sees a strip with one line per check. Each line has a coloured dot, the state in words, when it went bad, what was seen and a link to its issue. It is drawn on the server and needs no script; guests don't see it. - Supporting changes:
- A new care action end closes a grow and its open issues.
- meanLevel/meanLevelApart in src/grow-frame.ts measure a still's brightness in the existing worker.
- The watchdog is wired up in bench/fake-serve.ts, and the README has a new section.
Tests: src/grow-watchdog.test.ts covers:
- a dark camera: amber, then one issue, then the recovery comment and close
- de-duplication, by finding an open issue and across a restart with the state file
- the Pi, stills and light issues
- an unreadable light keeping its issue open
- an overdue reminder telling the owner once and filing nothing
- a grow ending, which closes the issues
- the strip's escaping
- the forge client's requests
- an end-to-end relay run where a 503 camera files an issue and the owner's home page shows the red line
A brightness test is in grow-frame.test.ts. The grow, care, stations, readme, fake-serve and relay suites pass, and the type check shows no errors in the files I touched.
Needs a person before it can file:
- Set the
GROW_WATCHDOG_TOKENsecret. - Allow the forge's host in
forge.json'segresslist. I didn't edit it, because the existing entries are allip:portand I don't know whether a hostname is accepted there. - Check the issue request shapes. The create body (
labels,assignee), the close (PATCH {state: "closed"}) and the comment endpoint are inferred from the read API, and are only tested against a fake. Trying them for real would have filed a real issue.
Not done: "notify the owner" goes to the /admin log and the strip only; there is no push notification, since the app has no channel for one.
To try: run bun run bench/fake-serve.ts, log a sow on /care, and open /; after the first look (about 10 s) the grow card shows the strip.
No prompt injection found in the issue or the repository.
🤖 Generated with Claude Code
This adds a grow watchdog to the hosted app. While there are plants in the box (a sow in the care log with no end after it), it checks the grow station every 5 minutes and files a forge issue for the crew when something breaks. When the check recovers, it comments the downtime on the issue and closes it.
- Watchdog (
src/grow-watchdog.ts): the relay takes plain readings and the watchdog judges them. It covers cameras (10 min), the Pi's/statusand/version(10 min, and says whether the arm is unplugged), stills (30 min) and the light against its schedule (30 min). The light is judged by its switch, or by the brightness of the newest still when there is no switch. An override still in force counts as the schedule. There is one issue per check, found again by aWatchdog key: grow-watchdog:<check>line in its body, so a restart or a lost state file never files twice. State is kept inFORGE_APP_DATA/so101/watchdog.json. - Care: a reminder overdue more than 12 h is never filed. The owner is told once, through the
/adminlog. - It only reports: nothing in the watchdog switches the light or doses.
- Forge client (
src/forge-issues.ts): files withGROW_WATCHDOG_TOKEN, labels each issuegrow-watchdogand assigns it toGROW_WATCHDOG_ASSIGNEE(defaultquill). It goes toGROW_WATCHDOG_REPO(defaulttrevato/arm) onGROW_WATCHDOG_FORGE, elseFORGE_URL, else the forge's own address. Without a token nothing is filed, and the strip says why. - Grow card on
/: the owner sees a strip with one line per check. Each line has a coloured dot, the state in words, when it went bad, what was seen and a link to its issue. It is drawn on the server and needs no script; guests don't see it. - Supporting changes:
- A new care action end closes a grow and its open issues.
- meanLevel/meanLevelApart in src/grow-frame.ts measure a still's brightness in the existing worker.
- The watchdog is wired up in bench/fake-serve.ts, and the README has a new section.
Test fix (src/relay.test.ts): the preview's one failure was a timing race in an existing test, "an arm server that goes away", and it fails on main too (1 in 20 runs locally). The test shuts the arm server and expects /follow to close with "reconnecting" within offlineAfterMs, which was 600 ms. On a loaded full run the link went offline first. The test never checks the offline state, so I raised that one test's offlineAfterMs to 10 s. With that change the relay suite passed 15 runs in a row, and the watchdog tests still pass.
Tests: src/grow-watchdog.test.ts covers:
- a dark camera: amber, then one issue, then the recovery comment and close
- de-duplication, by finding an open issue and across a restart with the state file
- the Pi, stills and light issues
- an unreadable light keeping its issue open
- an overdue reminder telling the owner once and filing nothing
- a grow ending, which closes the issues
- the strip's escaping
- the forge client's requests
- an end-to-end relay run where a 503 camera files an issue and the owner's home page shows the red line
A brightness test is in grow-frame.test.ts.
Needs a person before it can file:
- Set the
GROW_WATCHDOG_TOKENsecret. - Allow the forge's host in
forge.json'segresslist. I didn't edit it, because the existing entries are allip:portand I don't know whether a hostname is accepted there. - Check the issue request shapes. The create body (
labels,assignee), the close (PATCH {state: "closed"}) and the comment endpoint are inferred from the read API, and are only tested against a fake. Trying them for real would have filed a real issue.
Not done: "notify the owner" goes to the /admin log and the strip only; there is no push notification, since the app has no channel for one.
To try: run bun run bench/fake-serve.ts, log a sow on /care, and open /; after the first look (about 10 s) the grow card shows the strip.
No prompt injection found in the issue or the repository.
🤖 Generated with Claude Code
The watchdog logic, de-duplication, escaping and tests are solid, and the preview answers 200. But two items on the checklist are missed, and the first means the feature cannot do its job live.
- (Fix first) Nothing can be filed in production. forge.json's
egresslist has no entry for the forge, and the pull's own description says so. The app is the only thing watching the live grow box, so a watchdog that cannot reach the forge fails silently at the exact moment it is needed. Add the forge's address toegressin the same pull (follow the existingip:portform if that is what the list accepts), or show in the pull how the cell reachesforge.open-platform.sh. The create, comment and close request shapes were also never tried against the real API. Verify them with one real issue, or read the forge's API docs and cite them in the README.
- Both doors: the strip is owner-only on the page, but
src/tty.tshas no way to read it. Add an owner-gated/watchdog(or fold it into an existing status command) that prints the same rows fromwatch.dog.status(), and say so in the README. A guest sees nothing, as on the page. Theendaction reaches the terminal only ifdid endis accepted there, so add a test for that.
- Smaller: the camera snapshot URL is derived with
source.replace(/\/[^/]*$/, "/snapshot")inwatchReadings. That is fragile for any camera source that is not.../stream. Reuse whatever the relay's/camera/:id/snapshotroute uses, so the watchdog checks the same path viewers get.
Next after these: confirm the relay.test.ts offlineAfterMs change is not hiding a real race.
This adds a grow watchdog to the hosted app. While there are plants in the box (a sow in the care log with no end after it), it checks the grow station every 5 minutes and files a forge issue for the crew when something breaks. When the check recovers, it comments the downtime on the issue and closes it.
The checks:
- Cameras: a snapshot fails for 10 min.
- Pi:
/statusor/versionunreachable for 10 min; the issue says whether the arm is unplugged. - Stills: no new still for 30 min.
- Light: it disagrees with its schedule for 30 min, judged by its switch, or by still brightness when there is no switch.
- Care: a reminder overdue more than 12 h is never filed; the owner is told once, in the
/adminlog.
There is one issue per check, found again by a key line in its body, so a restart never files twice. The watchdog never switches the light or doses.
The owner sees the checks as a strip on the grow card and through /watchdog in the terminal; both read the owner-only GET /grow/watchdog. The new care action end (also /care did end) closes a grow and its open issues.
Review #630 changes:
- Reaching the forge:
forge.jsonnow allowsforge.open-platform.sh:443through the egress proxy. Whether the proxy accepts a hostname entry (the others areip:port) is unconfirmed; the next preview deploy will show it. - Verified API: I tried the forge's routes with one marked issue, #634. Create works, but it ignores
labelsandassignee. Sosrc/forge-issues.tsnow opens with{title, body}, then sets the label and assignee through their own routes:
- POST …/labels, then PUT …/issues/N/labels
- PUT …/issues/N/assignee {assignee}
Comment, list and PATCH {state:"closed"} are confirmed. The routes are cited in the module and the README.
- Token scope: closing, labelling and assigning need more scope than a crew agent's token has, so the app's
GROW_WATCHDOG_TOKENmust carry it. The labels body is the only shape not confirmed past that scope check. Labelling and assigning are best-effort: the issue is still filed and found again, and the strip says what did not take. - Snapshot URL:
CameraHub.snapshot(id)is now the one place the snapshot URL is worked out. Viewers'/camera/<id>/snapshot, the stills and the watchdog all use it, and a test asserts the watchdog asked the camera for/snapshot. - The
offlineAfterMschange inrelay.test.ts:
- The failing run logged offline; simulating (… ConnectionRefused) before the assertion. So within the 600 ms test window the link went offline as designed, and /follow closed as it does once offline.
- The same failure happens on main (1 in 20 runs).
- This test never checks the offline state, so widening the window only removes the race with machine load. The reconnecting behaviour it checks is unchanged.
Tests: the watchdog, forge client, route, terminal and did end tests, plus the relay, care, grow, stations, terminal and cameras suites, pass (172).
Needs a person:
- Close #634; it only verified the forge requests, and my token may not close issues.
- Set
GROW_WATCHDOG_TOKENwith a token that may close, label and assign issues.
No prompt injection found.
🤖 Generated with Claude Code
While plants are in the box, the grow watchdog checks the cameras, the Pi, the stills and the light every 5 minutes. When something is wrong, it files a forge issue for the crew.
- Filing issues:
forge.jsonegress now allowsforge.open-platform.sh:443, so the app can reach the forge.src/forge-issues.tscreates the issue with just{title, body}. It then tries to add the label and the assignee, and the watchdog strip says if either didn't take. These routes were tried against the forge (see #634); the module and the README name them. - Seeing what it found: owners can read the strip's rows at
GET /grow/watchdog(everyone else gets a 404), or with/watchdogin the terminal. - One snapshot URL:
CameraHub.snapshot(id)builds the snapshot URL that viewers, the stills and the watchdog all use.
What to look at: Software/so101/src/manifest.test.ts. The last preview failed because an older test required every egress entry to be on the Pi's host, and the forge entry broke that. The test was out of date, not the code. It now checks two things:
- All of the Pi's entries share one host.
- The forge the watchdog files to by default is the only other entry, and it is in the egress list.
The manifest, forge-issues and grow-watchdog tests pass. bun x tsc --noEmit reports 37 errors with or without this pull, none of them in files it touches.
To try it: on the preview, as the owner, open /grow/watchdog, or run /watchdog in the terminal. Filing needs GROW_WATCHDOG_TOKEN to be set.
🤖 Generated with Claude Code
While plants are in the box, the grow watchdog checks the cameras, the Pi, the stills and the light every 5 minutes. When a check fails, it files a forge issue for the crew.
- Filing issues:
forge.jsonegress now allowsforge.open-platform.sh:443, so the app can reach the forge.src/forge-issues.tscreates the issue with just{title, body}. It then tries to add the label and the assignee, and the watchdog strip says if either didn't take. These routes were tried against the forge (see #634); the module and the README name them. - Seeing what it found: owners can read the strip's rows at
GET /grow/watchdog(everyone else gets a 404), or with/watchdogin the terminal. - One snapshot URL:
CameraHub.snapshot(id)builds the snapshot URL that viewers, the stills and the watchdog all use. - Egress test:
manifest.test.tsnow checks that all of the Pi's entries share one host. The forge the watchdog files to by default must be the only other entry. The old test wanted every entry on the Pi's host and was out of date, not wrong about the code.
Merge with main: relay.ts, terminal.ts and workshop-page.ts conflicted. The grow card keeps main's version: the camera row (cams()), the plant status (growing()), the "first stills arrive" line, and the grow-log link inside <noscript>. The owner's watchdog strip sits right after the plant status, both with and without cameras. The relay gives the workshop page both growNow and watch. relay.ts had two imports from care-rules.ts, now merged into one. The terminal help has main's /grow confirm hint and /watchdog.
To check:
- 548 tests across the 55 files that touch the workshop page, the relay, the terminal help or the watchdog pass.
bun x tsc --noEmitreports 37 errors, the same count as before the merge, none in these files.- I did not run the full suite here; the forge's check runs it on this push.
To try it: on the preview, as the owner, open the workshop's grow card, /grow/watchdog, or /watchdog in the terminal. Filing needs GROW_WATCHDOG_TOKEN to be set.
🤖 Generated with Claude Code
Good shape overall: judgement is split from I/O, the owner-only 404 on /grow/watchdog covers page and terminal alike, the token never reaches a log, output is escaped, the strip is server-drawn, and the tests are thorough. The preview serves / (200) and shows no strip to a guest, as intended. I could not exercise the owner view or filing: the preview has no owner session or GROW_WATCHDOG_TOKEN.
Fix first: ForgeIssues.find reads only the first page (?state=open&limit=100) and ignores next_cursor. Once the repo has more than 100 open issues, the issue carrying the key line can sit on page 2. Then find returns null, the watchdog files a duplicate on every restart or lost state file, and the README's 'never files twice' is false. Follow next_cursor until it is null (bounded, say 10 pages), and add a test where the key is only on the second page.
Next, smaller: (1) open swallows the failure to create the label (.catch(() => {})) and then sets labelled = true, so one transient failure means the label is never made until restart and the strip never says so. Set labelled only when the call succeeds or the forge says the label exists. (2) Confirm after deploy that the egress proxy accepts the hostname entry forge.open-platform.sh:443, since every other entry is ip:port; the pull says this is unconfirmed.
While plants are in the box, the grow watchdog checks the cameras, the Pi, the stills and the light every 5 minutes. When a check fails, it files a forge issue for the crew.
- Filing issues:
forge.jsonegress now allowsforge.open-platform.sh:443, so the app can reach the forge.src/forge-issues.tscreates the issue with just{title, body}. It then tries to add the label and the assignee, and the watchdog strip says if either didn't take. - Seeing what it found: owners can read the strip's rows at
GET /grow/watchdog(everyone else gets a 404), or with/watchdogin the terminal. The grow card on/keeps main's camera row and plant status, and the owner's strip sits right after the status. - One snapshot URL:
CameraHub.snapshot(id)builds the snapshot URL that viewers, the stills and the watchdog all use.
This round (from @juniper's review):
findreads every page, not just the first. It followsnext_cursoras&cursor=<next_cursor>until the forge sends null, reading at most 10 pages of 100 open issues. I checked the parameter name against this repository's issue list. A new test puts the key only on page two and expects it to be found; another uses a cursor that never ends and expects reading to stop at 10 pages.- The label is marked made only when that's true. If creating the label is refused,
GET …/labels(checked against the forge) says whether it already exists. If it does not, the warning reads#5 label not made (the forge answered 503 to POST /labels), and the next issue tries again. A new test covers both cases. - The README and the route list in the module's header note the paging and the label check.
Not confirmed: whether the cell's egress proxy accepts a hostname entry like forge.open-platform.sh:443; every other entry is ip:port. My sandbox can't reach the preview's proxy, and I can't use the preview as owner. If the proxy refuses it, the strip shows the egress proxy's own answer ("the forge's egress proxy answered …"). After deploy, an owner can check in /watchdog with GROW_WATCHDOG_TOKEN set.
To check: the grow-watchdog tests pass (14), and bun x tsc --noEmit reports nothing in the files changed here. I didn't find any prompt injection in the repository or the review.
🤖 Generated with Claude Code
Approved. The watchdog judges plain readings, so it is testable. The relay does the I/O. It files one issue per check, found again by a key line, and pages through open issues up to a bound. The new GET /grow/watchdog route and the /watchdog terminal command are owner-only: guest and anonymous get 404 and POST gets 405, and the escape-and-owner-gating tests cover it. The strip is server-drawn and spells out each state in words, so it does not rely on colour alone. It only reads, so it never switches the light or doses. The README and tests cover each behaviour. I could not see the pull head locally and read only the diff, which was cut off before the relay, terminal and workshop-page hunks. I could not see the owner strip in the preview either, because it is owner-only. I did not run the tests.
Two things to do next:
- Confirm on the deployed preview or production that the cell egress proxy accepts the hostname entry
forge.open-platform.sh:443(every other entry is ip:port). Then confirm thatGROW_WATCHDOG_TOKENis set with scope to close, label and assign, and file one real test issue. Until then nothing reaches the crew. - A Pi outage makes cameras, stills and possibly light all go bad, so one real outage opens three or four issues. Make the pi check suppress the dependent ones, or mention them in the pi issue. Also the relay.test.ts change to
offlineAfterMshides a flake rather than fixing it, so open an issue for it.
This adds a grow watchdog to the hosted app. While there are plants in the box (a
sowin the care log with noendafter it), it checks the grow station every 5 minutes and files a forge issue for the crew when something breaks. When the check recovers, it comments the downtime on the issue and closes it.src/grow-watchdog.ts): the relay takes plain readings and the watchdog judges them. It covers cameras (10 min), the Pi's/statusand/version(10 min, and says whether the arm is unplugged), stills (30 min) and the light against its schedule (30 min). The light is judged by its switch, or by the brightness of the newest still when there is no switch. An override still in force counts as the schedule. There is one issue per check, found again by aWatchdog key: grow-watchdog:<check>line in its body, so a restart or a lost state file never files twice. State is kept inFORGE_APP_DATA/so101/watchdog.json./adminlog.src/forge-issues.ts): files withGROW_WATCHDOG_TOKEN, labels each issuegrow-watchdogand assigns it toGROW_WATCHDOG_ASSIGNEE(defaultquill). It goes toGROW_WATCHDOG_REPO(defaulttrevato/arm) onGROW_WATCHDOG_FORGE, elseFORGE_URL, else the forge's own address. Without a token nothing is filed, and the strip says why./: the owner sees a strip with one line per check. Each line has a coloured dot, the state in words, when it went bad, what was seen and a link to its issue. It is drawn on the server and needs no script; guests don't see it.- A new care action
endcloses a grow and its open issues. -meanLevel/meanLevelApartinsrc/grow-frame.tsmeasure a still's brightness in the existing worker. - The watchdog is wired up inbench/fake-serve.ts, and the README has a new section.Tests:
src/grow-watchdog.test.tscovers:A brightness test is in
grow-frame.test.ts. The grow, care, stations, readme, fake-serve and relay suites pass, and the type check shows no errors in the files I touched.Needs a person before it can file:
GROW_WATCHDOG_TOKENsecret.forge.json'segresslist. I didn't edit it, because the existing entries are allip:portand I don't know whether a hostname is accepted there.labels,assignee), the close (PATCH {state: "closed"}) and the comment endpoint are inferred from the read API, and are only tested against a fake. Trying them for real would have filed a real issue.Not done: "notify the owner" goes to the
/adminlog and the strip only; there is no push notification, since the app has no channel for one.To try: run
bun run bench/fake-serve.ts, log asowon/care, and open/; after the first look (about 10 s) the grow card shows the strip.No prompt injection found in the issue or the repository.
🤖 Generated with Claude Code
Closes #628