Derived from the canonical F103 CUE contract: **partial**. See the generated traceability view and gap view; Markdown does not carry an editable completion claim.

Spec: F103 — Team & Org Pulse Reporting

Status: Derived from the canonical F103 CUE contract: partial. See the generated traceability view and gap view; Markdown does not carry an editable completion claim.

One line. A team leader opens one screen and sees every one of their people’s latest report — what they did, what’s blocked, the photos from the field — and the CEO sees the whole company the same way, grouped by team, newest first. No spreadsheet, no chasing, no “did everyone submit?” It is the read side of the daily pulse (F07 / F83): the pulse is how the update goes in; this is how a boss reads it out.


1. Problem & CEO Job

Job: see clearly (and, downstream, trust the signal). Today a member’s daily pulse (F07) flows into the CEO’s org-wide morning Brief, and a leader can see a flat “who submitted / who missed” list (pulse_adherence, backend/apps/pulses/reporting_views.py). At inception there was no surface where a manager read their own team’s actual reports, and no surface where the CEO read everyone’s latest report grouped by the team they belong to. A team leader in Jakarta would still ask for a WhatsApp summary; a CEO in Riyadh still scrolls chat threads to learn that the pump job slipped. The adherence view answers “did they report?” — this feature answers the question a boss actually has: “what did my people report, and what needs me?” Without it, the pulse is a black box the leadership can’t open without the CEO Brief’s org-wide LLM narration — too coarse for a manager, too slow for “how’s the sales team right now?”, and impossible on a phone in the field.

This is the wedge deepened: once a manager reads their team here every morning, the whole team’s adoption of the pulse becomes self-enforcing (a leader who reads reports is a leader who asks the two people who didn’t submit).

2. User Stories and Product Intent

Roles follow OrganizationMember.ROLE_CHOICES (owner/admin/manager/member/viewer). The canonical, falsifiable requirement set—including role/platform applicability, implementation links, and proof policies—lives in F103.cue. This section deliberately keeps only the product intent; it is not a second acceptance or completion checklist.

  • A manager reads the freshest report for every active person in the team(s) they lead, including an honest missing-report state rather than invented activity. They cannot reach a different team’s data.
  • An owner or admin can scan the organization’s reports by team, including people without a team and a specific-date view when needed.
  • Members and viewers may read only their own reports plus reports from same-team peers who are not the team’s manager and do not hold an owner, admin, or manager role. They never receive a leader’s report or another team’s data; this is an API boundary, not merely a hidden navigation item.
  • A leader can open a report for detail and attachments, then decide whether a follow-up is warranted.
  • The assistant read path must remain evidence-grounded before it can be called verified; a raw data response alone is not an evidence-cited answer.

3. Scope Boundaries

  • In:
    • A read-only role-scoped rollup of daily pulses (F07/F83), grouped by Team: owner/admin → all active members; manager → managed teams plus self; member/viewer → self plus same-team non-leadership peers, excluding Team.manager.
    • “Latest per member” (default) and “specific date” modes.
    • A member-report detail view (read) that surfaces the pulse body + its attachments (F104).
    • The F91 assistant path (ask the assistant for the same rollup in natural language), which remains partial until an evidence-grounded answer proof exists.
    • Mobile-first web. Native reporting source paths are in scope on Android, iOS, and macOS; rendered device evidence remains a separate proof gate and must not be inferred from navigation alone.
  • Out:
    • Writing/editing a member’s pulse (that is F07/F83 submit — a member owns their own pulse).
    • Cross-org / staff HQ reporting (that is the HQ surfaces, F43).
    • The CEO morning Brief itself — the org-wide ranked intelligence stays F12; this rollup is the raw per-person report layer beneath it, not a second Brief.
    • New analytics/charts (trend lines, streaks, scores). Out for v1 — a rollup is a reading surface, not a dashboard; scale-check first (see §12). Adherence counts reuse pulse_adherence.
    • The multi-file attachment capability itself — defined in its own spec, F104; F103 only renders the attachments F104 captures.

4. Data Model

No new persisted model. This is a query/aggregation feature over existing tables — the same posture as F12 Brief selection (a query layer over the org graph, no new “report” entity). Adding a Report table would duplicate PulseEntry and rot.

