Enhancement: Court Planner + Recommendations workflow (Planner, Court Report, Herald Script, Recs Manager, notifications, pass-to-local) - #490
Conversation
|
Updated with the Court Planner grant-safety + workflow/UX implementation (commit Records integrity
Concurrency
Workflow / mobile / a11y
Ceremony polish
Verified end-to-end against a live instance (6/6 functional + 4/4 fix re-tests; all data-integrity SQL invariants at 0). Migrations: 🤖 Generated with Claude Code |
- New Court class, Court controller, and CourtAjax controller - Court detail and list templates - DB migration for court planner table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…mendations - Court Planner tab added to Kingdomnew profile (visible to canManage users only): loads CourtList/UpcomingEvents from controller, inline card list with status badges and award counts, Plan a Court modal using kn-* CSS classes - Anonymous toggle on all recommendation modals (Playernew, Kingdomnew, Parknew): checkbox stores mask_giver=1 in ork_recommendations; submitter fields are nulled in PlayerAwardRecommendations for non-ORK-admin callers; ORK admins see name with (anon) label; CallerUid + CallerIsOrkAdmin wired through all three controllers and the Park/Kingdom/Player AJAX handlers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Court Planner tab on Kingdom and Park profiles (Monarch/Regent/PM access) - Court detail page: create/manage courts, add awards, reorder, track scroll/regalia status - Award recommendations: anonymous rec support, snooze functionality, rec enhancements - Print court script: two-column table layout with rec reason and recommender attribution - Fix: Court detail template path (HTTP_TEMPLATE → relative path) - Fix: Print script overlay moved to body level so print CSS selector works correctly - Fix: Authorization switch fall-through missing break for AUTH_EDIT case - Fix: Password reset email copy and expiry duration - DB migrations: snooze-to-recommendations, court-rec-enhancements Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Award row layout (Court_detail.tpl): - Replaced rigid 6-column CSS grid with 2-line flexbox layout - Line 1: bold persona + park abbreviation + note icon - Line 2: award name + rank + compact icon-only flags + tracking icons - Removed type badge (left border color already communicates title/ladder/award) - Pass-to-Local and From-Rec flags reduced to 20px circle icons with tooltips - Updated both PHP server-rendered markup and JS cpAppendAwardRow() to match Plan a New Court modal (Kingdomnew_index.tpl, Parknew_index.tpl, revised.css): - Fixed modal rendering outside overlay (missing position:fixed CSS) - Aligned field classes to match existing modal pattern (kn-acct-field, kn-modal-close-btn) - Added scoped CSS for both kingdom and park new-court modals revised.js syntax fix: - Repaired two unclosed IIFE/addEventListener blocks at end of file that prevented the court planner tab from responding to clicks DB migration (local only): - Created ork_court, ork_court_award, ork_court_award_artisan tables Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reverse-engineered PRD covering: ork_court / ork_court_award / ork_court_award_artisan tables, recommendation snooze (regnum-scoped auto-expiry), award context preservation (court_award_id + source_reason on ork_awards), anonymous recs (mask_giver wired end-to-end), planning + run-of-show + post-court workflows. Flags the parallel ork_recommendation_support vs Mask branch's ork_recommendation_seconds collision that needs resolution before either can merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Spreadsheet layout for the Order of Court list — sticky column header, CSS-grid rows, density toggle (Cozy / Comfortable / Compact) persisted to localStorage, sidebar collapse rail, row #'s kept in sync on reorder/add/remove. - Comprehensive dark-mode coverage on both Court_detail and Court_list: status bar, sidebar cards, modals, type chips, tracking icons, status badges, recommendation modal, expand area, autocomplete dropdowns. Inline-styled pills/buttons refactored to classes so theme overrides actually apply. - "Add from Recommendations" modal now delegates to Reports::PlayerAwardRecommendations, gaining Master-peerage cascade, custom-award carve-out, award_id cross-check, snooze awareness, age, seconds, and anon-rec masking. Modal gets 4 view filters (Open / All / Snoozed / Already Has), inline age badges, seconds count, on-another-court warning, and per-row "Already Has / Covered by Master" context. Each row collapsed from ~5 lines to 2 (header + reason). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Standalone spreadsheet-style tool (Recommendations/manage/{kingdom|park}/{id})
consolidating admin rec actions (Grant Now, Dismiss, Snooze, Add to Court) off
the inline profile tab. Reuses existing AJAX endpoints + Court Planner.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 bite-sized tasks: route scaffold, getRecommendationCourtMap, controller data, spreadsheet grid, expand rows, sort/filter, selection+bulk, snooze/ dismiss, Grant Now, Add to Court, inline-tab migration, dark-mode/QA pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Kingdom/Park profile recs tabs: add authority-gated "Manage Recommendations" button; remove inline grant/snooze/dismiss/add-to-court controls + the now- orphan add-court overlays (kept the Enter-Awards award modals; community +1/second controls retained). revised.js handlers left inert (delegated/null- guarded). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…$DB in controller)
…de cluster paging)
…e, not filtered count)
Adds the rows() action + rmParkMap() helper to Controller_Recommendations and the recommended_awards_page() model passthrough. The endpoint renders one 500-row batch of shared _rm_row.tpl partials as JSON. Also fixes a MariaDB correlation bug in Report::PlayerAwardRecommendationsPage's support subquery (outer alias not visible through a UNION-in-FROM derived table) that made every page return zero rows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ pill eligibility
… the count SQL The page/count query approximated AlreadyHas with kacount/awcount only, so the 'Showing N of M' total read a few high (and ator a few low) for ladder recs covered by a Master peerage. Inject the GetLadderMasterMap ladder->master pairs as a derived table + correlated EXISTS so the SQL AlreadyHas matches the PHP hydration exactly. Verified park 76 open now rows==total (272==272). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the plain one-word data-tip on the snooze button with the rich .rm-passlocal-tip pattern so it can render a bold title plus description: "Snooze to Next Monarchy" / "Temporarily dismiss this recommendation until either the Monarch or Regent officer at this level changes." Snoozed state shows an Unsnooze counterpart. Right-anchored + dark-mode variant; JS swap targets the icon span so it no longer clobbers the tooltip markup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt Award modal; + Court/Recs polish Recommendations Manager - CSV export endpoint streaming the full filtered/sorted set (Recommendations/export) - Server-side Rank sort + sortable Rank column header - Split Park into its own column (abbreviation); Rank into its own column (pill without the "Rank" word) - Grant Award modal (replaces insta-grant — never grants without confirmation): pre-filled recipient/award/rank/note, JSON PlayerAjax grantaward endpoint, court-plan reconciliation folded in, resolves the cluster on success - Grant modal parity with the regular award modal: rank pills (green = already held), officer quick-pick chips (Monarch/Regent), "Given at" park/kingdom/event location picker Court Planner + Recs polish - Security: add_award scopes KingdomAwardId to the court's kingdom (IDOR) - grant_award atomic claim (no double-grant); reorder_awards single CASE UPDATE (no N+1) - All CourtAjax mutations moved into the class.Court lib layer (no raw $DB in the controller) - controller.Court lookups + hero heraldry routed through the lib (restores cache-buster) - Court_detail: error handling on post()/cpSaveOrder, native confirm()/alert() → in-product dialogs, batch add-from-rec no longer marks failed adds as succeeded - class.Report: expose HeldRank; class.Court do-while → while (read-after-Next fix) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Design spec for splitting grant (stage) from commit-to-record (finalize), adding a stored run-vs-plan court mode, a two-tap pre-filled grant modal, per-row undo, drag-and-drop reorder, a live multi-manager heartbeat, a prepopulate-skipped-from-last-court banner, and a complete-court modal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GDxMDhZUEMepFQ9hMidnpJ
Six-expert review roadmap: S1 idempotent grant sink + QW1-9 + S2 mode-driven workflow + S3 responsive mobile + S5 optimistic concurrency/presence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXQ2YePHm59VSzozsxbNXJ
…kflow, mobile, collaboration & ceremony polish Implements the stage/finalize + grant-safety specs on top of the Court Planner. Grant safety / records integrity: - Single idempotent commit sink (Court::commitStagedAward); court-line identity is the idempotency key. Player::AddAward returns the real inserted id (findRecentAwardId date-heuristic removed); throw-safe finalize (revert-on-throw, re-runnable). - Server-side cross-path reconcile (reconcileGrantForRecommendation, exact rec id OR cluster key) so a Recs-Manager grant can't be re-granted at finalize. - update_court_status rejects 'complete' (only finalize completes a court); remove/skip/setStatus guard 'given'; update_award no longer writes lifecycle status. Concurrency / collaboration: - ork_court_award.row_version optimistic-lock token (409/stale -> non-destructive reload); full-field heartbeat reconcile (add/remove rows); presence roster + honest sync indicator; silent background poll. Workflow / UX / mobile / accessibility: - Run mode: 'Grant' vocabulary + auto-finalize on Complete; walk-on adds while published; inline un-skip. Mobile: overflow-x wrapper + stacked-card layout so Grant/Skip are reachable on a phone. a11y additive pass (aria-live, labels, contrast, tracker glyphs, dialog roles); native confirm/alert removed. - Rank-pill contrast ramp reworked to WCAG AA (uniform saturated + white text). - Functional status (inert manual dropdown removed; Cancelled -> Skipped); expanded-row highlight; hero breadcrumb tidy; flags/type instant tooltips. - Ad-hoc add split into Add Award / Add Title with typeable autocompletes grouped exactly like the player Add Award modal (new Model_Award::fetch_award_option_groups), rank pills replacing the number box. Migrations: 2026-07-11-court-stage-finalize.sql, 2026-07-12-court-grant-safety.sql. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DXQ2YePHm59VSzozsxbNXJ
748e3df to
d32dc7c
Compare
Full mobile-review pass over the Court Planner, measured live at 320/390/768px in light and dark mode. The module previously had only two media queries (640px and 800px, both off the house cluster) and no real phone layout; because <html> is overflow-x:hidden and no ancestor in the court-detail chain is a scroller, overflowing content was destroyed rather than merely off-screen. Layout / overflow - Re-anchor breakpoints to the house cluster: 800px -> 768px, 640px -> 600px. The module now has exactly two, 600 and 768. - .cp-section-header wraps at <=768px; .cp-main-content no longer sizes to the button row's max-content, which was setting a 419px floor and clipping the scroll/regalia/chevron cells off every award row. - Hero stacks at <=600px. The court name rendered at literally 0px wide (nowrap + a flex-shrink:0 actions column); it now wraps and the run-mode workflow buttons no longer sit 78px past the right edge. - .cp-expand-grid collapses to one column, so Pass to Local and Regalia Maker are reachable instead of laying out 120px off-screen. - Kingdom/Park profile court cards wrap; the "Open" link was up to 237px off-screen and is now a full-width 44px row action. Court_list.tpl gains its first media queries. Overlays - Recommendation rows stack their meta instead of destroying award/rank/date/ age-badge under overflow:hidden (up to 196px was unreachable, with no horizontal scroll to recover it). - Court Script header wraps, so Close no longer overlaps the Citation toggle by 37px mid-ceremony; script entries stack at a readable scale. - Modals get 16px gutters, dvh sizing, and a scroll lock on documentElement (body-level locking is a no-op here, since <html> is the scroll container). - Autocomplete repositions/flips on scroll and resize, and is cleared on close so a stale result list can no longer bind a MundaneId the user never searched. Touch / a11y / dark mode - Reorder arrows were 30x15 because .cp-density-* out-specified both the mobile and pointer:coarse rules; driven through the density custom properties they now compute 36px in every density. Tracking icons reach 44px and carry a visible label, so their state no longer depends on hover-only data-tip. - pointer:coarse extended to the controls that carry the workflow (fields, modal footers, rec-modal selects, giver pills, grant confirm) at a real 44px floor; inline font-size overrides that defeated the 16px iOS anti-zoom rule removed. Touch sizing is scoped to <=600px so it cannot overlap the fixed grid tracks on tablets. - Status badges moved from PHP inline styles to modifier classes so html[data-theme="dark"] can reach them; light mode is byte-identical. - Run mode gains a one-line sticky progress/presence bar, mirrored from the existing heartbeat updaters. Bugs found along the way - Court/list was dead at every viewport: index.php collapses route segments past the 3rd into a single string, so Controller_Court::list() received 'kingdom/17' as $context with $id null and always rendered "Invalid location." Fixed in the controller (index.php's branch is global and has no other Court/list caller). - Court_list.tpl read $courtList/$upcomingEvents but the controller supplies $CourtList/$UpcomingEvents, so the page rendered an empty state over real data and silently dropped the "Link to Event" select from the new-court modal. - Plan-a-New-Court date field now uses the house Flatpickr altInput pattern with disableMobile:true, so the human-readable date shows on phones too; its Escape handler moved to the capture phase, where the guard actually runs before flatpickr closes the calendar. Desktop verified unchanged at 1823px. Every fix is scoped to a media query, pointer:coarse, or a mobile-only class. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhD3rP2wzTgWPe4okEvYsu
# Conflicts: # orkui/controller/controller.KingdomAjax.php # orkui/model/model.Award.php # orkui/model/model.Player.php
Branch brought current with
|
…main layer The Court Planner work left five raw $DB->DataSet() sites in the controller layer. Each is moved to a domain class in system/lib/ork3/ with identical SQL and return semantics; no behavior change. - CourtAjax::lookupPersona -> reuses the existing Player::GetPersona(). - CourtAjax::update_award_tracking_status -> reuses the existing Court::getCourtAwardCourtId() (already used by skip_award for the same lookup). - ParkAjax 'checkrecommendation' -> new Player::GetPeerAwardRecommendations(), with a thin Model_Player::get_peer_award_recommendations() pass-through. - Reports::courts park/kingdom name lookup -> new Court::getCourtReportScope(), called the same way as the adjacent getCourtReportList()/getCourtReportDetail(). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Controllers reached into Ork3::$Lib->court directly in 69 places across
seven controllers, bypassing the model layer every other domain uses.
Adds orkui/model/model.Court.php: a thin pass-through fronting the Court
domain class, following the model.QualTest.php / model.KingdomProfile.php
pattern (snake_case wrappers over the camelCase lib API, delegating via a
private _court() accessor). No SQL and no business logic live in the model
-- every method forwards its arguments unchanged, defaults included.
All 69 call sites in controller.Court, controller.CourtAjax,
controller.Kingdom, controller.Park, controller.PlayerAjax,
controller.Recommendations and controller.Reports now go through
$this->Court, with load_model('Court') wired into the constructor for the
Court-centric controllers and into the specific actions elsewhere, matching
each controller's local convention.
Name resolution: Model_Court extends Model, so the base constructor derives
'Court' from the class name and startup.php has already require_once'd
DIR_ORK3 . class.Court.php, making the domain class available.
Pure call-convention change -- no behavior difference.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up: snooze guards verified, Court model layer addedTwo items from the sync comment above, resolved. 1. Snooze handlers are authorized — no guard added, and none neededI asked for evidence before adding anything, and the evidence says the lib enforces it.
if (($mundane_id = Ork3::$Lib->authorization->IsAuthorized($request['Token'])) == 0) {
return NoAuthorization();
}
...
// Auth: must be park admin for recipient's park
$recipientInfo = $this->player_info($awardRec->mundane_id);
if (!Ork3::$Lib->authorization->HasAuthority($mundane_id, AUTH_PARK, $recipientInfo['ParkId'], AUTH_EDIT)) {
return NoAuthorization();
}
The important detail: the scope is derived from the loaded recommendation's recipient ( Same applies to the identical unguarded pair at 2. Court model layer (
|
…bile) into court-planner Conflict resolutions: - controller.KingdomAjax.php: keep master's model-layer refactor (KingdomProfile / AdminDashboard models, Authorization->has_authority) while preserving the branch's recommendation endpoints (passtolocal, resolvecluster, snooze/unsnooze, geteventtemplates) and the Anonymous/Granted rec fields. Restored the branch-only memcached flush after setconfig that the recorded resolution had dropped. - model.Award.php: keep master's delegation of fetch_award_option_list() to Award::GetAwardOptionListHtml(), and re-point the branch's Court Planner fetch_award_option_groups() at Award::GetAwardOptionGroups() so award classification lives only in the lib layer instead of being duplicated (the copy's stated source, the old inline classifier, no longer exists). - model.Player.php: master's PSR-12 rewrite plus the branch's four recommendation passthroughs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…igrations Two issues surfaced by bringing the branch current with master. 1. Password login was dead on any environment that had already applied 2026-07-13-add-ork-session-table.sql. Master added user_agent/ip to ork_session by editing that migration in place, but the file leads with CREATE TABLE IF NOT EXISTS, so re-running it on a database built from the pre-metadata version adds nothing. CreateSession() then INSERTs both columns into a table that has neither; the insert fails, the verifying read-back finds no row, CreateSession returns '' and every login reports "Could not establish a session." Added an idempotent repair migration. Fresh installs were never affected — ork.sql and the current migration both declare the columns. 2. bin/run-ork-db-checks.sh failed on this branch: all nine Court Planner / recommendation migrations were unclassified in the ork-db manifest. All are pure DDL, so they are class S / render full. Migration coverage now passes (93 files classified). The remaining drift-check failures (missing extracted/*.sql catalog artifacts) reproduce on master and are not branch-introduced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…leak, id race)
Five findings from a hostile review of the Court Planner and Recommendations
Manager, each verified against the running app before and after the fix.
1. CRITICAL — Granting from the Recs Manager left the recommendation unresolved
and made a duplicate award one click away. grantaward now reconciles court
lines server-side (S1), but the client still ran its own post-grant courtStep
against those same lines. Both branches hit the given-row guards this branch
added — remove_award refuses a given row, set_award_status carries
AND status != 'given' — so the promise chain threw BEFORE
resolverecommendationcluster ever ran. The award landed in ork_awards, the rec
was never soft-deleted, and the row stayed live with a working Grant button
while the toast told the officer to retry. Player::AddAward has no duplicate
guard, so a retry wrote a second permanent award — precisely what the
grant-safety spec exists to prevent, on the routine triage path.
Deleted courtStep. The officer's leave/remove choice now travels as a
CourtAction parameter and reconcileGrantForRecommendation applies it as the
line's terminal status: 'leave' -> given, 'remove' -> cancelled (soft-cancel,
so the planned line's audit trace survives; the old client path hard-DELETEd).
Cluster resolve is no longer gated behind court cleanup, and the
granted-but-not-cleared path now retires the row instead of inviting a retry.
2. HIGH — Finalize published the internal officer note as the player's permanent
public citation. commitStagedAward fell back PublicComment -> RecReason ->
Notes; that third branch is not in the spec. Notes is the internal column
("hold until the drama settles"), and ork_awards.note renders on the
recipient's profile to every visitor with no post-finalize undo. Two one-click
paths reached it: an ad-hoc walk-on award with notes filled, and Plan-mode
"Record grants", which stages every planned row without touching
public_comment. Dropped the Notes branch.
3. HIGH — addAward()/addArtisan() named the row they had just inserted with
"highest id for this court" instead of LAST_INSERT_ID(). That query is not
connection-scoped, so under the concurrent editing this tool was built for one
officer's response carried another officer's row id, and every follow-up
action keyed off it mutated the wrong award. Both now use LAST_INSERT_ID(), as
createCourt() already did.
4. MEDIUM — CSV export was open to spreadsheet formula injection. Reasons and
personas are written by ordinary players; fputcsv() handles quoting, not
formula triggers, so a reason starting with = + - @ executed on open in the
officer's spreadsheet. Added csvSafe() over every free-text cell.
5. MEDIUM — CourtAjax ran two raw $DB queries (lookupPersona, and the court_id
lookup in update_award_tracking_status). Moved to the lib layer as
Court::getPersona() and the existing Court::getCourtAwardCourtId().
Verified live: leave/remove both reconcile correctly and write exactly one
ork_awards row; the cluster resolve that was previously unreachable now soft-
deletes the rec; add_award's returned id matches the inserted row; an
internal-notes-only award finalizes with an empty public citation; a
=HYPERLINK() reason exports neutralized. Test data was reverted from the shared
dev DB.
The stage->finalize core, object-level authorization on every court_award_id
endpoint, the complete-bypass rejection, and the row_version plumbing were
attacked directly and held.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to the adversarial-review fixes. The review raised this as a critical
finding and a refuter dismissed it on the grounds that the only wired sender of
Status=given was the Recs Manager's post-grant courtStep, which the spec
sanctioned. That reasoning was sound at the time — and the previous commit
deleted courtStep, which removed the justification along with the sender.
set_award_status writes the status column only; it never links award_id. With
'given' still in the client-settable whitelist, a POST of Status=given against
any planned/announced/staged row produced `status='given' AND award_id IS NULL`
— the one state the grant-safety spec forbids outright. Such a row is invisible
to finalize (getStagedAwards selects status='staged' only), so the recipient
never receives the ork_awards row, and setAwardStatus's own `AND status !=
'given'` guard means nothing can move it back. Terminal, silent, unrecoverable.
Reproduced against the running app before the fix: POSTing Status=given to a
planned row returned {"status":0,"award_status":"given"}, left award_id NULL,
and every attempt to restore it was refused as "already granted". After the fix
the same POST is rejected as an invalid status.
A line now reaches 'given' only through commitStagedAward() or
reconcileGrantForRecommendation(), both of which link the real awards id in the
same statement. The endpoint's one remaining caller (cpUnskipAward) sends
'planned' and is unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dup line, order corruption) The adversarial review raised 15 findings; 12 were adjudicated and 3 were never handed to a refuter, so they were neither confirmed nor dismissed and did not reach the report. All three are in the Court_detail client logic. Verified by hand against HEAD and the running app; all three reproduce. 1. Wrong award added. cpAwardSearch() rebuilt the autocomplete on every keystroke but never cleared the hidden cp-adhoc-award-id. Selecting "Order of the Rose" and then editing the text without picking a new suggestion still posted the ROSE id: the visible text and the submitted award disagreed, and the wrong honor landed on the plan where it could be staged and finalized into the permanent record. Now cleared whenever the text stops matching the remembered selection — compared rather than cleared unconditionally because the same handler is wired to onfocus, where the selection must survive. This is the pattern cpAcSearch() (the player field) already used. 2. One recommendation on two court lines. cpSubmitRecs() left "Add Selected" enabled while its POSTs were in flight, and addAward() had no per-court dedup, so a double-click created two court_award rows for one rec. Per-line idempotency does not help: claimStagedForGrant guarantees each LINE commits once, so both lines finalize into two ork_awards rows. Fixed at both ends — the button locks for the batch, and a rec-backed add now returns the existing live line (AlreadyOnCourt) instead of inserting. The dedup deliberately ignores 'cancelled' rows so an officer who skips a rec and then re-adds it still gets a fresh line, and it never overwrites the existing line's notes/comment/rank — a stray double-submit must not clobber real edits. cpAppendAwardRow() also refuses to paint a second DOM row for a court_award_id already on screen, which likewise protects the delta-sync path. 3. Running order silently rewritten. The Printing List view physically reorders the DOM (scroll-tracked rows to the end, red before green) and hides the rest, while cpSaveOrder() serialises every .cp-award-row including the hidden ones. Any reorder performed while that view was active persisted the filtered arrangement as the court's real running order. Reordering is now refused with an explanation while the view is on (arrows, drag, and both sort buttons). Verified live end to end, and the Recs Manager's full function set was re-tested after these changes rather than only the parts that were edited: paging and every filter/sort, snooze, unsnooze, pass-to-local and its reversal, dismiss, add to court, double-add (one row, same id returned), re-add after skip (new line), create court, grant on both court actions, and CSV export. Test data was reverted from the shared dev DB. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…all sites) into court-planner
Conflicts were all CSS, from master's DRY refactor landing on top of the Court
Planner's modal styles.
- orkui.css body rule: master normalized tabs to spaces but still carried the old
combined `html, body { overflow-x: hidden }`, which makes BOTH elements vertical
scroll containers and produces duplicate scrollbars. Kept this branch's split
(overflow-x:hidden on <html> only, overflow-x:clip on <body>) in master's
normalized style, and preserved master's added scrollbar-gutter.
- revised.css: master merged every modal overlay base, .open state, header and
footer into single DRY rules, while this branch had added the Court Planner's
#kn-addcourt-overlay / #pk-addcourt-overlay and the #kn-cp- / #pk-cp-new-court-
modal blocks. Resolved as a union rather than by re-declaring: the court
overlays are folded into master's merged selector lists, master's expanded
header/footer selector lists are kept, the standalone .pk-badge-ladder that
master DRY'd into the .kn-badge-ladder rule is not reintroduced, and the
new-court-modal blocks ride along unchanged.
- revised.css dark-mode recs table: kept this branch's `table.pk-recs-table`
scoping. Master still had the older `table.dataTable` selector, and narrowing
that scope was the point of the original bugfix.
Verified: no branch-added rule lost (the only lines the sweep flags are the
duplicate overlay declarations deliberately folded into master's merged rules,
whose selectors remain); braces and comments balanced in both files; 19 changed
PHP files lint clean; migration coverage still passes; Court detail/list, Recs
Manager, both court reports, kingdom/park/player profiles and the directory all
render without errors; the served revised.css still carries the court modal rules.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-fix work origin/feature/court-planner had three commits this local branch did not: a merge of upstream/master, "Move Court Planner DB access out of controllers into the domain layer", and "Route Court Planner calls through a Court model" (new orkui/model/model.Court.php). They overlap heavily with the adversarial-review fixes made here, so this is a merge, not a replacement — none of that work is dropped. Conflict resolutions, all resolved toward the refactor's call convention while keeping the fixes' behavior: - CourtAjax::lookupPersona and the update_award_tracking_status court_id lookup: took the incoming routing (Ork3::$Lib->player->GetPersona and $this->Court->get_court_award_court_id). Both are the fuller version of the review's finding #5, which had moved the same two queries into Court:: — so Court::getPersona is now unreferenced and is removed rather than left as dead code. Player::GetPersona / Model_Player::get_persona is what the other four controllers already use. - PlayerAjax grantaward: took the incoming model routing ($this->Court->reconcile_grant_for_recommendation) and carried through this branch's CourtAction argument and returned line count, which the incoming version predates. Model_Court::reconcile_grant_for_recommendation gains the matching $court_action parameter so the officer's "leave on court" / "remove from court" choice still reaches the domain layer. - model.Award::fetch_award_option_groups: kept this side. The merged method body delegates classification to Award::GetAwardOptionGroups(), so the incoming side's compareAwardsByName helper and its comment describing an inline classifier no longer apply. Verified after the merge: every review fix is still present (CourtAction threading, addAward rec-dedup, LAST_INSERT_ID on both insert sites, the dropped internal-note citation fallback, getArtisans, 'given' removed from set_award_status, csvSafe, courtStep gone, cpReorderBlocked, award-search selectedName). Re-tested live through the new model layer: grant with CourtAction=remove cancels the line and writes exactly one award row; with =leave marks it given and links award_id; a repeat add returns the same court_award_id with AlreadyOnCourt; set_award_status still rejects 'given'; court_state and update_award_tracking_status respond; Court detail/list, Recs Manager and both court reports render clean. Test data reverted from the dev DB. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…l review
1. CRITICAL — one honor could reach the permanent ledger many times. The add-to-
court dedup keyed on recommendations_id, but the honor is the cluster
(recipient + award + rank). Several people recommending the same person for the
same award is routine — the production mirror has clusters of fifteen — and the
picker emitted one row per recommendation, so ticking them added one court line
each. At finalize every line is its own idempotency key, so AddAward ran once
per line. Now: the dedup probe matches the rec id OR the cluster key (the same
match the reconcile already builds), getPendingRecommendations groups by cluster
and carries a support count, and finalize tracks the clusters it has committed
and cancels any later line for the same honor rather than leaving it to commit
on the next run. Verified: adding two real sibling recs now yields one line.
2. CRITICAL — the cross-path reconcile rewrote courts it had no business touching.
Its UPDATE matched on a global rec id or the cluster key with no court join: no
court scope, no authority check, no court-status check. Finalize's "Leave As-Is
and Close" legitimately leaves 'planned' lines on a completed court, so a
Recs-Manager grant months later flipped a finalized court's line to 'given' and
stamped it with the new award and giver — publishing an honor on the login-free
Court Report for a ceremony where it was never announced. Nothing in that
request path authorized against the rows being mutated. Now candidates are
selected (excluding complete courts), filtered per row through canManage against
that row's OWN court, and only then updated by explicit id; an actor id of 0
reconciles nothing. Verified: a completed court's line is untouched, a
kingdom-17 officer cannot touch a kingdom-18 court line, and the ordinary path
still reconciles.
3. HIGH — anonymous recommenders were unmasked to the wrong people. Masking was
computed from the requesting viewer and then written into a response cached for
300s under a key with no viewer dimension: an admin warming the cache exposed
every anonymous recommender to non-admins for the life of the entry, and a
non-admin warming it blinded admins. Masking now happens per request in
applyViewerFlags(), after the viewer flags are derived, so it also restores the
ability of someone who filed an anonymous recommendation to edit their own
reason — that was broken even uncached, because the masked null id could never
match the viewer. Verified in both warm orders, and for the author.
4. MEDIUM — a withdrawn grant could still be committed. unstageAward reverted only
the status, leaving the giver and public citation the officer had just
withdrawn, and 'staged' was client-settable through set_award_status, which
writes status alone and skips the giver/citation capture. Together those let
finalize commit rejected values past the giver backstop. unstageAward now clears
the giver and citation ('' — yapo drops nulls), and 'staged' has left the
whitelist; staging happens only through grant_award/bulk_record_grants.
5. MEDIUM — Run mode never delivered the one-tap grant the mode exists for. The
badge and row actions keyed purely off status, so a live ceremony showed the
amber "Staged" badge and "nothing is recorded until finalize" wording that spec
S2 says to hide. Run mode now renders a staged row as Given with an Undo, in
the PHP render and both JS renderers. Display only — the row stays 'staged'
underneath and Undo still un-stages, so the finalize guarantees are unchanged.
6. LOW — removed a QW#4 "keep the status select in sync" block whose target
element does not exist anywhere in the file.
Verified live for each fix, plus a full page smoke test; the dev DB was restored
to its pre-test state. Not covered: no concurrency was exercised, and PHPUnit
still cannot run here (no composer).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Court Planner + Recommendations workflow
A complete workflow for planning royal courts and managing award recommendations.
Court Planner
staged; it does not touch the permanent record. A single Finalize (folded into Complete Court) batch-commits every staged grant, so undo before finalize is free. The idempotency key is the court line — an atomicstaged→givenclaim means a line commits at most once, and finalize is throw-safe (a failedAddAwardreverts that row tostagedand the court will not complete until it is clear)row_version) plus a presence heartbeat, so several reeves can run one court live without silently clobbering each otherRecommendations Manager
CourtActionand applied in the domain layerArchitecture
DB work lives in
system/lib/ork3/(Court+Reportlibs); controllers stay thin and reach the domain layer throughorkui/model/model.Court.php.Migrations
Ten migrations under
db-migrations/— court tables, stage/finalize + grant-safety columns, recommendation snooze / pass-to-local, the notification table, and an idempotent repair forork_session.user_agent/ip. Apply before deploy.Review
The branch went through two rounds of adversarial review (hostile multi-agent panels; findings kept only if they survived an organized attempt to refute them), the second run after a layering refactor moved the Court Planner behind a model. Everything found in both rounds is fixed.
Round 2 — the ledger and the public record
canManageagainst that row's own court, and completed courts are excluded.stagedstayed client-settable), Run mode never delivered the one-tap grant the mode exists for, and a dead status-sync block was removed.Round 1 — the grant seam
givenwithout linking an award id; concurrent adds returned the wrong row id (LAST_INSERT_IDvs a "highest id" re-query); a double-click could put one recommendation on two court lines; the award autocomplete could submit a different award than the one displayed; reordering under the Printing List filter corrupted the running order; and the CSV export was open to formula injection via player-written text.Verification
PHP lint clean across changed files; ork-db migration coverage passes; end-to-end HTTP + DB checks on every fix and on grant (both court actions), stage/finalize, add/dedup, snooze, pass-to-local, dismiss, pagination, filters/sorts and export. Not covered: PHPUnit was not run (no composer in the dev container), and no concurrency was exercised — the multi-reeve and atomic-claim guarantees are reasoned from the SQL shape, not reproduced under load.
🤖 Generated with Claude Code