"Shared with me" Inbox¶
The receiving stringer's surface — the place a stringer sees jobs that other stringers (Rule #1) or clients (Rules #2/#3, V3) have shared with them. Mobile-first; standalone V2 view (not auto-merged with the grantee's existing ClientProfile views per OQ-5 closure). Owned by Mira. Cross-cuts: client-identity-and-sharing § hidden requirements, ADR-0004 § Visibility / redaction, ADR-0005, stringer-dashboard § Inbox, share-management.
Source requirement¶
- client-identity-and-sharing § hidden requirements item 1: "'Shared with me' inbox view for receiving stringers — a single list of all jobs currently shared with them, by source. Filterable by client and by source stringer."
- OQ-5 closure — V2 keeps the inbox standalone; even when the receiving stringer already has a ClientProfile for the same Person, shared jobs do NOT auto-merge into the receiving stringer's existing views of that client. Auto-link is V3 polish.
- ADR-0004 § Visibility / redaction matrix — under Rule #1, the receiving stringer sees: racket + strings + tensions + colors + DT + method + Strung/Ordered dates + the client's first name. Does not see: last name, email, phone, address, comments, pricing.
- stringer-dashboard § Inbox section — the entry point ("Shared with me" link with unread count chip on the dashboard).
- ADR-0005 § notification_kind enum —
share_granted_to_menotifications drive the unread-badge count;share_revoked_from_meremoves a row from the inbox.
Goal¶
Answer the receiving stringer's three questions, in order:
- What was shared with me? — the list, by source, with the freshly-arrived items highlighted.
- What's the technical setup on this job? — a per-grant detail page that surfaces racket + strings + tensions + colors + DT, with the redaction explicit.
- Do I want to keep this? — a Revoke action (the grantee can refuse a grant per § Edit + revoke semantics).
The inbox is read-only on the orders themselves — the receiving stringer never edits a shared job. They can only revoke (refuse) the grant.
V2 standalone-only — what this means¶
Per OQ-5:
- Distinct surface. A receiving stringer who already has Lukas Müller in their own ClientProfile list does NOT see Stefan's shared Lukas-Müller jobs intermingled with their own Lukas-Müller view. The shared jobs live here, in the inbox; the receiving stringer's own jobs for Lukas live in their normal client view.
- Unambiguous redaction boundary. This is the design rationale per the closure: "the receiving stringer can never confuse 'what I see because I have my own relationship with this client' with 'what I see because Stringer A shared a job'".
- V3 may collapse the two views once the surface is mature. This page commits the V2 standalone shape and flags the V3 collapse as out of scope.
Information architecture¶
"Shared with me" inbox
├── Filter / sort / search bar
├── List grouped by source (granter)
│ └── Per-grant card → tap → detail view
├── Empty state
└── (notification badge on entry — drives unread count)
The detail view is a separate surface (the Rule #1 grantee read view of an Order), described in § Per-grant detail view.
Viewports¶
sm 375 px (mobile-first baseline)¶
┌───────────────────────────────────────┐
│ ← Shared with me 3 new │ Header + unread chip
├───────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────┐ │
│ │ 🔍 Search jobs │ │ Search input
│ └───────────────────────────────────┘ │
│ ┌─────────┐ ┌─────────┐ ┌──────────┐ │
│ │ All ▾ │ │Source ▾ │ │ Recent ▾ │ │ Filter chips
│ └─────────┘ └─────────┘ └──────────┘ │
│ │
│ from Stefan Wagen Rule #1 │ Group header
│ ┌───────────────────────────────────┐ │
│ │ ● Lukas · Pure Aero 98 │ │ Card (unread dot)
│ │ Strung 2026-04-30 │ │
│ │ 24/24 kg · ALU Power 1.25 │ │
│ │ Open → │ │
│ └───────────────────────────────────┘ │
│ ┌───────────────────────────────────┐ │
│ │ Lukas · Pure Aero 98 │ │ Card (read)
│ │ Strung 2026-04-15 │ │
│ │ 25/24 kg · ALU Power 1.25 │ │
│ │ Open → │ │
│ └───────────────────────────────────┘ │
│ │
│ from Lisa Berger Rule #1 │ Group header
│ ┌───────────────────────────────────┐ │
│ │ ● Marc · RF 97 v14 │ │
│ │ Strung 2026-04-25 │ │
│ │ 23/22 kg · Hyper-G 1.20 │ │
│ │ Open → │ │
│ └───────────────────────────────────┘ │
│ │
└───────────────────────────────────────┘
The unread dot (●, indigo-700) sits at the row leader; the row also gets bg-indigo-50 until viewed. After the receiving stringer opens the detail view, the inbox row is marked read (writes notifications.read_at = now() for the corresponding share_granted_to_me row).
md 768 px¶
Same single column at 720 px max width, centered, with space-8 between source groups instead of space-6. Filter chips align horizontally with more breathing room.
lg 1280 px¶
Two-pane master-detail layout when desktop:
┌────────────────────────────────────────────────────────────────────┐
│ ← Shared with me 3 new │
├──────────────────────────────────┬─────────────────────────────────┤
│ 🔍 Search jobs │ │
│ [All ▾] [Source ▾] [Recent ▾] │ (Detail view of selected │
│ │ shared job — see § Per-grant │
│ from Stefan Wagen Rule #1 │ detail view.) │
│ ┌────────────────────────────┐ │ │
│ │ ● Lukas · Pure Aero 98 │ │ │
│ │ 2026-04-30 · 24/24 kg │ │ │
│ │ ALU Power 1.25 │ │ │
│ └────────────────────────────┘ │ │
│ ⋯ │ │
└──────────────────────────────────┴─────────────────────────────────┘
~40% ~60%
The list pane stays scrollable; tapping a card updates the detail pane (HTMX swap). Without JS, lg falls back to single-pane navigation (the sm flow) — desktop with JS off is a vanishingly rare case.
Component breakdown¶
Header¶
- Back arrow → dashboard.
- H1 "Shared with me" + unread count chip ("3 new"),
text-tinyonindigo-50background,indigo-700text,rounded-full. - No "Issue new grant" CTA here — the inbox is the receiving stringer's surface; granting is the granter's surface (see share-management).
Search bar¶
- Input placeholder "Search jobs". Hit target ≥ 48 px tall.
inputmode="search". - Behaviour:
keyupHTMX swap of the list (hx-trigger="keyup changed delay:200ms"). Search matches across the non-redacted fields: - Client first name (Rule #1 visible).
- Racket make / model / version.
- Main + cross string manufacturer / model / gauge.
- Source stringer's display name (helps "find what Sarah shared").
- Cannot search across last name / email / phone / comments / pricing (those fields aren't fetched server-side for Rule #1 grantees per ADR-0004 § Visibility / redaction).
Filter chips¶
| Chip | Default | Options |
|---|---|---|
All ▾ |
All | All / Unread only / Read only. Maps to notifications.read_at IS NULL / IS NOT NULL for the underlying share_granted_to_me rows. |
Source ▾ |
All sources | All sources / per-source-stringer. Multi-select via the dropdown. |
Recent ▾ |
Sort by recency | Recent first (sort by order_shares.created_at DESC) / Oldest first / By client (A→Z). |
Chips are native <select> styled or <details>-driven custom dropdowns. On sm, the chips wrap to a second row when needed.
Source groups + cards¶
Cards group by source granter. Per § hidden requirements item 1: "Filterable by client and by source stringer." Default is group by source stringer (since "what did Sarah share with me" is the more common entry-point). The Recent ▾ chip's "By client (A→Z)" option re-groups by client.
| Slot | Content | Source / notes |
|---|---|---|
| Group header | "from {granter display name} Rule #1" | granter.display_name; the rule label clarifies the redaction scope. Per § Visibility / redaction matrix. |
| Card unread dot | ● indigo-700, 8 px |
Drawn when the corresponding share_granted_to_me notification row has read_at IS NULL. |
| Card content line 1 | "{client_first} · {racket_make+model+version}" | First name only per Rule #1 redaction. |
| Card content line 2 | "Strung {date}" | Order.strung_at, ISO YYYY-MM-DD. |
| Card content line 3 | "{tensions} · {string spec}" | Compact — "24/24 kg · ALU Power 1.25". The cross-equals-main collapse from the receipt design carries over here. |
| Card affordance | "Open →" | Whole card is the tap target (≥ 44 × 44 px). |
Card chrome: rounded-lg, white surface (or indigo-50 for unread), shadow-card, 1 px border slate-200 on sm. Group headers separate sources visually with space-8 vertical gap.
Empty state¶
Three flavours:
| Empty case | Render |
|---|---|
| No grants ever received | Centered illustration (Lucide inbox 48 px slate-300) + "No one's shared anything with you yet." in text-h2 slate-600 + a one-line hint: "When another stringer or a client shares a job with you, it'll show up here." |
| Some grants received, but all current filters are empty | Smaller "No matches for these filters." with a "Clear filters" link. |
| All grants revoked (the inbox once held items, now empty) | "Nothing currently shared with you. Past grants are in the Audit log." |
The first state only shows on a fresh account; once any grant lands, this never re-renders.
Per-grant detail view¶
The receiving stringer taps a card; this is the read-only Rule-#1 view of the shared Order. Critical: this is NOT the receipt PDF (per receipt-pdf § Redaction modes) — the receipt PDF is for the originating stringer + client only. Rule #1 grantees get an HTML detail page, in the platform UI, with the redaction matrix applied.
sm 375 px¶
┌───────────────────────────────────────┐
│ ← Shared by Stefan Wagen Rule #1 │ Header
├───────────────────────────────────────┤
│ │
│ Lukas's Pure Aero 98 │ H1 (first name + racket)
│ Strung 2026-04-30 · Ordered 2026-04-28│ Subhead — dates
│ │
│ Racket │ H2
│ ┌───────────────────────────────────┐ │
│ │ Babolat Pure Aero 98 │ │
│ │ (PA98_2023_25) │ │ instance ID if set
│ └───────────────────────────────────┘ │
│ │
│ Main string │
│ ┌───────────────────────────────────┐ │
│ │ Luxilon ALU Power 1.25 │ │
│ │ 24 kg · color: black │ │
│ │ [BYO] │ │ if set
│ └───────────────────────────────────┘ │
│ │
│ Cross string │
│ ┌───────────────────────────────────┐ │
│ │ Same as main │ │ short form when applicable
│ └───────────────────────────────────┘ │
│ │
│ Method · Dynamic tension after │
│ ┌───────────────────────────────────┐ │
│ │ Standard · 22 kg │ │
│ └───────────────────────────────────┘ │
│ │
│ ┌─────── REDACTION NOTICE ────────┐ │
│ │ ⓘ This is a Rule #1 share. │ │ Always rendered
│ │ The granter chose to share │ │
│ │ the technical setup with you. │ │
│ │ Hidden from you: │ │
│ │ • Lukas's full name, contact │ │
│ │ • Comments on this job │ │
│ │ • Pricing │ │
│ └─────────────────────────────────┘ │
│ │
│ Granted 2026-04-30 by Stefan Wagen. │
│ ┌─────────────────────────────────┐ │
│ │ Refuse this grant │ │ Revoke (grantee-side)
│ └─────────────────────────────────┘ │
│ │
└───────────────────────────────────────┘
Component breakdown — detail view¶
| Slot | Content | Source / notes |
|---|---|---|
| Header back arrow | "← Shared by {granter}" | Returns to inbox list. |
| Header rule chip | "Rule #1" | Reminds the user of the redaction context. |
| H1 | "{client_first}'s {racket_short_name}" | Concise: "Lukas's Pure Aero 98". First name only; never the full name. |
| Subhead | "Strung {date} · Ordered {date}" | strung_at + ordered_at. Strung is required (Rule #1 is past-only). Ordered suppressed if NULL. |
| Racket card | Make + model + version + (optional) instance ID | Rule #1 visible per matrix. |
| Main string card | string identification + tension + (optional) color + (optional) BYO badge | Rule #1 visible. |
| Cross string card | string identification (or "Same as main" short form) + tension + color + BYO | Rule #1 visible. |
| Method + DT card | "Method · Dynamic tension after" | Both Rule #1 visible. Suppressed entirely if both NULL. |
| Redaction notice | Always-rendered explanatory panel | Lists what's hidden from the grantee. Mandatory — the grantee should never wonder "is there more I'm not seeing?". slate-50 background, border-slate-200, rounded, space-4 padding, text-small. |
| Grant metadata | "Granted {date} by {granter}." | From order_shares.created_at + granter.display_name. |
| Refuse-this-grant button | Revoke the inbound grant | Sets order_shares.revoked_at = now(). ADR-0005 hooks fire share_revoked_from_me to the granter. |
Refuse-this-grant flow¶
The grantee revoking (refusing) a grant is functionally the same DB write as the granter revoking, but the user-facing copy differs:
- CTA label: "Refuse this grant" (EN) — softer than "Revoke", since the receiving stringer didn't ask for it.
- Confirmation: toast "Refused — Stefan's share of #2026-0042 is no longer in your inbox. [Undo]" with the same 5-second undo window.
- On confirm (no Undo): the row leaves the inbox; the share_audit log records
event_kind=grant_revokedwithactor_kind=stringerandactor_id=:me(the grantee). - The granter is notified via
share_revoked_from_me— they see "Sarah refused your grant on #2026-0042" in their notification inbox + see the revoked row in their "Grants I've issued" surface.
What the detail view does NOT show¶
Per § Visibility / redaction matrix, Rule #1 grantees do not see:
- Client last name, email, phone, address, any other PII.
- Comments (free-text on the order).
- Pricing fields:
labor,main_price,cross_price,strings_subtotal,total.
These fields are NOT fetched server-side for Rule #1 grantees (per ADR-0004 § Visibility / redaction (serialization layer)). The Pydantic schema OrderRule1View has no slot for them. Pax's chokepoint enforces this; the template here only renders what the schema provides — defense in depth.
Notification badge integration¶
Per stringer-dashboard § Inbox section: the dashboard's "Shared with me" link shows a count chip "N new" only when there are unread share_granted_to_me notifications.
Counting¶
The unread count is the count of unread share_granted_to_me notifications, not the count of total active grants. So a grantee who already opened a job once doesn't keep seeing the badge increment.
Server-side query (per ADR-0005 § Unread-badge query):
SELECT count(*) AS unread
FROM notifications
WHERE recipient_stringer_id = :me
AND kind = 'share_granted_to_me'
AND read_at IS NULL;
Cap at 999+ per ADR-0005 — at V2 volume, the count is rarely > 5.
Marking read¶
Tapping a card in the inbox opens the detail view AND POSTs /notifications/{id}/read for the corresponding share_granted_to_me notification (per ADR-0005 § Mark-as-read API). The dashboard badge decrements on next page load.
A "Mark all as read" affordance lives in the inbox header (a small text-small link top-right) when the unread count is > 0, mapping to POST /notifications/read-all filtered to kind = 'share_granted_to_me'. Useful when an inactive granter has shared 10 jobs at once and the receiving stringer wants a cleaner badge.
Interaction states¶
| State | What renders |
|---|---|
| Loading (initial) | Server-rendered HTML; no skeleton needed. |
| Loading (after revoke) | The affected row swaps with a 200 ms cross-fade and slides up motion-default. |
| Searching (typing) | The list filters live (HTMX). Non-matches hide; group headers hide if empty. |
| Empty (no grants ever) | "No one's shared anything with you yet." + illustration. |
| Empty (filters) | "No matches for these filters." + Clear filters link. |
| Notification arrives mid-session (HTMX OOB swap) | A new card animates in at the top of its source group; toast "Stefan shared a new job with you." |
| Grant revoked by granter mid-session | The affected row fades out (motion-out); toast "Stefan revoked their share of #2026-0042." |
| Refuse pending | "Refusing…" with spinner. Disabled while in flight. |
| Refuse failure (5xx) | Toast "Couldn't refuse right now — try again." Row stays. |
| Detail view — no grant exists (URL stale) | "This grant is no longer active. [Back to inbox]" |
Validation rules (UI surface; canonical server-side)¶
| Rule | Inline message |
|---|---|
| Trying to view a revoked grant | "This grant is no longer active. [Back to inbox]" |
| Trying to refuse an already-refused grant (race) | Toast "This grant was already revoked." |
| Trying to access a different stringer's inbox via URL manipulation | 403 — the chokepoint refuses; the UI redirects to the receiving stringer's own inbox or 404s. |
Accessibility¶
- Keyboard navigation: Tab cycles through search → filter chips → first card → next card → ... → next group's first card → ... . Each card is a single focusable region with
aria-label="Shared by {granter} — {client_first}'s {racket}, strung {date}, {tensions}, {string}"so a screen-reader user gets the full row content without tabbing into sub-elements. - Unread state: the unread dot is decorative (color-only); the row's accessible name includes "Unread —" prefix when applicable.
- Heading hierarchy: H1 "Shared with me" → group H2 "from {granter}" → card content (no internal H3 — the card is announced via aria-label).
- Filter chips are native
<select>for keyboard accessibility — desktop screen readers announce them as combo-boxes. - Detail view: H1 carries first-name + racket; the redaction notice is in a
<aside>witharia-label="Redaction notice"so screen-reader users land on it explicitly. - Refuse button: confirms via toast + undo (same pattern as share-management's revoke). The toast has
role="status"andaria-live="polite". - Color-only signals avoided: the unread dot pairs with the row's
aria-label"Unread —" prefix; thebg-indigo-50background is supplemented by a left-border4 px indigo-700so high-contrast / forced-colors users still see the unread state. - Hit targets: all card rows ≥ 44 × 44 px; Refuse button 48 px tall.
- Focus rings: 2 px
indigo-700outline + 2 px white inset on every focusable element. prefers-reduced-motion: disables card-arrival / fade-out animations; replaces with instant swap.
HTMX / progressive-enhancement seams¶
- Initial render: server-side HTML at
GET /sharing/inbox. No JS required. - Search:
hx-get="/sharing/inbox/_filter?q=..." hx-trigger="keyup changed delay:200ms" hx-target="#inbox-list". - Filter chips: native
<select>withhx-trigger="change"driving the same swap. - Card → detail view (lg):
hx-get="/sharing/inbox/{order_share_id}" hx-target="#detail-pane" hx-push-url="true". Onsmthe link is a regular full-page nav. - Refuse:
hx-post="/sharing/inbox/{order_share_id}/refuse" hx-target="closest .grant-row" hx-swap="outerHTML". Without JS: regular POST that 303-redirects back. - Mark all as read:
hx-post="/notifications/read-all?kind=share_granted_to_me" hx-target="#unread-chip" hx-swap="outerHTML". - OOB notifications (V2.x polish): when JS is on AND the user is on this page AND a SSE/websocket carries a new
share_granted_to_meevent, the server pushes anhx-swap-oobinsertion of a new card. Round-2 scope: stub the OOB seam; full SSE delivery is V2.x. For Round 2, manual page refresh shows new shares.
i18n affordance¶
| String | Type | Catalogue key |
|---|---|---|
| "Shared with me" (page title, dashboard link) | {% trans %} |
inbox.title |
| "{N} new" | {% trans %} (Babel) |
inbox.unread_count |
| "Search jobs" (placeholder) | {% trans %} |
inbox.search_placeholder |
| "All" / "Unread only" / "Read only" (filter chip) | {% trans %} |
inbox.filter.read.{all,unread,read} |
| "All sources" / "Source" (filter chip + per-stringer dynamic) | {% trans %} |
inbox.filter.source.{all,label} |
| "Recent first" / "Oldest first" / "By client (A→Z)" | {% trans %} |
inbox.filter.sort.{recent,oldest,by_client} |
| "from {granter} Rule #1" (group header) | {% trans %} (Babel) |
inbox.group.from_granter, inbox.group.rule_label.rule1 |
| "Open →" | {% trans %} |
inbox.card.open |
| "No one's shared anything with you yet." | {% trans %} |
inbox.empty.fresh |
| "When another stringer or a client shares a job with you, it'll show up here." | {% trans %} |
inbox.empty.fresh.hint |
| "No matches for these filters." | {% trans %} |
inbox.empty.filtered |
| "Clear filters" | {% trans %} |
inbox.empty.filtered.clear |
| "Nothing currently shared with you. Past grants are in the audit log." | {% trans %} |
inbox.empty.all_revoked |
| "Mark all as read" | {% trans %} |
inbox.mark_all_read |
| Detail "Shared by {granter}" | {% trans %} (Babel) |
inbox.detail.shared_by |
| Detail "{client_first}'s {racket}" | {% trans %} (Babel) |
inbox.detail.h1 |
| Detail "Strung {date} · Ordered {date}" | {% trans %} (Babel, conditional) |
inbox.detail.dates |
| Detail section headers ("Racket", "Main string", "Cross string", "Method · Dynamic tension after") | {% trans %} |
inbox.detail.section.{racket,main,cross,method_dt} |
| Detail "Same as main" short form | {% trans %} |
inbox.detail.cross.same_as_main |
| Detail "BYO" badge | {% trans %} |
inbox.detail.byo |
| Detail color label | {% trans %} |
inbox.detail.color_label |
| Redaction notice body | {% trans %} |
inbox.detail.redaction.{intro,visible,hidden.{name,comments,pricing}} |
| "Granted {date} by {granter}." | {% trans %} (Babel) |
inbox.detail.granted_by |
| "Refuse this grant" (CTA) | {% trans %} |
inbox.detail.refuse |
| "Refused — {granter}'s share of #{number} is no longer in your inbox. [Undo]" (toast) | {% trans %} (Babel) |
inbox.detail.refused.toast |
| "This grant is no longer active. [Back to inbox]" | {% trans %} |
inbox.detail.stale |
| Client first name (data) | Data | n/a |
| Granter display name (data) | Data | n/a |
| Racket make/model/version (data) | Data | n/a |
| String mfr/model/gauge (data) | Data | n/a |
| Date "2026-04-30" | Format ISO YYYY-MM-DD |
n/a |
| Tension "24 kg" | Format Babel format_unit('kg', locale) |
n/a |
DE pass is Iris's later work.
DE width budget (designer note)¶
DE labels here run 20–30% longer than EN:
- "Shared with me" → "Mit mir geteilt" (similar).
- "Search jobs" → "Aufträge suchen" (similar).
- "Recent first" → "Neueste zuerst" (similar).
- "By client (A→Z)" → "Nach Kunde (A→Z)" (similar).
- "Refuse this grant" → "Diese Freigabe ablehnen" (~1.4×) — fits within full-width minus 16 px on
sm. - "Mark all as read" → "Alle als gelesen markieren" (~1.7×) — wraps to two lines on
sm; designer note: render at the top-right of the inbox header where wrap is acceptable, not inline with the page H1. - "When another stringer or a client shares a job with you, it'll show up here." → "Wenn ein anderer Bespanner oder ein Kunde einen Auftrag mit Ihnen teilt, erscheint er hier." (similar) — wraps cleanly.
- "Hidden from you: Lukas's full name, contact / Comments on this job / Pricing" — DE renders longer ("Vor Ihnen verborgen: Lukas's vollständiger Name, Kontaktdaten / Anmerkungen zu diesem Auftrag / Preise"); the bulleted layout absorbs the extra length.
Data fetched (informative — Pax's lane)¶
The chokepoint per ADR-0004 § Authorization predicate admits a row for the receiving stringer either because:
- They are the order's
stringer_id(their own jobs — not what this surface shows), or - They have an active
order_sharesrow withgrantee_stringer_id = :me, or - They have an active
person_stringer_sharerow to the order's Person (Rule #3).
The inbox query for the list (informative, not normative — Pax owns the SQL):
SELECT os.id AS order_share_id,
os.created_at AS granted_at,
o.id AS order_id,
o.strung_at, o.ordered_at,
o.main_tension, o.cross_tension,
o.main_byo, o.cross_byo, o.main_color, o.cross_color,
r.make, r.model, r.version, r.serial_or_instance_id,
ms.manufacturer AS main_mfr, ms.model AS main_model, ms.gauge AS main_gauge,
cs.manufacturer AS cross_mfr, cs.model AS cross_model, cs.gauge AS cross_gauge,
p.display_first_name,
gs.display_name AS granter_display_name,
n.read_at AS notification_read_at
FROM order_shares os
JOIN orders o ON o.id = os.order_id
JOIN client_profiles cp ON cp.id = o.client_profile_id
JOIN persons p ON p.id = cp.person_id
JOIN rackets r ON r.id = o.racket_id
LEFT JOIN strings ms ON ms.id = o.main_string_id
LEFT JOIN strings cs ON cs.id = o.cross_string_id
JOIN stringers gs ON gs.id = os.granter_stringer_id
LEFT JOIN notifications n
ON n.subject_kind = 'order_share'
AND n.subject_id = os.id
AND n.kind = 'share_granted_to_me'
AND n.recipient_stringer_id = :me
WHERE os.grantee_stringer_id = :me
AND os.revoked_at IS NULL
ORDER BY os.created_at DESC;
Columns intentionally NOT selected: o.comments, o.labor, o.main_price, o.cross_price, o.strings_subtotal, o.total, p.display_last_name, p.email, p.phone. The Rule-#1 redaction is enforced by the SELECT shape, not by the template.
This shape feeds the OrderRule1View Pydantic schema (per ADR-0004 § Implementation shape).
Open questions for Stefan (with proposed defaults)¶
-
Default sort: "Recent first" vs "By client". Proposed default: Recent first. New shares are the most actionable — Stefan's vacation-handover use case is "Sarah just shared 10 jobs with me; what's the latest setup?" Alternative: by-client (better for "I'm about to string for Lukas — what did Stefan do last time?"). Mid-V2 we'll see which Stefan reaches for; flagged for Round-3 polish if both are wanted as a stringer-level preference.
-
Notification dot vs row background highlight for unread. Currently both. Proposed default: keep both. The dot is the precise signal; the background tint draws the eye to the group. Alternative: dot only (cleaner). A11y: the background tint helps users with weaker color discrimination. Both is moderately heavy visually; if Stefan dislikes it, drop the background, keep the dot.
-
Should the inbox surface group V3 client-granted shares (Rules #2/#3) differently from Rule #1? Proposed default: yes — group by source AND rule label. A Rule-#3 grant ("client shared everything past + future with me") is qualitatively different from a Rule-#1 grant ("Stefan handed me 2 specific jobs"); a chip "Rule #1" / "Rule #2" / "Rule #3" on each group header keeps the redaction context honest. Already drawn that way in the wireframes. V3-deferred for client-granted shares; the V2 surface only ever shows Rule #1.
-
"Mark all as read" affordance — top-right link or inline in the count chip? Proposed default: top-right link, only when unread count > 0. Doesn't crowd the chip; reachable in one tap. Alternative: a long-press on the count chip (Android-pattern; inconsistent with iOS).
-
Detail-view back-button behaviour. Currently returns to the inbox list (preserves filter + scroll position). Proposed default: keep. Alternative: hard-redirect to dashboard (loses context). Standard browser-back behaviour wins.
-
"Refuse this grant" copy vs "Revoke this grant". Proposed default: "Refuse" — the receiving stringer didn't ask for the grant; "Refuse" reads more like a passive-recipient action. The granter-side surface uses "Revoke" (active grantor). Alternative: unified "Revoke" everywhere (less nuanced; clearer to translate). Stefan to confirm.
-
"Granted {date} by {granter}." line — is the date enough, or also include "Last viewed by you {date}"? Proposed default: just the granted date in V2. Last-viewed-by-you self-tracking is noisy; the granter-side surface already shows "Last viewed YYYY-MM-DD HH:MM". Alternative: surface both for symmetry.
-
Desktop master-detail (lg) — keep or simplify to single-pane? Proposed default: keep master-detail on
lg. Stefan-on-desktop scenarios are real (he reviews his colleague's shares from a laptop); two-pane is meaningfully faster than full-page navigation. Alternative: single-pane everywhere (simpler). Master-detail's complexity is small (one HTMX swap); keep. -
Auto-link to existing ClientProfile on receive (V3 polish, currently V2-deferred per OQ-5). Reaffirm V2 standalone-only? Proposed default: yes — keep V2 standalone. The OQ-5 closure is recent and the rationale (unambiguous redaction boundary) holds. The V3 collapse is a separate design exercise once the surface is mature.
Cross-references¶
- Source requirements: client-identity-and-sharing § hidden requirements item 1; § User stories — Stringer (Grantee).
- Architecture: ADR-0004 § Visibility / redaction (the redaction matrix that drives the detail view); ADR-0005 § notification_kind enum (the unread-badge plumbing).
- Linked design surfaces: stringer-dashboard § Inbox (entry point), share-management (the granter-side companion), receipt-pdf § Redaction modes (why Rule #1 grantees get an HTML detail view, not a PDF).
- i18n strategy: i18n architecture.
- Tokens: design-tokens.
- Issue tracking: racket-book#102.