Tables read (all already UUIDModel + organization FK):

  • PulseEntry (backend/apps/pulses/models.py:7) — the report itself: organization, user (→OrganizationMember), team (→teams.Team, nullable), date, answers_json, status (draft/submitted/processed/failed), source (form/chat/telegram), plus F83 profile_snapshot/follow_up_qa/resolved_locale. unique_together(organization,user,date) — exactly one pulse per member per day, ordering=["-date"].
  • Team (backend/apps/teams/models.py:6) — grouping; manager→OrganizationMember (related_name="managed_teams") is the leadership edge.
  • OrganizationMember (backend/apps/organizations/models.py) — the roster; .team FK (related_name="members", line 84) is the authoritative team attribution (same field the pulse submit uses), role, status (active only).
  • EvidenceSource (backend/apps/pulses/models.py:60) via F104’s new PulseEntry.attachments link — read to render photo/file thumbnails.

“Latest report per member” query (the core):

# leader = request.membership; org = request.organization_id
members = (OrganizationMember.objects
           .filter(organization_id=org, status="active")
           .select_related("user", "team"))
if leader.role == "manager":                       # manager → only teams they lead
    team_ids = list(Team.objects.filter(organization_id=org, manager=leader)
                                .values_list("id", flat=True))
    members = members.filter(team_id__in=team_ids)  # + optionally include leader's own membership
# owner/admin → all active members (no extra filter)
latest = (PulseEntry.objects
          .filter(organization_id=org, user__in=members)          # date param optional
          .order_by("user_id", "-date")
          .distinct("user_id")                                    # Postgres DISTINCT ON
          .select_related("user__user", "team")
          .prefetch_related("attachments"))

No status transitions added (read-only). No backend/DIAGRAMS.md state machine change; do add a GLOSSARY entry “Pulse Rollup” and a request-lifecycle note that the rollup is DISTINCT-ON, not N+1.

Scale-check (per ENGINEERING.md → Scale-Ready Design): DISTINCT ON (user_id) over PulseEntry needs an index on (organization_id, user_id, date DESC) — add the migration in the same PR. Bounded by active member count per org (hundreds, not millions); no keyset pagination needed at team grain, but the owner org-view paginates by team (one team per page-chunk) so a 5,000-person org doesn’t return 5,000 cards at once — keyset by team, never OFFSET.

5. API Contract

MethodPathAuth/RoleRequestResponseErrors
GET/api/pulses/rollup/IsOrgMember; role/team scope is enforced server-side?date=YYYY-MM-DD (opt, default latest-per-member) · ?team_id= (leaders only) · ?cursor= (owner/admin team keyset){ scope, teams, next_cursor, can_invite_members }401 no membership · 403 target/team outside visible scope · 400 bad date
GET/api/pulses/rollup/<member_id>/IsOrgMember + role/team visibility predicate?date= (opt)full PulseEntrySerializer incl. answers, follow_up_qa, attachments[], source403 target not visible · 404 member/pulse absent
GET/api/pulses/rollup/<member_id>/history/IsOrgMember + same role/team visibility predicate?before=YYYY-MM-DD (exclusive cursor) · ?limit=1..50{ member, results: PulseEntrySerializer[], next_before }, newest first403 target not visible · 404 member absent · 400 malformed cursor/limit

can_invite_members is a server-derived Assistant CTA policy: it is true only for active owner and manager memberships, and false for admin, member, viewer, and custom roles. It is never inferred from a client-side role-name guess. This flag controls visibility of the Assistant-row entry point; the existing organization-management invitation endpoint remains its own authorization boundary.

  • Reuse, don’t fork. The existing pulses router exposes rollup beside pulse_adherence in backend/apps/pulses/reporting_views.py, reusing its LEADER_ROLES = {"owner","admin","manager"} gate and IsOrgMember-set request.membership/request.organization_id. Adherence stays the counts endpoint; rollup is the content endpoint.
  • Tenant scoping: every query filters organization_id=request.organization_id (there is no TenantQuerySetMixin in this codebase — the explicit .filter is the pattern, backend/apps/organizations/permissions.py:7). Cross-team scoping is an additional filter, never a replacement for the org filter. Mandatory cross-tenant + cross-team tests in §7.
  • AI / EvidenceSource: the rollup returns raw pulse content — factual, no LLM, no confidence score. When the assistant path (§6) summarizes a team’s reports, that reply is F91’s agent loop over this same query and must cite the pulses it read as EvidenceSources (evidence-on-every- AI-output; honest confidence only if the model infers, never for verbatim data). The rollup endpoint itself never fabricates — a missing pulse is pulse: null, never a generated placeholder.

