Skip to content

Rebuild the /status Testing Environment panel as a live UI - #13311

Merged
RayBB merged 62 commits into
masterfrom
testing-env/status-ui
Aug 19, 2026
Merged

Rebuild the /status Testing Environment panel as a live UI#13311
RayBB merged 62 commits into
masterfrom
testing-env/status-ui

Conversation

@lokesh

@lokesh lokesh commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #13278 β€” review/merge that one first; this diff is only the UI on top of it.

Screenshot 2026-08-11 at 10 58 26 PM

TBH, the polish is overkill for this page. Oh well, it's done. Moving on. :-P

For maintainers using the panel

  • The set and the deploy step are two bands of one card. Staging intent is on top; what the next deploy will actually apply is directly beneath the Deploy button, itemized by PR (Add / Update pin / Enable / Disable / Remove), so you read the plan before you click. Rows the deploy will touch are tinted and say changes on deploy.
  • On/off switch is the leftmost column, beside the checkbox, so a scan down the left edge answers "what's on the box" without reading each row.
  • Drift is one pill, carrying the pinned sha in its tooltip and linking to the GitHub compare for the commits it's behind β€” the pinned-sha column is gone.
  • Bulk select-all plus per-row actions, Refresh from GitHub to pull fresh drift, and last-deploy state with a link to the Jenkins job.

Technical

  • macros/TestingEnvironment.html.jinja renders the whole panel server-side; status.html shrinks to one render_jinja_template() call.
  • Works with JavaScript off, and the JS is pure progressive enhancement. Every control is a native submit button whose formaction names its endpoint; row toggles are <button aria-pressed> switches that post the state they are not in, so one click flips the row either way. Checkboxes and row buttons join two separate anchor forms by id, so a row action never picks up the bulk selection.
  • js/testing-status/ (~240 lines) intercepts those same buttons, reads the same formaction, posts the same fields, and swaps the re-rendered panel in place, restoring focus on the control that was clicked. It never builds a row β€” the macro stays the single source of markup. On any error it falls back to a reload.
  • Styles move out of the inline <style> into static/css/components/testing-env.css, behind a new page-status.css entry point. Adds -object color tokens for the status shapes that the AA-tuned -fg ramps render muddy.

Testing

  1. On /status as a maintainer: add a PR, toggle one off and on, update a drifted one, remove a merged one. Each action re-renders the panel in place, and the deploy plan underneath matches what the rows say.
  2. Reload with JS disabled β€” the table renders and every control still works, including the row toggles.
  3. pytest openlibrary/tests/fastapi/test_testing_status.py Β· npm run test:js Β· pre-commit run --files …

Stakeholders

@RayBB cc: @mekarpeles @cdrini @jimchamp

@lokesh
lokesh force-pushed the testing-env/status-ui branch from c864cc4 to 304560e Compare August 12, 2026 03:49
@github-project-automation github-project-automation Bot moved this to Waiting Review/Merge from Staff in Ray's Project Aug 12, 2026
@RayBB

RayBB commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

@lokesh what do you think about the idea of instead of having checkboxes we just have buttons that automatically do the actions.
So like a toggle for enable/disable and then an update button and delete button. The update button can be a little one next to the hash only if there is an update.

Something kinda like this AI slop I made quick :)
image

@RayBB
RayBB force-pushed the testing-env/status-ui branch from bc1adc8 to b5bef46 Compare August 12, 2026 19:45
@lokesh

lokesh commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

what do you think about the idea of instead of having checkboxes we just have buttons that automatically do the actions.

@RayBB great call! It's updated and ready for review.

image

@RayBB

RayBB commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

@lokesh I think that's pretty great!

One thing I noticed is that we're still not calling to the new api so sometimes it's still quite slow to load the page on testing.
Can we make this so it's a Vue (I think vue is better in this case?) component that actually loads the data in from the api. Before AI was having CORs issue's but that's now fixed because it's served by localhost:8080 and then we can add a small check like if host == testing.openlibrary.org then add "/_fast" to the url for the api.

In this world we also would need page reloads for when you enable/disable PRs and we should probably even have it update every X seconds or something so if someone else changes something you automatically see what's new!

One other feedback is that it would be great if for last deployed at / last deploy started we say X minutes/hours/days ago so that it's a little quicker to see how recent things changed. Drini said in the future we can even add something to the endpoint that says if any deploys are ongoing/queued so we can show that. But for another time!

Thoughts? I know it's a lot of feedback

@lokesh

