Rebuild the /status Testing Environment panel as a live UI - #13311
Conversation
c864cc4 to
304560e
Compare
|
@lokesh what do you think about the idea of instead of having checkboxes we just have buttons that automatically do the actions. |
bc1adc8 to
b5bef46
Compare
@RayBB great call! It's updated and ready for review.
|
|
@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. 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 |
|
@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. |
ff9004e to
f65bb57
Compare
|
Working on the vue stuff. These merge conflicts are quite annoying so I'm going to try to get the backend bits merged tomorrow. |
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
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>
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>
db1531a to
f449594
Compare
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
left a comment
There was a problem hiding this comment.
@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!


TBH, the polish is overkill for this page. Oh well, it's done. Moving on. :-P
For maintainers using the panel
Add/Update pin/Enable/Disable/Remove), so you read the plan before you click. Rows the deploy will touch are tinted and saychanges on deploy.Refresh from GitHubto pull fresh drift, and last-deploy state with a link to the Jenkins job.Technical
macros/TestingEnvironment.html.jinjarenders the whole panel server-side;status.htmlshrinks to onerender_jinja_template()call.formactionnames 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 byid, so a row action never picks up the bulk selection.js/testing-status/(~240 lines) intercepts those same buttons, reads the sameformaction, 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.<style>intostatic/css/components/testing-env.css, behind a newpage-status.cssentry point. Adds-objectcolor tokens for the status shapes that the AA-tuned-fgramps render muddy.Testing
/statusas 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.pytest openlibrary/tests/fastapi/test_testing_status.pyΒ·npm run test:jsΒ·pre-commit run --files β¦Stakeholders
@RayBB cc: @mekarpeles @cdrini @jimchamp