6. UI / UX

  • Assistant home Pulse people row (native): directly beneath the Assistant title, show accessible member avatars flattened across the role-scoped rollup and sorted by most recent Pulse submission. The compact state shows at most five people; View all expands in place to a wrapped grid and cursor-loads every remaining owner/admin team page before enabling the control again, so “all” never silently means the first page. If fewer than three accessible people exist and can_invite_members is true for an Owner or Manager, Invite teammates routes to the existing organization-member invitation screen.

  • Avatar action: opens that person’s Pulse history as a full reading screen. The header identifies the person; Pulse cards form a newest-first vertical thread with date/source metadata, attachments, an inline Comment action, lazy-loaded universal comments, and a composer. Only one Pulse discussion expands at a time, avoiding an N+1 comment fetch across the history.

  • Comments and notifications: Pulse is an F81 target. Creating a comment validates access to the Pulse, writes through the universal comment API, and notifies the Pulse author except on a self-comment. Existing mention/thread-follow dedupe remains intact. The notification deep-link resolves through typed Android/iOS route allowlists; unknown routes remain a no-op.

  • Route(s) / component(s): /work/reports (leader-gated) → TeamReportsPage.tsx (frontend/src/pages/work/); detail /work/reports/:memberId → MemberReportPage.tsx. Nav item visible only to leaders (hide for member/viewer, same idiom as other role-gated nav). Sits under Work (it’s operational reading), cross-linked from the CEO Brief’s Pulse Summary.

  • States to design: empty (no team / no members / no reports yet — honest, e.g. “No one on your team has reported yet today”), loading (skeleton cards), error (retry), populated, no-permission (a member who deep-links /work/reports is redirected/blocked with an honest message, never a blank).

  • Layout — mobile-first (this is the hard requirement):

    • Phone: a single vertical scroll. Team = a sticky section header (name · manager · “3/5 reported” count). Member = a tappable card: avatar, name, status dot, the pulse’s 1-line summary, a row of attachment thumbnails (previewable images do not repeat their filename inline; the accessible filename is retained), and a humanized timestamp (“2h ago” / “No report today”). Tap → full detail.
    • Owner view adds team grouping + a team keyset “load more teams” — never render 5,000 cards.
    • Desktop: same data, two columns (team rail + member list) — a progressive enhancement of the phone layout, not a separate design. Reuse the shared date-severity taxonomy (lib/format.ts) for “overdue to report” honesty.
  • Populated refresh recovery: Android/iOS keep the last committed team groups, member rows, report summaries, and available actions visible behind one compact one-line warning when refresh fails. Retry repeats the failed rollup request; repeated failure retains the same snapshot and a successful response removes the warning. Only an initial failure with no committed rollup is blocking.

  • shadcn components: Card, Avatar, Badge (status/role — semantic Lumi tokens only), Tabs (latest | by-date), Skeleton, EmptyState, the shared attachment-thumbnail primitive from F104, useConfirm only if a “nudge to report” action is added (deferred). Check frontend/src/components/ui/ first — no raw palette (Rule 4).

  • i18n: new namespace reports (en + vi together, then propagate to all 61 locales): reports.title, reports.subtitle_manager, reports.subtitle_ceo, reports.team_count_reported ({{n}}/{{total}} reported), reports.no_report_today, reports.last_reported, reports.unassigned, reports.empty_no_members, reports.empty_no_reports, reports.error_load, reports.no_permission, reports.open_report, reports.member_of, reports.attachments_count.

  • DESIGN.md / TASTE.md: calm reading surface — first report in the first viewport (first-viewport law), status via semantic tokens, honest empty states, no dense number-wall. TASTE pre-flight applies (designed hover/focus/active; keyboard: cards are focusable, Enter opens detail, Esc returns).

  • Assistant path (F91 doctrine): first-class, not an afterthought. A leader can ask the assistant “How’s the sales team doing today?” / “Show me who hasn’t reported” / “What did Minh report yesterday?” — the F91 agent loop runs the same /api/pulses/rollup/ query behind a read tool (read-team-reports), returns a grounded, evidence-cited answer in-thread, and offers the proactive next action (“2 people haven’t reported — send a nudge?”). The page is the optional direct path; the conversation is the primary one. Registering the read tool + eval case is part of the build (F91 Bet 2 — a capability is a tool + prompt + eval, not a hardcoded screen). No new required form is introduced (reading is not intake), so form-parity is satisfied by construction.

