feat: consolidate PMC client UI PRs (PMC-2..PMC-119) #41

Merged
vas2000-work merged 45 commits from feat/pmc-merge into main 2026-05-21 02:49:21 +02:00
Owner

Consolidates the 15 open PMC client-UI PRs (#26-#40) into a single branch so they can be reviewed and merged as one unit instead of 15 interdependent PRs.

What this branch contains

All 15 PMC branches merged into feat/pmc-merge off main with real merge commits (git merge --no-ff), preserving every PR's individual commits:

  • #26 API client layer (PMC-2)
  • #27 sidebar scrollbar affordance on short viewports (PMC-17)
  • #28 wire tickets Add Note / New / Log Time / Detail title (PMC-23)
  • #29 work_item navigation on TimeEntryListPage (PMC-31)
  • #30 projects route-param titles + hide decorative Add Task (PMC-36)
  • #31 contacts detail titles, hide Edit, demote stats (PMC-41)
  • #32 render calendar dispatch board appointments (PMC-50)
  • #33 implement ContractNewPage form (PMC-55)
  • #34 billing InvoiceNew form, route-param title, PaymentList links (PMC-60)
  • #35 assets AssetNew form + Related Tickets sub-table links (PMC-66)
  • #36 KB editor, related links, route-param title (PMC-72)
  • #37 hide decorative Add Tenant in admin (PMC-88)
  • #38 portal row nav, file input, invoice detail, KB links, titles (PMC-92)
  • #39 route-param titles + data-testid prop (PMC-100)
  • #40 add claude-fix CI workflow (PMC-119)

Merge conflict resolved

One textual conflict, in src/pages/admin.rs (PMC-2 vs PMC-88): both branches edited the component import block. Resolved by keeping PMC-88's removal of the decorative "Add Tenant" button together with PMC-2's live-tenant data fetch; the merged import set drops Button/ButtonVariant/IconSize/PlusIcon, which are unused once the button is gone.

CI cleanup

The source branches carried pre-existing rustfmt drift and clippy warnings (their CI was already red). To land this branch green, two extra commits were added on top of the merges: a cargo fmt --all normalization, and a clippy pass that resolves every cargo clippy --all-targets -- -D warnings finding, including warnings pre-existing in main's src/modules/*.

Verification

Run in the local dev container (rust 1.94.1):

  • cargo check --target wasm32-unknown-unknown - passes
  • cargo clippy --all-targets -- -D warnings - passes
  • cargo test --lib - 47 passed
  • cargo fmt --all --check - clean

Supersedes

Replaces #26, #27, #28, #29, #30, #31, #32, #33, #34, #35, #36, #37, #38, #39, #40 - those PRs are closed in favour of this one.

Consolidates the 15 open PMC client-UI PRs (#26-#40) into a single branch so they can be reviewed and merged as one unit instead of 15 interdependent PRs. ## What this branch contains All 15 PMC branches merged into `feat/pmc-merge` off `main` with real merge commits (`git merge --no-ff`), preserving every PR's individual commits: - #26 API client layer (PMC-2) - #27 sidebar scrollbar affordance on short viewports (PMC-17) - #28 wire tickets Add Note / New / Log Time / Detail title (PMC-23) - #29 work_item navigation on TimeEntryListPage (PMC-31) - #30 projects route-param titles + hide decorative Add Task (PMC-36) - #31 contacts detail titles, hide Edit, demote stats (PMC-41) - #32 render calendar dispatch board appointments (PMC-50) - #33 implement ContractNewPage form (PMC-55) - #34 billing InvoiceNew form, route-param title, PaymentList links (PMC-60) - #35 assets AssetNew form + Related Tickets sub-table links (PMC-66) - #36 KB editor, related links, route-param title (PMC-72) - #37 hide decorative Add Tenant in admin (PMC-88) - #38 portal row nav, file input, invoice detail, KB links, titles (PMC-92) - #39 route-param titles + data-testid prop (PMC-100) - #40 add claude-fix CI workflow (PMC-119) ## Merge conflict resolved One textual conflict, in `src/pages/admin.rs` (PMC-2 vs PMC-88): both branches edited the component import block. Resolved by keeping PMC-88's removal of the decorative "Add Tenant" button together with PMC-2's live-tenant data fetch; the merged import set drops `Button`/`ButtonVariant`/`IconSize`/`PlusIcon`, which are unused once the button is gone. ## CI cleanup The source branches carried pre-existing rustfmt drift and clippy warnings (their CI was already red). To land this branch green, two extra commits were added on top of the merges: a `cargo fmt --all` normalization, and a clippy pass that resolves every `cargo clippy --all-targets -- -D warnings` finding, including warnings pre-existing in `main`'s `src/modules/*`. ## Verification Run in the local dev container (rust 1.94.1): - `cargo check --target wasm32-unknown-unknown` - passes - `cargo clippy --all-targets -- -D warnings` - passes - `cargo test --lib` - 47 passed - `cargo fmt --all --check` - clean ## Supersedes Replaces #26, #27, #28, #29, #30, #31, #32, #33, #34, #35, #36, #37, #38, #39, #40 - those PRs are closed in favour of this one.
Adds an `ApiError` enum (`Network`, `Status { code, message }`, `Decode`) alongside the existing string-returning fetch helpers, plus `_typed` variants of get_authed/post_authed/put_authed/delete_authed that decode the server's `ErrorResponse` envelope so callers can react to 401/403/404/422/etc. without re-parsing strings. `ApiError::user_message` produces the copy that gets dropped straight into a toast.

The toast surface is a `GlobalSignal<Vec<Toast>>` driven by `push_toast`/`push_toast_with_title` plus a `push_api_error` convenience that maps an `ApiError` to an Error-variant toast. `ToastRoot` mounts once inside `AppLayout` so every authed page is already wired; no per-page plumbing needed.

The existing `Result<T, String>` helpers stay so live callers (companies, calendar, system version) keep compiling. New call sites should prefer the typed variants going forward.

#PMC-10 State Done
CompanyListPage now drives `loading` from the in-flight state of the live-backend resource and swaps the table body to `TableLoading` skeleton rows while the first fetch is outstanding, instead of flashing the demo rows on every page load. When the backend responds with zero rows it shows `TableEmpty` rather than silently falling back to demo data, which had been masking real "the tenant has no companies yet" cases.

This is the reference wiring for the loading/empty pattern; subsequent list-page wires (tickets, contacts, admin tenants) will follow the same shape.

#PMC-11 State Done
TicketListPage now fetches from `GET /api/v1/tickets` via `use_resource` and renders the live rows when the backend answers, while keeping the existing hardcoded TKT-* rows behind a `Demo` source so the page stays demoable when the tenant has no rows yet or the user is signed out.

Status and priority names are humanized from the server's lowercase values; `assigned_to_name` falls back to "Unassigned"; `updated_at` is rendered relative via `relative_time`. Loading and empty states ride the same `TableLoading`/`TableEmpty` pattern just landed on companies.

Detail-page, create-ticket POST, and add-note POST wiring are out of scope here and tracked under PMC-28/PMC-27/PMC-25 respectively.

#PMC-6 State Done
ContactListPage now fetches from `GET /api/v1/contacts` via `use_resource` and renders the live rows when the backend answers, falling back to the seeded demo rows when the route is missing or the user is signed out. Email/phone/role/company columns tolerate missing fields (server contact records frequently have nulls there).

Companies list was already wired in a prior commit; combined with this change, both list views in the contacts module now drive off the same progressive-enablement pattern. Detail and create wires are tracked under PMC-46/PMC-47/PMC-48/PMC-49 in the contacts UI story.

#PMC-7 State Done
feat(admin): wire TenantManagementPage to /tenants with demo fallback (PMC-8)
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 8s
Create release / Create release from merged PR (pull_request) Has been skipped
eaea8c6fd6
TenantManagementPage now fetches the live tenant list via `GET /api/v1/tenants` when the multi-tenant feature is on, falling back to the seeded demo rows when the endpoint isn't reachable or the user is signed out. The same TableLoading/TableEmpty progressive-enablement pattern that landed on companies/contacts is used here.

User counts and MRR are not yet on `TenantResponse` and are tracked under PMC-91; this commit renders "0" / "-" placeholders for those columns so the table layout stays stable until the server adds aggregate fields.

The Bunyip hub now owns the day-to-day tenant switcher; this admin surface remains useful as a super_admin read-only roster, hence the wiring per the project decision to keep PMC-88 in scope.

#PMC-8 State Done
fix(layout): show scrollbar affordance on short-viewport sidebars (PMC-22)
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 4s
Create release / Create release from merged PR (pull_request) Has been skipped
404783ef4f
The desktop sidebar previously used \`.scrollbar-hide\` so a long nav list could scroll without visual chrome. At narrow viewport heights the nav overflows but the hidden bar leaves the user no signal that there's more below, and the main panel's own scrollbar makes it look like the bottom nav items are simply missing.

Switching the sidebar to \`.scrollbar-thin\` keeps the column visually quiet at full height (where the nav fits and nothing scrolls) but surfaces a slim affordance the moment the viewport gets short enough to push items below the fold.

#PMC-22 State Done
Replace the hardcoded "TKT-1234: Email server not responding" header with "Ticket {props.id}" so every detail-page URL gets a distinct title and the browser tab/back-button history reflect the route the user actually navigated to. Real ticket numbers and subjects will replace the bare id once the detail-page fetch lands (tracked under the API client story).

#PMC-28 State Done
The Log Time button on TicketDetailPage had no onclick handler. Wrap it in a Link to Route::TimeEntryNew so the click navigates to the time entry form instead of being a dead affordance. The destination form does not yet pre-select the originating ticket; threading a ticket_id query param into TimeEntryNew is a follow-up that requires extending the router and the TimeEntryNewPage form, tracked as part of the time-tracking story.

#PMC-26 State Done
TicketNewPage no longer rides a 1-second mock timeout. The submit handler now POSTs the form data to /api/v1/tickets and navigates to the new ticket's detail page on success.

The company dropdown still serves the hardcoded "1"/"2"/"3" placeholder options pending the contacts wire; non-UUID values are mapped to `Uuid::nil()` so the request exercises the wire and the server returns a validation error rather than 400ing on JSON shape. On failure the error is logged to the browser console and the form stays mounted so the user can retry without losing input. Toast surfacing of the error lands with the API client story (PMC-10).

#PMC-27 State Done
feat(tickets): wire Add Note modal submit + inputs (PMC-25)
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 4s
Create release / Create release from merged PR (pull_request) Has been skipped
85473f9006
The Add Note modal on TicketDetailPage was a UI shell: the Select and Textarea fed no signals, and the footer Add Note button had no onclick. Bind `note_type` / `note_content` signals to the inputs, wire the footer button to POST `/api/v1/tickets/{id}/notes` (using the route's `props.id`), close the modal and clear the textarea on success.

Empty-content submissions short-circuit instead of round-tripping. POST failures log to the browser console; the toast surface from PMC-10 isn't on this branch yet so we keep the modal open and let the user retry.

#PMC-25 State Done
feat(time): work_item navigation on TimeEntryListPage (PMC-35)
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 4s
Create release / Create release from merged PR (pull_request) Has been skipped
4d991a5884
TimeEntryListPage rows had a styled-blue "TKT-1234" span that looked clickable but was inert. Treat the work_item prefix as a routing hint: `TKT-*` linkifies to `TicketDetail`, `PRJ-*` linkifies to `ProjectDetail`, and anything else (e.g. `Internal`) renders as a non-link span so the styling matches the affordance. There is no per-time-entry detail route yet, so a true row-level click is deferred until that surface lands.

#PMC-35 State Done
ProjectDetailPage hardcoded the title "Network Infrastructure Upgrade" so every project URL rendered the same header. Switch the title and tab title to "Project {props.id}" so navigation history reflects the route. Real project names will replace the bare id once the server's projects module ships.

The Add Task header button had no onclick and no server destination; per the F5 pattern (audit P1-05 "decorative detail-page buttons") hide it until the projects module lands so the affordance stops lying. Live wiring is tracked under PMC-39 alongside the other Add Task surface.

#PMC-40 State Done
#PMC-38 State Done
fix(projects): hide decorative Add Task on tasks page (PMC-39)
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 4s
Create release / Create release from merged PR (pull_request) Has been skipped
134947ed0b
ProjectTasksPage had a header Add Task button with no onclick and no destination (server projects module is still stubbed at 501). Per the F5 pattern, hide it until the server lands so the affordance stops promising a flow that doesn't exist. Also switches the page header to "Project {props.id} - Tasks" so it reflects the route.

#PMC-39 State Done
fix(contacts): detail-page titles, hide Edit, demote Open Tickets stat (PMC-46, PMC-47, PMC-48, PMC-49)
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 8s
Create release / Create release from merged PR (pull_request) Has been skipped
e42460d33c
CompanyDetailPage and ContactDetailPage hardcoded "Acme Corp" / "Bob Johnson" headers regardless of the route, and the Edit header button had no onclick. Switch the headers to "Company {props.id}" / "Contact {props.id}" so navigation reflects the route, and hide the Edit affordance per the F5 pattern until the contacts mutation surface ships.

The Open Tickets statistic on CompanyDetailPage was styled link-blue but was a bare span, so it looked clickable and wasn't. There's no /tickets?company=:id filter to wire it to yet, so drop the blue styling and render it as a plain stat.

PMC-47: the contacts and recent-tickets sub-tables on CompanyDetailPage already wrap names in Link components pointing to ContactDetail / TicketDetail (lines 388-422 / 447-468). ContactDetailPage's sidebar already links its Company back to CompanyDetail. No remaining dead row clicks; sub-task validated as resolved in main.

#PMC-46 State Done
#PMC-47 State Done
#PMC-48 State Done
#PMC-49 State Done
feat(calendar): render dispatch board appointments (PMC-53)
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 4s
Create release / Create release from merged PR (pull_request) Has been skipped
78c9d85696
TechnicianRow on /dispatch was receiving an `appointments` vec but rendering 9 empty time-slot divs and silently ignoring the data. Replace the empty cells with a single relative-positioned grid area spanning the 9 hour columns, drop the hour divider lines into it as a background grid, and absolutely-position one colored block per appointment using start/end percent offsets within the 8am-5pm range.

`parse_dispatch_time` handles the "H:MM AM/PM" shape used by the seed data and gracefully degrades to a 1-hour block at first-hour if the input doesn't match, so a typo doesn't crash the page. `dispatch_color` maps the existing `onsite` / `remote` / `meeting` / `internal` kinds to badge colors.

#PMC-53 State Done
feat(contracts): implement ContractNewPage form (PMC-56)
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 4s
Create release / Create release from merged PR (pull_request) Has been skipped
3128d64dc4
ContractNewPage was a placeholder shell with literal "Contract creation form would go here." text. Add a real form with name, company select, contract type (Managed / Block Hours / T&M / Fixed Price), value, and start/end dates - mirroring the field set the server's contracts module will eventually need.

The server contracts module is still stubbed (501) so the submit handler rides the same 1s stub pattern as the other unwired forms and navigates to the list. POST goes live once the server contracts module ships.

#PMC-56 State Done
InvoiceDetailPage hardcoded "Invoice INV-2025-001" so every invoice URL rendered the same header. Switch the title and tab title to "Invoice {props.id}" so navigation reflects the route. Real invoice numbers replace the bare id once the billing module ships server-side.

#PMC-65 State Done
Payment rows on /payments rendered "INV-2024-097" as a blue-styled span - clickable-looking but a dead text cell. Extract the row to a PaymentRow component that wraps the invoice label in a Link to InvoiceDetail (using a stable id; live ids replace these strings once the billing module ships).

#PMC-64 State Done
feat(billing): implement InvoiceNewPage form (PMC-61)
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 3s
Create release / Create release from merged PR (pull_request) Has been skipped
b424683e74
InvoiceNewPage was a placeholder shell with literal "Invoice creation form would go here." text. Add a real form with bill-to / contract / issue-date / due-date / a single line-item row (description, qty, unit price) / notes - matching the field set the server's billing module will eventually need.

Multi-line invoices are deferred to the server module landing (added a small inline note rather than scaffolding repeater logic against a 501 endpoint). Submit rides the stubbed-form pattern (1s timeout + navigate to list) until the billing module ships.

#PMC-61 State Done
The Related Tickets sub-table on /assets/:id rendered TKT-#### codes as blue-styled spans - clickable-looking but dead. Wrap them in Link components targeting TicketDetail so the affordance matches.

Hardcoded ids (1234 / 1150) match the existing demo seed data; live joins replace these once the assets module ships a real /api/v1/assets/:id/tickets fan-out.

#PMC-71 State Done
feat(assets): implement AssetNewPage form (PMC-67)
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 7s
Create release / Create release from merged PR (pull_request) Has been skipped
9e2594a59e
AssetNewPage was a placeholder shell with literal "Asset creation form would go here." text. Add a real form with name / type / company / serial / location / warranty / notes - matching the field set the server's assets module will need.

Submit rides the stubbed-form pattern (1s timeout + navigate to list) until the server assets module ships its POST endpoint.

#PMC-67 State Done
feat(kb): scaffold article editor, linkify related, route-param title (PMC-73, PMC-75, PMC-77)
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 8s
Create release / Create release from merged PR (pull_request) Has been skipped
ae79cac264
KBArticleNewPage was a placeholder. Scaffolds a real editor with title / category / visibility / Markdown body. WYSIWYG is deferred (a comment notes that); submit stubs to navigate until the KB module ships.

KBArticleDetailPage hardcoded a "How to Reset a User's Password" header and the Edit button had no onclick. Switch header to "Article {props.id}" and hide Edit per F5. Related Articles links wired to Route::KBArticleDetail with slug placeholders (real ids land with the KB module).

#PMC-73 State Done
#PMC-75 State Done
#PMC-77 State Done
fix(admin): hide decorative Add Tenant on TenantManagementPage (PMC-90)
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 8s
Create release / Create release from merged PR (pull_request) Has been skipped
265c19176f
Add Tenant header button had no onclick and no destination - this client doesn't ship a tenant-provisioning surface, and the canonical add-tenant flow now lives in the Bunyip hub per the SSO migration. Per the F5 pattern, hide the affordance until/unless a client-side flow lands so users stop expecting one.

#PMC-90 State Done
feat(portal): row nav, file input, invoice detail, KB links, route titles (PMC-95, PMC-96, PMC-97, PMC-98, PMC-99)
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 3s
Create release / Create release from merged PR (pull_request) Has been skipped
858fa21c96
- PMC-95: PortalTicketListPage rows extracted into PortalTicketRow with Link + clickable navigation to PortalTicketDetail. No more inert TKT-#### spans.
- PMC-96: PortalInvoiceDetailPage now renders a read-only invoice view (bill-to / line items / total / payment-pending note) instead of the literal "Invoice details would be displayed here" placeholder. Live data lands when the portal billing endpoint ships.
- PMC-97: PortalKBPage article items wrap in Link to Route::KBArticleDetail with slug placeholder ids so clicks actually go somewhere.
- PMC-98: Replace the no-input "drag and drop" zone on the portal ticket form with a real `<input type=file multiple>`. Styled drag-drop with upload progress lands alongside the portal attachments endpoint.
- PMC-99: PortalTicketDetailPage uses "Ticket {props.id}" for both the layout title and the header h1. The audit noted "decorative header buttons" but this page has no header button bar; the only buttons are the inline Send Reply at the bottom of the Reply form. The route-param title fixes the practical effect (every URL got the same header) covered by this issue.

#PMC-95 State Done
#PMC-96 State Done
#PMC-97 State Done
#PMC-98 State Done
#PMC-99 State Done
AssetDetailPage and ContractDetailPage were the two remaining detail surfaces still rendering hardcoded sample headers ("Exchange Server 01", "Managed Services Agreement"). Switch them to "Asset {props.id}" / "Contract {props.id}" to complete the F10 sweep; the other detail pages were already done in their per-story PRs.

#PMC-104 State Done
feat(components): optional data-testid prop on Button, Input, TableRow (PMC-111)
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 4s
Create release / Create release from merged PR (pull_request) Has been skipped
de5747b649
Adds an `Option<String>` `data_testid` prop to Button, Input, and TableRow that flows to `data-testid="..."` on the rendered element when set. Enables stable selectors for Playwright / MCP browser automation without leaking test scaffolding into normal builds; when omitted (the default), no attribute is emitted.

Modal and form Select/Textarea can pick up the same prop as needed; landing the pattern on the three highest-traffic components is the cheap unlock.

#PMC-111 State Done
ci: add claude-fix workflow
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 9s
Create release / Create release from merged PR (pull_request) Has been skipped
f19c6125ac
Vendored copy of .forgejo/workflows/claude-fix.yml from the claude-fix-sandbox repo. Provides a manually-dispatched Forgejo workflow that runs Claude Code against a failing PR: checks out the PR branch, builds failure context, runs claude in --bare/headless mode with a restricted allowedTools list, then commits and pushes any changes using a bot PAT so CI re-runs. A [skip claude] commit tag guards against runaway loops.

The workflow has no repo-specific hardcoded values (it derives repo and host from github.repository / github.server_url), so it is copied unchanged. To enable it, ANTHROPIC_API_KEY and CLAUDE_BOT_PAT must be configured under Settings > Actions > Secrets.

#PMC-119 State Done

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts:
#	src/pages/admin.rs
Normalizes formatting drift inherited from the 15 PMC source branches
(and pre-existing in main) so the consolidated branch passes the CI
fmt-check gate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(clippy): resolve all clippy warnings for the CI -D warnings gate
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 27s
f275f68406
Clears every warning flagged by `cargo clippy --all-targets -- -D warnings` so the consolidated branch passes CI. Covers warnings from the PMC branches and ones pre-existing in main (src/modules/*), per the agreed full-cleanup scope.

Rewrites: manual `div_ceil` in table.rs and fetch.rs; manual `Option::map` in time.rs; manual clamp in pagination.rs; AuthState now derives Default; unused `///` doc comment on a thread_local turned into `//`.

Suppressions with rationale: clippy::type_complexity and clippy::derivable_impls on fetch.rs (the generic Default impl is intentionally unbounded); file-level clippy::should_implement_trait on the four model modules (their `from_str` returns Option by design and intentionally does not implement std::str::FromStr).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ci(check): stub the generated CSS asset before cargo steps
All checks were successful
Check / clippy + fmt + tests (pull_request) Successful in 1m23s
Create release / Create release from merged PR (pull_request) Has been skipped
964f092bab
`assets/styles.css` is a gitignored Tailwind build artifact, so a clean CI checkout does not have it, and `cargo check`/`clippy`/`test` fail on the `asset!("/assets/styles.css")` macro in src/main.rs. This workflow only compile-validates and never renders CSS, so an empty file is sufficient; the real Tailwind build stays in oci-build/Dockerfile for deployment. Pre-existing main bug, unrelated to the consolidated PMC branches.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
vas2000-work deleted branch feat/pmc-merge 2026-05-21 02:49:21 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-apps!41
No description provided.