lokesh commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

@RayBB do you want to take a stab at getting those upgrades in? I have to jump on some other work.

Also, I was attempting to keep the page working with JS disabled and then progressively enhancing, in case there was a JS build failure or other build hiccup, but that probably was unnecessary and we can lean into JS features and Vue.

@RayBB
RayBB force-pushed the testing-env/status-ui branch 2 times, most recently from ff9004e to f65bb57 Compare August 18, 2026 10:00
@RayBB
RayBB marked this pull request as draft August 18, 2026 10:25
@RayBB

RayBB commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Working on the vue stuff. These merge conflicts are quite annoying so I'm going to try to get the backend bits merged tomorrow.

RayBB and others added 13 commits August 18, 2026 10:56
Expose the /status deploy table data (testing environment) via a new
FastAPI endpoint GET /status/testing.json, gated to maintainers/admins.

- Extract get_testing_status() as a shared source of truth used by both
  the legacy /status page and the new API
- Add User.is_maintainer() and require_maintainer/MaintainerDep, mirroring
  the existing librarian auth dependency
- Auto-create _testing-prs.json at startup (if missing) so the page and
  API work from first boot without a manual file
- Add tests for the helper, endpoint, and auth gating
The deploy UI must not appear on the production status page. Gate the
_ensure_testing_state_file() bootstrap behind get_ol_env().LOCAL_DEV so
the file is only auto-created for local development.
The deploy table was a bare HTML table with inline styles, no feedback on
what a deploy would actually do, and no way to see state change without a
manual reload. This rebuilds it as a server-rendered Jinja macro plus a
small controller that keeps the volatile parts live.

- macros/TestingEnvironment.html.jinja renders the whole panel, so markup
  has one source. The controller patches cells; it never builds a row.
- js/testing-status/ polls a fresh copy of the page, diffs the volatile
  cells (drift, sync clock, relative timestamps), and wires row actions
  with optimistic feedback. Strings come from the macro via data-i18n.
- status.py grows a pending-change plan: instead of a has_pending boolean,
  get_testing_status() itemizes what deploying would apply (add / pin /
  enable / disable / remove), so the deploy button can say what it does.
- Deploys now record deploy_started_at when Jenkins accepts a build.
  Jenkins never calls back, so `deploying` is an expiring time window and
  is described as presumed, never as an observed success.
- _trigger_rebuild() takes the state it should build from rather than
  re-reading the file, which previously raced the save it followed.
- Styles move out of the inline <style> block into
  components/testing-env.css behind a new page-status.css entry point.

Claude-Session: https://claude.ai/code/session_01JBYWPEKCuGi9mgisF6RFeR
- data-i18n is single-quoted: tojson escapes `'` but not `"`, so the
  double-quoted attribute closed on the first key and every locale fell
  back to English.
- Client strings pass their `%s` in as a named placeholder value, so
  newstyle gettext's unconditional `%` formatting can't eat it.
- A toggle flipped back to the live state stages nothing: pending_toggle
  reports None when pending_active already matches active, so the plan,
  has_pending, the row note and the state file all drop the no-op.
- Bulk buttons carry formaction again and Deploy has its own form, so a
  click before the controller loads posts to the endpoint instead of
  405ing on /status. ol-button forwards formaction to the inner button,
  the only form-associated element it renders.
- Deploy is disabled only when there is nothing to apply. Jenkins runs
  this job one at a time, so a second deploy queues rather than needing
  the ten-minute window to expire.
- The poll swaps the deploy section on a server-rendered
  data-deploy-key. Its markup can't be diffed: the live copy carries
  hydrated ol-button internals and rewritten timestamps, so the old
  comparison was always unequal and rebuilt the section every 20s.
- A row with both a pin and a toggle staged says so, instead of the pin
  hiding the toggle.

Claude-Session: https://claude.ai/code/session_01JBYWPEKCuGi9mgisF6RFeR
The panel polled /status every 20s behind an Auto-refresh toggle, which is
what forced the controller to patch individual cells: a background repaint
must not tear down an <ol-toggle> the user is aiming at, so the template
declared its volatile cells as [data-cell] and the deploy section carried a
[data-deploy-key] to compare against. Updates now only ever follow an action
the user just took, so the panel is simply re-rendered from the response the
action returns, and that whole contract goes away.

Focus is restored across the swap by re-finding the control that triggered
the update β€” after its first render, since a fresh <ol-toggle> delegates
focus to a button Lit hasn't produced yet.