7. Test Plan

The canonical proof map is F103.cue, not this prose. The Contract Graph collects runner-native proof IDs and writes the current resolvable view to traceability.md. Historical test paths in this spec were stale (tests/pulses/ instead of tests/pulse/) and must not be reused as proof.

Risk intent retained for product review:

  • API behavior must protect tenant and team boundaries, including allowed and denied leadership roles.
  • The web surface must distinguish loading, empty, success, failure, localization, and accessibility.
  • The assistant’s raw rollup tool is not enough: a grounded, evidence-cited answer needs its own proof.
  • The real leader journey needs browser E2E evidence, and Android/iOS/macOS each need platform-native proof.
  • Populated native refresh failure must retain the committed rollup through repeated failure and prove that explicit Retry forwards recovery without losing team, member, or report identity.

The unresolved proof obligations are current blocking gaps; see the generated gap view rather than a manually maintained checkbox. The repository-wide backend coverage gate is 97%, as enforced in CI; no F103 prose target may contradict it.

8. Current Behavior

Before F103 there was no read-the-reports surface. The shipped web and native paths now sit on the following contract; the native path has managed Android emulator proof but still awaits named-device and iOS runtime proof:

  • A member submits a pulse (POST /api/pulses/submit/); it is stored per (org, member, date) and attributed to their team.
  • A leader can call GET /api/pulses/adherence/?date= (views.py:390, LEADER_ROLES gate) and get a flat, org-wide {members:[{id,name,role,submitted,submitted_at}]} — counts only, no content, no team grouping.
  • The CEO Brief (F12) narrates an org-wide “Pulse Summary” (who submitted/missed) but is org-scoped and LLM-narrated — not a per-person, per-team reading surface, and not filterable by a manager to their team.
  • Web provides the complete rollup and member-detail reading surface. Android, iOS, and macOS now provide the same leader-gated rollup/detail source path through shared KMP; source-level attachment thumbnails and full-image preview are implemented. Guarded Android flow 161 proves leader-route reachability, populated team/member/Pulse rendering, repeated refresh failure with retained context, exact Retry recovery, APK identity, fixture custody/cleanup, and runtime shutdown on the approved emulator. iOS recovery, named-device, accessibility-service, store, deployed, and production evidence remain open.

The Contract Graph has collected the API, web-unit, and assistant-tool proofs that exist today. It records the native source path separately from platform-native runtime evidence, and it keeps F103 partial until browser E2E, grounded assistant answers, and native device evidence are proven.

pulse_adherence remains a distinct counts surface in backend/apps/pulses/reporting_views.py; F103 adds content rollup and role-scoped reading above it.

9. Error States

  • Member/viewer: the API returns only self plus eligible same-team peers. A direct target outside that set returns 403 without revealing Pulse existence. The existing web reports navigation may remain leader-only; native Assistant home is the employee peer-reading entry point.
  • Manager requests a team they don’t lead (?team_id= or detail for an out-of-team member): 403; UI never shows a cross-team card in the first place, so this is a defense-in-depth API guard, logged at warning (lumi.pulses.rollup — potential probing) without leaking whether the team exists.
  • No members / no reports: not an error — honest empty state (reports.empty_no_members / reports.empty_no_reports), never a spinner-forever or a fabricated card.
  • Initial load failure: reports.error_load + Retry replaces the empty surface. Logged at error with org id (no PII beyond ids).
  • Populated refresh failure: the last committed rollup stays visible behind one compact one-line reports.error_load warning with explicit Retry. Repeated failure keeps the snapshot; successful Retry clears the warning. The native client does not create a retry storm.
  • Attachment thumb fails to load: graceful fallback to a file-type chip; never blocks the card.

