Mock of B5 — Teacher sends a photo (embeds B-Consent)

Sources: spec §3.5 B5 and §3.10 B-Consent, prd.md §5 Journey 5.

Invariants enforced in this screen: I-Consent-AtSend (§5.1) — consent evaluated at send, not at capture; I-Consent-Block (§5.1) — a tagged none child blocks send entirely; I-ConsentAudit (§5.1) — consent evaluation is stamped on every photo for audit; teacher-flow-critical-path (§5.2) — median open-to-sent < 30 seconds (AC1); offline-tolerant queue (§3.5 step 4); AC7 — upload-status indicator always visible, distinct visible states for all five entity statuses; AC8 — iOS-Safari-specific "keep tab open" teacher-facing note whenever ≥1 photo is in-flight.

Inference flags:

B5 — Teacher sends a photo

Step 1 — Capture or select

Multi-select supported (§3.5 step 1). Teacher either opens the camera in-app or picks from the device's camera roll.

Take a photo Select from camera roll

Selected:

photo 1
photo 2

Step 2 — Tag children

Roster MUST be filtered to children enrolled in the teacher's assigned classroom(s). A child from another classroom MUST NOT appear here (§3.5 step 2).

Photo 1 — tag

A tagged child from outside the teacher's assigned classroom is unreachable via the UI; if reached, treated as blocking error per §3.10 error case.

Step 3 — Consent check (B-Consent §3.10) — WARNING CASE

Heads up — photo consent

[child C] has "solo only" consent, and this photo has 2 tagged children. You need to choose:

[exact wording — not specified in spec; spec §3.10 step 2 names the three options but not the microcopy]

Teacher MUST take explicit action to proceed. Solo_only single-tag = permitted, no warning (§3.10 step 3).

Step 3b — Consent check — BLOCK CASE

Separate flow: a tagged none child blocks the entire send.

Send blocked

[child D] has "no photos" consent. You need to re-tag or remove [child D] to send.

I-Consent-Block (§5.1): a PhotoTag with consent_evaluation = blocked MUST NOT generate any PhotoDelivery. Zero-tolerance.

[exact copy on which child is named vs. anonymized — spec §3.10 step 1 says "without disclosing other children's consent details unnecessarily" — the mock names the blocking child because the teacher needs to act on them; calibration signal]

Step 4 — Confirm & queue

On confirmation, each photo is written to an offline-tolerant local queue and the send UI returns control to the teacher immediately (§3.5 step 4). Teacher does not wait for upload.

Optional caption / Midday Heartbeat message

[message entry mechanism — Q6 open parameter, §5.4]. Options listed in the spec: teacher_typed, library_picked, ai_generated, ai_assisted, none. Calibration signal: Nelson + Allan decide Q6 before this surface is designed.

Send

Step 5 — Background upload (teacher-facing status per photo, AC7)

Underlying entity status values: queued, uploading, delivered, failed_retrying, failed_permanent (§3.5 step 5).

AC7 (new, tot-8ba spike): the teacher send view MUST display a per-photo upload-status indicator at all times, with distinct visible states for all five entity statuses. Silent background progress is prohibited.

Per-photo upload-status indicator (concrete UI element per AC7)

PhotoStatus badgeState
photo 1
queuedqueued — awaiting network
photo 2
uploading…uploading — in-flight
photo 3
sent ✓delivered — teacher UI MAY label "sent"
photo 4
retrying…failed_retrying — backoff in progress
photo 5
failed — retry?failed_permanent — manual retry offered

[teacher-facing labels — spec notes teacher UI "MAY label delivered as 'sent'" (§3.5 step 5); other labels are inferred from enum values]

AC8 — iOS Safari "keep tab open" banner (conditional)

AC8 (new, tot-8ba spike): on iOS Safari specifically, the send view MUST display a teacher-facing note whenever at least one photo is in queued, uploading, or failed_retrying. Motivation: iOS Safari cannot resume uploads in the background (no Background Sync / Periodic Sync / Background Fetch).

[Visible only on iOS Safari when ≥1 photo is in-flight]

"Keep this tab open until your photos finish uploading." — exact copy is a product parameter per spec AC8; final wording TBD.

inference: the mock cannot actually user-agent-sniff (static HTML, no JS). The banner is rendered visible with the explicit conditional label above. A real implementation would:

Step 6–7 — Fan-out and notify

On server-side persistence, the system creates per-recipient PhotoDelivery records for each guardian linked to each tagged child, subject to the consent evaluation performed at send time (§3.5 step 6, I-Consent-AtSend). Guardians are notified per their preferences (real-time or daily digest). See B6.

AC2 (revised 2026-04-24 per Q2 Amendment): a photo queued offline MUST reach a guardian within 10 minutes of the teacher next actively opening the Littlereach app after connectivity returns — bounded-by-web-platform form, since iOS Safari lacks Background Sync / Periodic Sync / Background Fetch (see spike tot-8ba RESULTS.md, open-questions.md §Q2 Amendment 2026-04-24).

AC3: a photo with a none-consent child tagged MUST NOT reach any guardian under any circumstances.

AC6: a failed_permanent photo is never deleted from the device until the teacher confirms.

Error — no wifi, no cellular

Photo sits in queue with status queued. Teacher sees a clear queued indicator — no silent failure (§3.5 error cases, AC7).

Upload resumes automatically when connectivity returns. On iOS Safari specifically, the upload cannot resume while the tab is backgrounded or closed; it resumes on the next active app open (AC2 revised, AC8 banner applies).

Error — upload fails after max retries

Parameter: 10 retries over 24h (spec). Status moves to failed_permanent. Teacher sees this clearly and is offered a manual retry. Local copy MUST NOT be deleted until a successful delivery is confirmed.

Audit — consent evaluation stamp (I-ConsentAudit, §5.1)

Every PhotoTag gets exactly one immutable consent_evaluation value: permitted | warned_override | blocked, stamped at send time. Queryable for audit throughout the photo's lifetime.

PhotoTagged childConsent evalStamped at
photo 1[child A]permitted[timestamp]
photo 1[child C]warned_override[timestamp]
photo 1[child D]blocked[timestamp]