Gone with the poll: the timer and its visibility handling, the 30s clock that
kept relative timestamps honest, the "synced N min ago" label, the
Auto-refresh toggle, and patchRow()/indexCells(). "Refresh from GitHub"
remains the way to pull fresh drift.
Two bits of the panel that were carrying more weight than they earned.

The relative timestamps needed a clock to stay true, and that clock left
with the poll: "14 min ago" was rendered once and then quietly went stale
under whoever was reading it. The server already writes 2026-07-28 and
2026-08-11 09:12 into the same <time> elements, so the controller stops
rewriting them and no longer needs Intl.RelativeTimeFormat.

The pending note had five phrasings for the same fact β€” that the next
deploy touches this row. Which change is staged is already legible twice
over: the pills sitting next to the note, and the deploy plan below, which
itemizes every change by PR. One string says it, and six msgids retire.
Every control is now a native submit button whose formaction names the
endpoint it posts to, so the panel is fully operable with JS off. Row
toggles become <button aria-pressed> switches that post the state they
are not in, and the checkboxes and row buttons join separate anchor forms
by id so a row action never picks up the bulk selection.

The controller drops to pure progressive enhancement: it intercepts the
same buttons, reads the same formaction, and swaps the re-rendered panel
in place. Replacing ol-toggle/ol-button with plain buttons also removes
the shadow-DOM focus-restore dance.

Also folds the set and the deploy step into two bands of one card, tints
pending rows instead of striping them, and adds -object color tokens for
status shapes that the AA-tuned -fg ramps render muddy.

Claude-Session: https://claude.ai/code/session_01NPfHCzuurUmKXvPWN9ReqC
The /status panel hand-rolls its native <button> submitters, so no
<ol-button formaction> exists anywhere. Left over from an earlier
iteration of the macro.

Claude-Session: https://claude.ai/code/session_01Lg7b9D9KrRspPCCx1ETHGK
Drop the page-status.css entry point and carry the panel's styles in the
site-wide page-user bundle instead (+1.9KB gzipped). A new page-*.css
entry only exists once assets are rebuilt, and patch deploys can't do
that, so static_url()'s bare open() raised FileNotFoundError and 500'd
/status for every visitor β€” on the one server the panel exists for.

Record what each deploy actually built in TestingState.deployed, and
diff it in _pending_changes. Removing a PR deletes its row outright, so
nothing else survived one: the plan came back empty, Deploy rendered
disabled, and the panel claimed "Nothing to deploy" while the box was
still running the PR. Active PRs only, matching what _trigger_rebuild
sends, so removing an already-disabled PR still stages nothing.

Let a failed Jenkins call fail the deploy: _trigger_rebuild now reports
triggered/failed/unconfigured, catches the read-timeout and dropped-
connection errors that escape urlopen unwrapped, and persists nothing
when the build never started, so staged changes stay retryable instead
of being silently discarded. Each outcome gets its own banner.

Default the macro's payload so the structural template test can render
it without data, which it has been failing to do since the macro landed.

Claude-Session: https://claude.ai/code/session_01NPfHCzuurUmKXvPWN9ReqC
A removal reaches the plan two ways β€” a PR merged to master, or one a
maintainer took out of the set β€” and both are kind "remove". The plan
printed "merged to master" for either, so removing an open PR asserted
it had landed. Carry the cause in `reason` and say it only when true.

The deploy banners are rendered outside [data-testing-env], which the
controller swaps wholesale, so with JS on none of them ever reached the
DOM: a failed Jenkins call came back ok, re-rendered an unchanged panel,
cleared the toast, and read as a no-op. Give them a container that is
always present in both documents and swap it alongside the panel.

Claude-Session: https://claude.ai/code/session_01NPfHCzuurUmKXvPWN9ReqC
RayBB and others added 8 commits August 18, 2026 14:40
A silent 60s poll plus a visibilitychange refresh keep drift and deploy
state fresh without a spinner β€” the explicit Refresh button is the only
thing that spins, and in-flight actions are never raced.

Generated with Codebuff πŸ€–
Co-Authored-By: Codebuff <noreply@codebuff.com>
Production records a failed merge as "Merge conflict for PR #N (pinned
<sha>) β€” skipping" (scripts/make-integration-branch.sh), not git's
"Automatic merge failed…" β€” so neither the panel dot nor the legacy table
ever turned red on a real conflict. Match both formats, and fall back to
the PR number named in the summary when the pull_line carries none.