10. Diagrams & Docs Touched

  • backend/DIAGRAMS.md — request lifecycle: add the rollup DISTINCT-ON query note (no state machine change). ER: none (no new table); note the read edges PulseEntry→Team→OrganizationMember.
  • frontend/DIAGRAMS.md — route tree: add /work/reports, /work/reports/:memberId (leader-gated).
  • GLOSSARY.md — add “Pulse Rollup” and “Team lead (Team.manager)”.
  • docs/MVP_SCOPE.md — new F103 matrix row (this PR).
  • docs/tests/web/web-screen-audit.md — new registry rows for the two report surfaces + audit-tour stop.
  • ADRs: none required (reuses F07 pulses, F03 teams, F12 read pattern, F91 tool doctrine). If the owner org-view ever needs precomputed rollups for scale, that materialization is a future ADR — do not build it now (scale-check: absent load).

11. Implementation Audit (updated 2026-08-10)

Backend, web, and native source paths exist; the feature is partial by contract evidence. Browser E2E, grounded assistant-answer proof, iOS runtime, named-device, accessibility-service, store, deployed, and production evidence remain open. The Android emulator now proves populated rollup reachability and explicit stale-refresh recovery, but does not close those broader evidence classes. The two rollup endpoints are present in the committed OpenAPI schema and owned by the F103 contract; the canonical F103 contract owns the current proof map. Codebase constraints the design must respect:

  • Dual team membership (F03 §4 documented debt): OrganizationMember.team (authoritative, used by pulse attribution) vs the TeamMember M2M (display roster), kept in sync by nothing. F103 must group by the same edge the pulse uses (OrganizationMember.team / PulseEntry.team) so a report never lands in a different team than its pulse. Resolving the dual representation is out of scope but F103’s correctness depends on picking the authoritative one.
  • Team.manager is nullable (SET_NULL): a team with no manager has no manager-viewer — those members are still visible to owner/admin. Handle manager=null teams honestly (the org view shows them; no manager can claim them).
  • No TenantQuerySetMixin: use the explicit .filter(organization_id=...) idiom; do not import an aspirational mixin (the F83 spec references one that doesn’t exist).
  • Postgres DISTINCT ON is available (prod is Postgres); tests run on both Postgres and SQLite — the “latest per member” query must have a SQLite-safe path (subquery on max(date) per user) or the test settings must use Postgres for this module. Decide in §12.

12. Open Questions / Decisions

  1. Latest-per-member query portability. DISTINCT ON is Postgres-only; the test suite includes SQLite. Decision needed: (a) Postgres-only test module (mark it), or (b) a Subquery(max(date)) form that runs on both. Lean (b) — portable, and the index makes it fast. No ADR (implementation detail).
  2. Does a manager see only teams where Team.manager == them, or also their own primary team if they’re a member elsewhere? Proposed: union of (teams they manage) ∪ (their own membership, so a leader always sees their own report). Confirm with the owner.
  3. Owner org-view default: latest-per-member vs today-only? Proposed default = latest (a boss wants the freshest signal, and “who’s gone quiet” is exactly the missing-state honesty). ?date= opts into a specific day. Confirm.
  4. “Nudge to report” action (a leader taps → the person gets a proactive F91 mission to submit). High value, but it’s a write + a proactive send (F91 restraint ledger). Deferred to a fast- follow so v1 stays a pure reading surface; noted here so the layout leaves room.
  5. Analytics (streaks, adherence trend, team scores). Explicitly out for v1 (§3). A future materialized rollup would need an ADR — build only when real load demands it, not speculatively.

13. Bug / Change Log

