feat: edit token expiry from the admin dashboard#11
Merged
Conversation
Each row's Expires cell is now click-to-edit (or focus+Enter): a datetime-local editor prefilled from the stored UTC value saves on change, a blank value clears expiry, and a Today pick snaps to 23:59 local. PUT /admin/api/tokens/:hash patches status and/or expiresAt through the shared parseExpiry validation; setTokenStatus becomes patchToken. Row wiring is delegated at the body (one copy instead of per-row handlers), same-row requests serialize via hx-sync, the 120s poll pauses while an editor is focused, and the PUT response row keeps the separately stored lastUsed.
The body-level config-request delegate already snaps a Today pick to 23:59 local before the UTC conversion, so the create form's input-time dance (blur/showPicker Chromium workaround) duplicated the same logic. Both the form and the row editors now share one converter.
- Snap rule is now deterministic: only a today-and-already-past value (what a Chromium Today pick commits) snaps to 23:59; any valid future choice is untouched regardless of submit delay. - Same-row actions show a busy state while a request is in flight (hx-indicator on the row + pointer-events:none) so a second click is visibly blocked instead of silently swallowed; hx-sync stays as a queue backstop for non-swapping error responses. This also closes the one-browser stale read-modify-write path; cross-client writes remain last-write-wins on KV, documented. - The cosmetic lastUsed read is parallel and best-effort so a committed patch can no longer surface as a 500. - The expiry trigger is a native button (focusable, named by content) and the editor input carries aria-label; the custom keydown delegate is gone. - token-expiry-check-at-validate.md no longer claims expiry never mutates; .dev.vars* covers environment variants.
… polls - Expiry saves exactly as picked: the today+past-to-23:59 snap could extend authorization when the admin deliberately entered a past time, so no client rewrite survives; a past pick renders as an expired row immediately. - A per-hash TokenWriter Durable Object serializes patch/delete, since a bare KV read-modify-write let a concurrent or stale-read expiry patch resurrect a disabled token; every merge now follows its own prior write. Regression test races disable against expiry PUTs. - Poll vs mutation contention is asymmetric now: the poll drops itself when any #tokens-scoped request is in flight, and row mutations abort a stale in-flight poll (hx-sync replace, inherited from tbody), so a late poll response can never overwrite a newer row swap and clicks are never eaten. Verified with route-level response delays.
- TokenWriter now merges against its own strongly consistent storage; KV (which does not guarantee read-your-write) only bootstraps a hash the writer has never touched, guarded by a deletion tombstone so a stale KV echo can neither revert a disable nor resurrect a delete. patchToken/deleteToken are absorbed into the writer; stale-echo and tombstone regression tests simulate the exact failure. - Opening an expiry editor aborts any in-flight poll (opening issues no request, so the sync scopes could not see it and the late response would destroy the focused editor). Verified with a 1.2s route delay. - README cost note covers the TokenWriter DO request per admin write.
- remove() deletes from KV before tombstoning: a thrown KV delete used to leave a tombstoned-but-live token that 404'd every disable attempt. - patch() rolls its merge base back when the KV write throws, so an errored edit cannot silently replay weeks later via an unrelated patch. - Creation goes through the writer (mintToken splits pure minting from KV persistence): the merge base is seeded at birth, the first edit of a fresh token no longer depends on a KV read, and the tombstone becomes a plain flag cleared on recreation - the cross-clock createdAt comparison is gone. Recreate-then-edit regression test added. - Row request wiring (target/swap/indicator) hoisted to the tr; with the tbody replace scope serializing at the writer, busy rows now dim but stay clickable, so a commit-and-toggle gesture lands both writes. - Create form joins the #tokens sync scope so a stale poll cannot overwrite the out-of-band row insert; two subsumed test pins dropped; lifecycle doc names the writer methods.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Admins can now edit or clear the expiry of existing tokens directly from the table.
UX
config-requestdelegate converts the picked local value to UTC ISO on submit (shared with the create form); the server validates and stores UTC; thetime[datetime]localizer renders it back. Values save exactly as picked - a past pick renders as a red "expired" row immediately.API / server
PUT /admin/api/tokens/:hashpatchesstatusand/orexpiresAtthrough the sameparseExpiryvalidation as creation (offset-less 400, empty patch 400); blank clears. Zero KV data migration.TokenWriterDurable Object (new binding + migrationv2) whose own storage is the merge base: KV guarantees neither atomic read-modify-write nor read-your-write, and merging from a KV read let a stale expiry patch resurrect a disabled token. Creation seeds the merge base (a fresh token's first edit never depends on a KV read); only pre-writer hashes bootstrap from KV, gated by a deletion tombstone that recreation clears. Failure ordering is safe: KV deletes precede the tombstone, and a failed KV write rolls the merge base back instead of silently replaying later.lastUsed(parallel, best-effort read).Concurrency & UI honesty
#tokensrequest is in flight and pauses while an editor is focused; row mutations and the create form abort a stale in-flight poll (hx-syncreplace); opening an editor aborts an in-flight poll so its late response cannot destroy the focused editor.hx-target/hx-swap/hx-indicator) is declared once on thetr; editor behavior is delegated once at the body; keyboard handling is native.Verification
wrangler deploy --dry-runvalidates the DO binding and migration.wrangler devwith route-level response delays: edit/clear/cancel roundtrips with IST conversion, past-pick renders expired, keyboard open, both poll-contention directions, editor-open aborting an in-flight poll with the typed value surviving, and the commit-and-toggle gesture applying both writes.changeon day-click (mid-edit save; benign, row re-renders), cross-row abort residual above, dup-check 409 creation race (pre-existing, documented).