Generated with Codebuff πŸ€–
Co-Authored-By: Codebuff <noreply@codebuff.com>
loadStatus skips identical JSON to avoid a repaint flash, so the "X ago"
deploy labels froze even though fetches kept running β€” a deploy's start
time never advanced. timeAgo now takes an injectable now, bumped by the
same poll and tab-focus that refresh the data, so only the deploy-status
section re-renders and the labels stay honest.

Generated with Codebuff πŸ€–
Co-Authored-By: Codebuff <noreply@codebuff.com>
Jenkins can lag before surfacing the accepted run as IN_PROGRESS, leaving
the chip on "Last deploy" right after clicking Deploy. A local optimistic
flag now shows "Deploying, started just now" the moment the trigger is
confirmed, and the next server update carrying a real Jenkins verdict
replaces it.

Generated with Codebuff πŸ€–
Co-Authored-By: Codebuff <noreply@codebuff.com>
The wfapi/runs fetch now asks for fullStages so a running deploy names the
stage it is on ("Deploying, started 3 minutes ago β€” Build oldev"), and the
memcache TTL drops from 30s to 1s so a poll or tab-focus reads near-live
run state.

Generated with Codebuff πŸ€–
Co-Authored-By: Codebuff <noreply@codebuff.com>
With the near-live Jenkins data (1s cache) and a 5s panel poll, the
server-driven chip arrives within seconds of clicking Deploy, so the local
"just triggered" guess was redundant and is removed. The deploying chip is
now entirely server truth.

Generated with Codebuff πŸ€–
Co-Authored-By: Codebuff <noreply@codebuff.com>
- Move the 682-line shadow-DOM style block out of TestingEnvironment.vue
  into TestingEnvironment/styles.css via <style src> (build-time inlining
  keeps the encapsulation; the SFC drops from 1,045 to 366 lines)
- Replace the TestingPR/TestingState dataclasses, their hand-rolled
  to_dict/from_dict, and the mirrored FastAPI response models with one
  pydantic model set that serves the state file, the compose path, and
  the API alike; legacy state-file formats still load
- Extract the Jenkins client (jenkins_deploy_status, trigger_rebuild)
  into openlibrary/plugins/openlibrary/jenkins.py and drop its 1-second
  memcache β€” the panel polls every 5s and wfapi is cheap

Generated with Codebuff πŸ€–
Co-Authored-By: Codebuff <noreply@codebuff.com>
The bordered header buttons and the icon-only row actions were 12
nearly-identical declarations apart; they now share one base rule. The
.status--success modifier re-declared the base chip's colors, so it is
gone (template included). The two prefers-reduced-motion blocks merge
into one, and the :link/:visited selector re-listings on the four link
styles drop β€” author rules outrank the UA's by cascade origin, so plain
class selectors cover both states.

Behavior-identical; stylelint, JS tests, FastAPI tests, and the
components build all pass.

Generated with Codebuff πŸ€–
Co-Authored-By: Codebuff <noreply@codebuff.com>
@RayBB
RayBB marked this pull request as ready for review August 19, 2026 19:04
RayBB and others added 3 commits August 19, 2026 12:07
The status panel was the only consumer of the seven status_* handlers, yet
they answered 303 redirects whose query-string markers the panel had to
parse out of response.url β€” and it only ever checked add_failed, so a
Jenkins-refused deploy (or a missing token in local dev) failed silently
while the panel kept showing the previous run's stale "Deploy succeeded".

Have each handler return {"ok": true} or {"ok": false, "error": "<code>"}
directly; runAction maps the code to a translated toast, and addPrs keeps
the input on add_failed. Auth and input errors stay real HTTP errors. The
unconfigured path still advances state so local dev can exercise the panel,
but now says nothing was actually deployed.

πŸ€– Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
get_dev_merged_status was functools.cache'd and only cleared when testing
state was saved, but the deploy pipeline rewrites _dev-merged_status.txt
after each build finishes β€” minutes after status_deploy last saved. A fresh
merge conflict therefore stayed invisible until the next state mutation.

The file is small and read at most once per panel poll or /status page load,
so reading it fresh on every call is cheaper than any invalidation scheme
and can never go stale. Remove the decorator, its now-unused import, and
the cache_clear in _save_testing_state.