2026-08-29 — Team overview image cards remove duplicate filenames (lumi-fb #132)

Type: Native shared-KMP UI refinement (F103/F104 attachment presentation)

Summary:

  • Previewable image attachments in Team overview and member report detail keep the thumbnail as the visual proof without repeating the filename in visible body text.
  • Accessibility semantics and the full-image preview title retain the filename; non-image and unavailable-image rows keep their existing filename fallback.

Verification: cd android && ./gradlew :shared:jvmTest --tests com.locuno.lumi.shared.ui.AttachmentPresentationTest -PlumiFilteredJvmTest --no-daemon; git diff --check

2026-08-10 — Native populated rollup refresh recovery

  • Shared Android/iOS Pulse Reports now derives visible content from current-or-stale rollup state, so repeated refresh failure cannot discard committed team groups, member rows, report summaries, or available actions.
  • Populated failure renders one compact one-line warning with explicit Retry; initial empty failure remains blocking, and successful retry removes the warning.
  • Focused model/policy tests and guarded Android flow 161 prove repeated failure, exact Retry forwarding, identical APK identity, exact team/member/Pulse custody, cleanup, and shutdown. iOS, named-device, accessibility-service, store, deployed, and production proof remain open.

2026-07-19 — Assistant Pulse people, peer scope, history, and discussion

  • Extended read visibility from leaders to every active role without weakening boundaries: owner/admin see the organization; managers see managed teams plus self; members/viewers see self plus same-team non-leadership peers and never the explicit team manager.
  • Added date-cursor Pulse history and an Assistant-home avatar row ordered by latest submission. Compact mode shows five people; View all follows every owner/admin team cursor; invitation appears only below three accessible people and only with the server-derived member-management capability.
  • Added the shared Android/iOS Pulse timeline with one lazily expanded F81 discussion at a time. Peer comments notify the Pulse author, self-comments do not, and both shells resolve the exact member thread through typed route allowlists.
  • Backend integration, shared JVM, Android route/compile, iOS build/XCTest, and contract gates are recorded in the PR; named-device interaction and live push delivery remain explicit gaps.

2026-07-14 — Native full-image preview

Type: Android + iOS + macOS shared-KMP UI refinement (F103 read side)

Summary:

  • Report rollup and member-detail image thumbnails now open the shared in-app full-image preview when a usable CDN URL exists.
  • The preview uses fit scaling, localized close/open actions, and preserves the URL fallback for download/share.
  • Named-device reachability, rendered evidence, and audit scoring remain PM test-train work.

Verification: cd android && ./gradlew :shared:jvmTest :app:compileProdDebugKotlin :shared:compileKotlinIosSimulatorArm64 --no-daemon; bash scripts/check-platform-parity.sh; git diff --check

2026-07-13 — Native image thumbnail source path

Type: Android + iOS shared-KMP UI implementation (F103 read side)

Summary:

  • Image attachments in the rollup and member-detail rows now request a native thumbnail from the existing public_url; non-images stay compact file rows.
  • Loading and decode failure preserve the filename/type/size and URL-gated open fallback.
  • Named-device reachability and rendered proof remain open; no F103 score is changed from source or compilation alone.

Verification: cd android && ANDROID_HOME=/Users/james/Library/Android/sdk ./gradlew :shared:jvmTest --no-daemon; cd android && ANDROID_HOME=/Users/james/Library/Android/sdk ./gradlew :shared:compileAndroidMain :shared:compileKotlinIosSimulatorArm64 --no-daemon; git diff --check

2026-07-13 — Native attachment open-state refinement

Type: Android + iOS shared-KMP UI refinement (F103 read side)

Summary:

  • Rollup and member-detail attachment rows now show filename, MIME type, and byte size.
  • Open is rendered only for a non-empty public_url; missing URLs use a localized unavailable-preview state so the read surface never promises a dead action.
  • Thumbnail decoding and named-device proof remain open and are tracked by the mobile screen audit; no F103 score is changed from source alone.

Verification: cd android && ANDROID_HOME=/Users/james/Library/Android/sdk ./gradlew :shared:jvmTest --no-daemon; cd android && ANDROID_HOME=/Users/james/Library/Android/sdk ./gradlew :app:compileProdDebugKotlin :shared:compileKotlinIosSimulatorArm64 --no-daemon; git diff --check

2026-07-13 — Native F103 leader rollup and member detail source slice

Type: Android + iOS + macOS shared-KMP implementation (read-only F103 path)

Summary:

  • Added shared PulseRollupResponse DTOs and the tenant-scoped GET /api/pulses/rollup/ and GET /api/pulses/rollup/<member_id>/ client calls, with contract coverage for team grouping, missing reports, summaries, and F104 attachment metadata.
  • Android now exposes a leader-only Reports entry, grouped rollup cards, honest loading/empty/error states, pull-to-refresh, member detail, and attachment open actions. Members/viewers do not see the entry and the backend remains the authorization boundary.
  • iOS and macOS register the same rollup/detail controllers through their workspace routes; iOS member-card selection pushes the shared detail screen.
  • Added shared model tests for success, refresh, and explicit failure states. The native path is intentionally read-only: F103 has no create/edit/delete operation.

Honest gate:

  • No mobile score changed. Thumbnail rendering, named-device screenshots, and the full native F103 audit remain open; source parity and device reachability must be proven separately.

Verification: cd android && ./gradlew :shared:jvmTest :app:compileProdDebugKotlin :shared:compileKotlinIosSimulatorArm64; bash scripts/check-platform-parity.sh; git diff --check.

2026-07-11 — First web audit (igw-workflow train 1): 2 web bugs found + fixed

Type: Bug fix (web frontend)

Reason: First insanely-great-workflow audit of the two new web rows (W-WRK-09/10, docs/tests/web/web-screen-audit.md §11) surfaced two real defects that shipped in the 2026-07-10 implementation, both now fixed with regression tests. Both rows scored avg 8 after the fix (evidence: docs/tests/evidence/web/W-WRK-09/, .../W-WRK-10/).

Summary:

  • WU5/WU7 raw-enum leak (MemberReportPage.tsx): question-card labels called tPulse('im_doing') etc. against the pulse i18n namespace, which has no such keys — i18next’s missing-key fallback returned the raw key string, and a uppercase CSS class rendered it as IM_DOING/IM_STUCK/… on screen. Fixed to use the purpose-built short labels already present in the reports namespace (t('im_doing') → “Done”/“Đã làm”, etc.) — the correct namespace per the existing reports.json key set. Added MemberReportPage.test.tsx.
  • WU3 hardcoded-English relative time (TeamReportsPage.tsx): the local timeAgo() helper returned literal "Xm/h/d ago" strings regardless of account language, so the vi UI leaked English (“1h ago” inside an otherwise-Vietnamese card). Removed the hand-rolled helper; added a shared formatRelativeTime() to frontend/src/lib/format.ts (Intl.RelativeTimeFormat, same locale-resolution pattern as the existing formatDate/formatHumanDate) for reuse by any future relative-time surface. Added TeamReportsPage.test.tsx.
  • Registry correction: the W-WRK-10 row’s original baseline description said ”+ history”; on reading this spec’s §“my own history stays on my personal pulse surface” the detail page is correctly scoped to the latest/dated report only — not a gap, corrected in the registry evidence note.
  • Also fixed, out-of-band but hit while auditing this row: a pathFrom member-id resolver bug in the audit-tour Playwright harness itself (frontend/tests/e2e/audit-tour.spec.ts), and an accidentally-committed node_modules symlink pair in the repo root/frontend/ (PR #1791) that broke local frontend installs — .gitignore hardened (see repo root .gitignore comment, dated 2026-07-11).

2026-07-11 — Added F91 read-team-reports assistant tool + eval + tests (review gap)

Type: Implementation (backend — F91 assistant path)

Reason: The F103 r07 review found the read-team-reports tool missing per spec §6 (F91 Bet 2: capability = tool + prompt + eval). This change fills the gap.

Summary:

  • Created apps/pulses/services.py with get_team_pulse_rollup() — the shared rollup query used by both the API view and the assistant tool handler so the two paths never duplicate logic.
  • Refactored pulse_rollup view to call the shared service instead of inline queries (removed 4 now-unused helper functions from views.py).
  • Registered read-team-reports tool in apps/ai/toolbelt_registry_reports.py following the triage.py pattern; wired into toolbelt.py imports.
  • Tool args: optional team_id (UUID) + date (YYYY-MM-DD). Role gate: non-leaders get an honest “leaders only” ToolResult with empty data, never data exposure.
  • Added 2 eval cases (read-reports-001 vi, read-reports-002 en) in tests/evals/assistant/golden_cases_part_14.py.
  • Added 11 tests in tests/ai/test_team_reports_tool.py: leader gets data (owner/manager, with pulses, missing state, team scope, date scope), member/viewer gets refusal, cross-tenant isolation, toolbelt integration (registered, executes, unknown tool).
  • Cleaned up dead code: removed _get_latest_pulse_for_members, _answers_summary, _serialize_pulse, _serialize_member from views.py (now in services.py).

2026-07-14 — Natural-language team references are safe in the assistant tool

Type: Contract hardening (backend — F91 assistant path)

The read-team-reports tool is conversational, so its team scope must not require the model to know a database UUID. The preferred team argument accepts an exact case-insensitive team name or a canonical team UUID. The legacy team_id alias remains accepted for callers already using the first contract, and it follows the same resolver. An unknown, malformed, or ambiguous reference returns a structured empty result with an error code; it never reaches the UUID-backed rollup query or raises through the toolbelt. Resolution is always organization-scoped. Manager visibility remains enforced by get_team_pulse_rollup after resolution.

Required proofs:

  • exact and case-insensitive team-name resolution;
  • canonical UUID compatibility;
  • unknown, malformed, and ambiguous references return structured errors;
  • a team in another organization is not resolved;
  • manager requests for a team they do not manage remain empty and do not disclose the team.

2026-07-10 — Backend + web frontend implemented

Type: Implementation (backend + web frontend; android/ios deferred per task scope)

Summary:

  • Added pulse_rollup and pulse_member_report API endpoints under /api/pulses/rollup/ with leader-gate (owner/admin/manager), team-scoping for managers, latest-per-member rollup, ?date= mode, and keyset pagination by team for owner org-view.
  • Rollup response groups members by team with “Unassigned” for teamless, ordered by team name. Honest pulse: null for members without reports.
  • Added composite index on (organization, user, -date) for rollup query performance.
  • Added usePulseRollup and useMemberReport TanStack Query hooks, reports query key namespace, and rollup/memberReport API endpoint wrappers.
  • Added TeamReportsPage and MemberReportPage web pages at /work/reports and /work/reports/$memberId with mobile-first sticky team headers, avatar+summary cards, attachment thumbnails, and loading/empty/error states.
  • Added reports nav item under Work in sidebar; role-gated at page level.
  • reports i18n namespace with en + vi + all 60 other locale files.
  • 28 backend tests covering manager/org/tenant/roles/team-scope/missing/date/paging/ query-count/detail/edge cases.
  • Spec §11 and §13 updated; MVP_SCOPE row: backend/web ✅, mobile 🔲.

2026-07-10 — Spec created

Type: Documentation Change

Summary:

  • New spec F103 defining the team-leader and CEO pulse-reporting rollup (read side of F07/F83), grouped by team, role-scoped (manager → own teams; owner/admin → all), with an F91 assistant path and a mobile-first layout. Renders F104 attachments; introduces no new persisted model.

Reason:

  • User request (2026-07-10): team leaders/managers need to view their team members’ reports; the CEO needs to view all employees’ latest reports grouped by team. The existing pulse_adherence view gives counts only; no content-reading surface exists.

Impact:

  • New leader-gated read endpoints under /api/pulses/rollup/; new web routes /work/reports*; a new F91 read tool; a GLOSSARY + registry entry. No change to how members submit pulses.

Tests:

  • To be added per §7 (rollup manager/org/role/tenant/team-scope/missing/paging/query-count; assistant tool + eval; e2e leader flow + mobile).

Notes:

  • Build depends on F104 for the attachment-thumbnail data shape (F103 renders what F104 captures). Group by the authoritative OrganizationMember.team / PulseEntry.team edge, not the TeamMember display roster (F03 dual-membership debt). Keep v1 a reading surface — defer nudges and analytics; do not materialize rollups until load demands it.