πŸ€– Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
@RayBB
RayBB force-pushed the testing-env/status-ui branch from db1531a to f449594 Compare August 19, 2026 19:32
RayBB and others added 13 commits August 19, 2026 13:03
SMIL animation doesn't tick inside a favicon in Chrome, so the previous
inline SVG rendered as a static ring. Draw the real favicon with a
rotating 270Β° arc on a canvas and swap PNG frames into the rel="icon"
links instead β€” the portable pattern (favico.js, CSS-Tricks). Only
openlibrary favicons are touched; hrefs are restored when the deploy
ends or the panel unmounts.

πŸ€– Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
The "Deploying, started X" label paired the Jenkins verdict (deploying:
true from the live wfapi run) with the state file's deploy_started_at,
which local dev never updates β€” a stale copy made the panel say a fresh
deploy started days ago. The Jenkins override already carried start_time
from startTimeMillis; wire it into deploy_started_at so the label uses
the real run whenever Jenkins is reachable.

πŸ€– Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Replace the animated canvas-frame spinner with a single drawn frame: the
real favicon plus a bright-orange wedge covering its top-right half,
swapped in once when the deploy starts and out when it ends. No animation
loop means nothing to lag on a busy main thread and no per-frame favicon
repaint for the browser to throttle, and the mark is unmistakable at
tab-strip size. A 20-minute failsafe still removes the badge on return
from a frozen tab.

πŸ€– Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
The conflict dot used --color-error-object (brick red-500), which reads
muddy at 8px. Add --red-400, a vivid fire-engine step to the red ramp,
and point the semantic object token at it β€” the palette stays brick
everywhere else, with 400 documented as the deliberate exception for tiny
attention dots.

πŸ€– Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Use pure fire-engine red (hsl(0, 100%, 50%)) for the conflict dot,
replacing the warm brick red (--color-error-object / red-400) so the
8px indicator shouts louder.

πŸ€– Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Consolidate the poll into a single 1s interval that advances the wall-clock
'now' used by the relative deploy labels every tick, and refreshes the
/status/testing.json data only on a tick at a :05 wall-clock boundary β€”
so the seconds-since-deploy label updates smoothly while the network request
stays on its 5s cadence.
Rename the jenkins_url prop to jenkinsUrl in the TestingEnvironment and
DeploySection components (conflicting jenkinsUrl computed renamed
safeJenkinsUrl), and pass it kebab-cased (jenkins-url) in the Vue template
and the Jinja render_component attrs so defineCustomElement still hydrates
it. Clears the vue/prop-name-casing ESLint warning.
toastTimer's ID is never rendered or depended on by the UI, so move it out
of data() into a non-reactive _toastTimer property alongside _timer and
_deployBadge (cleared on unmount).
The Jenkins link reads a compile-time server constant and the avatar URLs
come from GitHub API responses, so the https-only sanitization shipped in
utils.js was guarding nothing an attacker can influence. Drop it from all
three components, utils.js, and its unit test.
Migrate all four Vue components (PersonCell, DeploySection, TestingRow,
TestingEnvironment) from the Options API to the Composition API with
<script setup>. This reduces boilerplate, colocates related logic, and
aligns with Vue 3's recommended approach for new code.

No functional changes β€” all 15 existing tests pass and the Vite build
succeeds cleanly.

πŸ€– Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Use shallowRef for primitive values, add immediate: true to the
deploy-favicon watcher, and reorder SFC sections to
script β†’ template β†’ style per Vue best practices.

πŸ€– Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Split orchestration logic into three focused composables:
useToast, useTestingStatus, and useActions, leaving the
container as a thin composition surface.

πŸ€– Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
`strings` is set once at setup time from server-rendered i18n and never
mutates, so the shallowRef proxy was unnecessary overhead. Also replace
three `<template v-if/v-else>` wrappers with direct `<div>` elements
since each branch renders a single child.

πŸ€– Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

@RayBB RayBB left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lokesh and I have been testing this very thoroughly and discussing what works and what doesn't. Right now we feel it's in a really good spot in terms of functionality and code quality. We're also hitting some merge conflicts with other branches so we want to get this out there.

There's still room for improvement for a few things but it should be later:

  • Convert endpoints to fastapi
  • Show which other PR is causing your merge conflict (will require updating the script)
  • Adding the ability to search for PRs
  • Recently deleted section.

Anyway, look forward to hearing feedback and really hope this is gonna make the whole experience much nicer for folks to deploy!

@RayBB
RayBB merged commit 30b1371 into master Aug 19, 2026
9 checks passed
@github-project-automation github-project-automation Bot moved this from Waiting Review/Merge from Staff to Done in Ray's Project Aug 19, 2026
@RayBB
RayBB deleted the testing-env/status-ui branch August 20, 2026 01:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants