shipped foundation

Spec: F03 — Teams

Status: shipped foundation · agreed channel expansion · Audited: 2026-08-10

1. Problem & CEO Job

See clearly. A 30-person company is not a flat list — the CEO thinks in units: “sales is fine, ops is on fire.” Teams are the grouping dimension for pulses, tasks, risks, and commitments so the brief and filters can answer “which part of my company?” Teams are labels with an owner, not an org chart — Lumi must never become an HR tool.

2. User Stories & Acceptance Criteria

  • As a CEO, I want to create a team and name a manager, so that signals roll up to someone accountable.
    • Given a team with a manager, when AI extracts a risk from that team’s pulse, then the risk carries the team FK and is filterable by it.
  • As an admin, I want to add/remove team members without ceremony.
    • Given a member added to a team, when they submit a pulse, then the pulse is attributed to that team automatically.
    • Given duplicate team name in the same org, when creating, then validation error (unique per org). Fixed E-r31.
    • Given a member is added to or removed from a team, when the mutation commits, then their access is added to or removed from every linked team channel in the same transaction.
  • As a CEO, I want team names to appear wherever work appears, so that I never have to remember who belongs where.
  • As a team member, I want my team’s conversation ready without joining another thing, so that work and discussion stay together.
    • Given an authorized user creates a team, when the server returns 201, then the creator is a team admin and one private default channel already exists with every current team member joined.
    • Given I can view a team, when I open its mobile detail, then I see its linked channels and can open a channel thread in one tap.
    • Given an authorized user creates another team channel, when the server responds, then all current team members are joined and future roster changes continue to synchronize.
    • Given a linked channel, when any user calls generic join/leave or directly edits channel membership, then the server rejects it; the team roster remains the only membership truth.

3. Scope Boundaries

  • In: team CRUD, manager designation, team membership (admin/member), one required linked channel, additional linked channels, inherited channel membership, standard-team bootstrap keys, and team as a filter dimension across work/pulses/operations.
  • Out: nested teams, channel-specific members or roles, cross-team channels, team-level permission matrices, and cross-team dashboards (the Brief is the rollup).

4. Data Model

  • Team — organization FK, name (unique per org), manager → OrganizationMember, nullable immutable bootstrap_key for F02’s eight standard teams.
  • TeamMember — join table to OrganizationMember, role: admin/member, unique per (team, member).
  • F29 Channel.team links zero or more private channels to one team with on_delete=CASCADE. is_parent_default=True marks the protected first linked channel without overloading F29’s organization-wide is_default flag. ChannelMember.is_active is the materialized access index synchronized from active TeamMember organizations, including while the channel is archived; rows and unread watermarks survive suspension but are not independently editable. One team may have at most 50 linked channels.

⚠️ Dual representation: OrganizationMember.team FK (primary team, used by invites and pulse attribution) and TeamMember M2M. See §11 row 3.

5. API Contract

MethodPathMin roleNotes
CRUD/api/teams/roles.manage (write) / member (read)POST atomically creates creator membership + default linked channel
GET/POST/api/teams/<id>/members/roles.manage for writeList/add team members; add synchronizes linked channels
PATCH/DELETE/api/teams/<id>/members/<id>/roles.manageChange role / remove; remove synchronizes linked channels
GET/POST/api/teams/<id>/channels/team viewer / roles.manage for POSTList linked channels or create an additional inherited private channel

Every queryset explicitly uses request.organization_id; cross-tenant → 404. Foreign-key inputs must belong to that same organization. Roster responses and channel ACLs include active organization members only; suspension retains the underlying TeamMember and inactive ChannelMember rows, reactivation restores their access and unread watermarks, and explicit team removal deletes the relationship.

6. UI / UX

  • /settings/teams (TeamsPage.tsx) — list of teams with manager + member count; expanded detail manages members and lists/creates linked channels. A linked row opens the canonical F29 thread. Team deletion confirmation names the linked-channel count and permanent message-history loss.
  • Android/iOS Workspace → Teams — shared list and detail surfaces. Owners/admins can add an organization member as member or admin, render those roles through localized product copy, and remove through an explicit confirmation; ordinary members receive a read-only roster. The detail roster, not the older list snapshot, owns the count after mutations.
  • After a populated mobile Teams refresh fails, the last committed count and team rows remain visible behind one compact warning with explicit Retry. Repeated failure keeps that snapshot; successful retry clears the warning. An initial empty failure remains blocking.
  • After a populated mobile Team Detail or roster refresh fails, the team facts, roster, and authorized actions remain visible behind one compact warning with explicit Retry. Team and roster recovery remain independent; an initial empty team failure remains blocking.
  • The mobile team detail places Channels directly below the roster. Each row shows channel name and unread count and opens the existing F29 thread. One compact New channel action is shown only when the server returns can_create_channel=true; clients never infer it from role text.
  • States: existing organizations may be empty (“Tạo team đầu tiên” with one-field create); a newly bootstrapped organization starts with eight standard rows but still exposes Create team for custom teams. Loading uses skeletons; member pickers come from org members only.
  • No-training rule: creating a team asks for exactly one thing — a name. Creator membership and the first channel are automatic; manager and additional members come after.
  • i18n: teams.

7. Test Plan

#CaseTypeWhere
1CRUD + unique name per orgbackendtests/test_api.py::TestTeam
2Membership add/remove/role, cross-tenant blockedbackendtests/test_multitenancy.py::TestTeamMembers
3Teams page renders, create flowe2edescribe("Teams")
4Mobile add/admin-role, cancel/confirm remove, re-entry persistence, member denialshared JVM + iOS Maestro policy/lifecycleTeamMemberWriteScreenModelTest, TeamsListSourceTest, TeamDetailScreenLogicTest, flows 108/109
5Team creation is atomic with creator membership and default linked channelbackendbackend/tests/teams/test_team_channels.py
6Add/remove member synchronizes all linked channels; generic join/leave is deniedbackendbackend/tests/teams/test_team_channels.py
7Team detail lists linked channels and server capabilities; foreign-tenant inputs return 404backendbackend/tests/teams/test_team_channels.py
8Mobile team detail loads channels, handles partial failure, and routes to the selected threadshared JVM + MaestroTeamChannelsScreenModelTest, P60 mobile flow
9Web team detail/list handles seeded teams, linked channels, safe deletion consequence, and no optimistic successfrontend + ChromiumP60 W03/W05–W07
10Populated mobile Teams refresh retains count and team rows through repeated failure, then explicit Retry recoversshared JVM + Android Maestro policy/runtimeTeamsScreenModelTest, flow 160, run-android-teams-recovery-proof.sh
11Populated mobile Team Detail and roster retain exact context through independent repeated failures, then explicit Retry recovers each surfaceshared JVM + Android Maestro policy/runtimeTeamDetailScreenModelTest, flow 160, run-android-teams-recovery-proof.sh

8. Current Behavior

Shipped. Teams are tenant-scoped, membership assignment is enforced through the organizations/work graph, and web plus shared native surfaces support ordinary CRUD and roster management. Mobile role choices use the backend’s literal member/admin vocabulary, destructive roster changes are visible and confirmation-gated, and a refreshed detail roster owns its displayed count. Duplicate team name fix (E-r31, 2026-06-13) added serializer-level validation returning 400 instead of a DB IntegrityError 500.

The P60 backend boundary creates each team, creator membership, and protected default linked channel atomically. Team detail lists linked channels and server-derived capabilities; roster add/remove operations synchronize every active or archived linked channel through explicit services. Cross-organization manager and member inputs fail closed, generic channel join/leave cannot mutate inherited membership, and suspension retains inactive membership rows so reactivation restores access and unread watermarks. The 50-channel limit returns a stable 409 conflict. Shared Android/iOS team detail now lists and creates linked channels and routes the exact channel id into the canonical Messages thread; E01–E08 passed on the approved emulator and simulator. Web, macOS, named-device, native screen-reader-service, and store proof remain later P60 slices.

The shared Teams list now treats a refresh failure as degraded state rather than a destructive replacement when a committed directory exists. The page count and row details read from the same current-or-stale snapshot, while a compact warning exposes one explicit Retry. Guarded Android flow 160 retained two server-backed teams through two bounded list failures and recovered on the forwarded retry; iOS and named-device evidence remain open in the Contract Graph.

Shared Team Detail applies the same current-or-stale boundary independently to team facts and its roster. A populated failure keeps the exact team, member row, and authorized actions visible behind one compact Retry warning; successful retry removes only that warning. Guarded Android flow 160 proved exact team-detail and roster outages sequentially on the same server fixture; iOS and named-device evidence remain open in the Contract Graph.

9. Error States

#ErrorResponseNotes
ES1Missing required name400 {"name": ["This field is required."]}Covered by CRUD tests
ES2Duplicate team name in same org400 {"name": ["A team with this name already exists in your organization."]}Fixed E-r31; test_create_team_duplicate_name_rejected
ES3Name > 255 chars400 {"name": ["Ensure this field has no more than 255 characters."]}DRF auto-validator; test_create_team_name_too_long_rejected
ES4Cross-tenant team access404test_team_tenant_isolation
ES5Viewer creates/updates/deletes team403Requires IsOrgOwnerOrAdmin; test_viewer_cannot_add_team_member
ES6Duplicate team member added400 {"organization_member": "This member is already on the team."}Fixed; test_add_duplicate_team_member_rejected
ES7Cross-org manager_id on PATCH200 — silently succeeds; cross-tenant FK acceptedtest_patch_team_cross_org_manager_accepted; no serializer validation
ES8Cross-org organization_member_id on member add200 — silently succeedstest_add_team_member_cross_org_accepted; no serializer validation
ES9Team detail with no members200 {members: [], admin_count: 0}Valid behavior
ES10Frontend load failuretoast.error(t('error_load'))No E2E test
ES11Frontend create failure (duplicate name)toast.error(t('error_create')) + structured 400Fixed E-r31
ES12Deleting team used by active projects/tasks and linked channelsExplicit irreversible confirmation names the linked-channel count; projects/tasks get team_id=NULL, while linked channels/messages cascade so no frozen-ACL orphan remainsAudit log + P60 C09
ES13Default linked channel archive/delete400 default_channel_requiredThe team must retain one ready conversation
ES14Join/leave linked channel directly400 membership_inheritedChange the team roster instead
ES15Channel list fails on mobile while team/roster loadedKeep team and roster visible; inline retry in Channels sectionNever replace the whole detail with an error
ES16Create 51st linked channel409 linked_channel_limitExisting channels and memberships unchanged
ES17Populated mobile Teams refresh failsKeep last committed count and team rows visible; compact Retry above the directoryInitial empty failure remains blocking
ES18Populated mobile Team Detail refresh failsKeep team facts, roster, and authorized actions visible; compact Retry above the detailInitial empty team failure remains blocking
ES19Populated mobile team roster refresh failsKeep team facts and last committed roster visible; compact Retry above the rosterTeam-detail recovery remains independent

10. Diagrams & Docs Touched

backend/DIAGRAMS.md ER diagram.

11. Implementation Audit (2026-06-13)

#ClaimStatusEvidence
1Team CRUD, unique name per org✅ matchTeamSerializer.validate_name(); test_create_team_duplicate_name_rejected
2Membership add/remove/role change; cross-tenant 404✅ matchTeamMemberViewSet; test_team_tenant_isolation
3Dual representation: OrganizationMember.team FK (attribution) vs TeamMember M2M (display) — no code enforces consistency⚠️ spec drift / debttest_team_member_add_does_not_sync_org_member_primary_team; by design: FK is authoritative for attribution, M2M for display — documented in §4
4Last admin removed from team — no guard prevents it⚠️ gaptest_remove_last_admin_team_member_succeeds; a team can be left with zero admins
5Team deletion cascade: Project.team and Task.team SET_NULL✅ matchDjango on_delete=SET_NULL; test_org_member_primary_team_set_null_on_team_delete
6Cross-org manager_id on PATCH is rejected✅ matchP60 same-organization serializer validation; test_patch_team_cross_org_manager_rejected
7Cross-org organization_member on member add returns 404✅ matchP60 tenant-scoped roster service; test_add_team_member_cross_org_rejected
8Duplicate team name → 400 via serializer (not IntegrityError 500)✅ matchFixed E-r31 2026-06-13; test_create_team_duplicate_name_rejected
9Authenticated creator is atomically added as team admin with the default linked channel✅ matchcreate_team_with_channel; test_create_team_atomically_creates_admin_and_default_channel
10Pulse attribution loss on team delete: OrganizationMember.team SET_NULL✅ matchtest_org_member_primary_team_set_null_on_team_delete; expected behavior
11Visual QC: team management uses localized role badges and icon-led CTAs instead of duplicated + labels / raw English role keys✅ match2026-06-18 visual QC: TeamsPage.tsx strips stale + prefixes, adds icon-led team CTAs, and localizes owner/admin/manager/member badges through roles + teams namespaces; frontend/screenshots/13-teams/01-teams-list.png
12Visual QC: member rows and team cards surface structure before controls✅ match2026-06-18 visual QC: /settings/teams now opens with a short page summary + count chips, team cards use calm manager/member metadata pills, and member rows group org role plus team memberships beside the person instead of scattering status/action elements across the row.
13Native roster management uses valid roles, explicit confirmed removal, refreshed count, and a member capability gate✅ matchTeamDetailScreenModel, TeamDetailScreen, LumiDirectoryRow; model/source/logic tests; iOS flows 108/109 and isolated lifecycle runner. Named Android and signed-iPhone proof remain Contract Graph gap F03.GAP002.
14Populated native Teams refresh preserves the last committed directory and exposes compact explicit recovery✅ matchTeamsScreenModel.dataOrStale(), TeamsScreen, TeamsScreenModelTest, policy test, and guarded Android flow 160. iOS and named-device proof remain F03.GAP004.
15Populated native Team Detail preserves team facts and roster through independent refresh failures✅ matchTeamDetailScreenModel.dataOrStale(), TeamDetailScreen, TeamDetailScreenModelTest, policy test, and guarded Android flow 160. iOS and named-device proof remain F03.GAP005.

Summary: 14/15 match; last-admin removal remains an explicit gap.

Edge Cases Catalog (formerly §10, 2026-06-12; E1–E14): Key items integrated above. Remaining items: E1 empty org → empty list (not explicitly tested); E2 dual representation no-sync (row 3); E3 last admin removal (row 4); E4 team deletion CASCADE (row 5); E7 duplicate name (row 8); E8 name max_length (ES3); E9 blank name (ES1); E11 role transitions no validation; E12 pulse attribution (row 10); E13 concurrent creation (fixed — validate_name guard); E14 pagination (DRF default). P60 closed E5, E6, and E10 with tenant-scoped services and atomic creation. Full catalog in git history (2026-06-12 changelog entry).

12. Open Questions

  • None blocking. Resist any request for team hierarchies.

13. Bug / Change Log

2026-08-10 — Preserve populated mobile Team Detail and roster through refresh failure

Type: P1 reliability and recovery fix (shared KMP + managed Android proof)

Summary:

  • Kept the last committed team facts, roster, and authorized actions visible through repeated team-detail or roster refresh failure.
  • Added one compact warning with explicit Retry for each independently recoverable surface; an initial empty team failure retains the existing blocking recovery state.
  • Added shared model proof, source/runtime policy proof, and guarded flow 160 with exact APK, sequential bounded outages, team/member custody, retained frames and trace, fixture cleanup, and runtime shutdown.

Proof boundary:

  • The retained Android result is current-source Pixel 7 API 35 emulator mechanics evidence. It does not prove iOS behavior, named-device fidelity, screen-reader-service behavior, store binaries, deployed releases, or production behavior.

2026-08-10 — Preserve populated mobile Teams directory through refresh failure

Type: P1 reliability and recovery fix (shared KMP + managed Android proof)

Summary:

  • Kept the last committed team count and row details visible when a populated refresh fails, including across repeated failure.
  • Added one compact warning with explicit Retry; successful retry clears the warning, while an initial empty failure retains the existing blocking recovery state.
  • Added shared model proof, source/runtime policy proof, and guarded flow 160 with exact APK, bounded outage, server custody, retained frames and trace, fixture cleanup, and runtime shutdown.

Proof boundary:

  • The retained Android result is current-source Pixel 7 API 35 emulator mechanics evidence. It does not prove iOS behavior, named-device fidelity, screen-reader-service behavior, store binaries, deployed releases, or production behavior.

2026-07-30 — Specify container-owned team conversations

Type: Behavior Change

Summary:

  • Every new team receives a private default channel; additional linked channels are allowed.
  • The team roster is the only membership truth and synchronizes every linked channel transactionally.
  • Team detail lists and opens related channels on Android and iOS.
  • F02’s eight standard teams use stable bootstrap keys without changing user-created teams.

Reason:

  • Requiring users to recreate the same group in Teams and Messages is setup tax and creates stale access when the two rosters drift.

Impact:

  • F03 owns team lifecycle; F29 owns channel custody; ADR-050 defines the boundary.

Tests:

  • Planned in docs/test-plan/project-team-channels.md cases T01–T11, C09/C14–C18, W03–W07, and M11–M20.

2026-07-20 — Mobile team membership role, interaction, and roster-truth repair

Type: P1 bug fix (shared KMP + iOS deterministic proof)

Summary:

  • Replaced the invalid mobile manager TeamMember choice with the backend-supported admin role, reject unknown role keys in the shared screen model, and localize persisted roster badges instead of title-casing raw API keys.
  • Replaced long-press-only removal with a visible, localized action and explicit cancel/confirm dialog; ordinary members see neither add nor remove controls.
  • Published LumiDirectoryRow activation on its merged accessibility node so iOS VoiceOver/XCTest can activate the same full-row callback as a physical pointer.
  • Made the loaded/refreshed detail roster authoritative for the member-count chip, preventing an accepted add/remove from leaving stale summary copy.
  • Added flows 108/109 and run-ios-team-membership-proof.sh: one reused simulator, exact current app, disposable tenant, owner/member lanes, database postconditions, and exact cleanup.

Proof boundary:

  • The iOS 26.4 simulator lifecycle is current-source UI mechanics evidence. Android runtime and a signed named-iPhone execution remain open in F03.GAP002. Maestro screenshots are local ignored artifacts and are not committed to git history.

2026-07-14 — Mobile: restore team manager/member signal in list rows (WRK-10)

Type: UX fix (shared KMP list anatomy)

Summary:

  • Team rows now include the API-provided manager name alongside the description when available, so the list carries the ownership signal promised by F03 instead of discarding it before the detail screen.
  • Member counts now use the existing English/Vietnamese singular/plural resources; the previous hard-coded "members" badge could leak English in a Vietnamese UI.
  • Added stable team_row_<id> semantics tags and TeamsListSourceTest for device-proof targeting and regression protection.

Scope boundary:

  • This improves source-level row anatomy. Populated multi-team named-device evidence and WRK-10 scoring remain PM-owned audit work.

Tests:

  • git diff --check
  • PYENV_VERSION=system python3 scripts/check_docs_truth.py
  • bash scripts/check-platform-parity.sh

2026-07-13 — Contract Graph migration slice

Type: Contract and proof traceability

Summary:

  • Added the canonical F03 CUE contract for tenant-scoped team CRUD and roster management, with stable proof IDs attached to the existing backend tests.
  • Regenerated the Contract Graph reports. F03 is intentionally partial: the audit’s cross-org manager/member foreign-key acceptance and last-admin behavior remain open, as do browser and named Android, iOS, and macOS runtime proofs.

Impact: No product behavior change. The contract makes 12 previously unowned team endpoints traceable and preserves F02 as the tenant-membership dependency.

Tests: The Contract Graph verifier executes the selected F03 pytest proof records; remaining security and runtime gaps are explicit in contracts/features/F03.cue.

2026-06-21 — Edit team member role in-place

Type: Feature (CRUD completion)

Summary: TeamMemberViewSet (ModelViewSet) and teamMembersApi.update existed, but TeamsPage only allowed add/remove — a member’s team role (member/admin) couldn’t be changed without removing and re-adding. Added owner/admin-gated role Select per member row (handleUpdateTeamMemberRole → PATCH), with a read-only badge for non-admins. i18n keys added to en/vi (then propagated). testid team-member-role-{memberId}.

2026-06-15 — Frontend route namespaced

Frontend route namespaced: /teams -> /settings/teams (app-wide IA restructure; API paths unchanged).

2026-06-14 — Spec restructured to TEMPLATE.md canonical order (§1–§13)

Type: Documentation Change

Summary:

  • §3 Competitive Positioning (Appendix) and §4 Success Metrics (Appendix) moved to §14/§15 appendix positions.
  • §5 Scope → §3, §6 Data Model → §4, §7 API → §5, §8 UI → §6, §9 Test Plan → §7, §10 Current Behavior → §8, §11 Error States → §9, §12 Diagrams → §10, §13 Implementation Audit → §11, §14 OQ → §12, §15 BL → §13.
  • Updated internal §-references: §13 row 3 → §11 row 3; EC note “formerly §12” → “formerly §10”; documented in §6 → §4.

Reason:

  • Align with TEMPLATE.md canonical section order per SP fleet restructuring pass.

Impact:

  • Spec authors and future implementation agents.

2026-06-13 — E-r31: add serializer-level duplicate team name validation + test

Type: Bug Fix

Summary:

  • Added TeamSerializer.validate_name() to catch duplicate names within the same org at the serializer level — returns 400 instead of unhandled 500 IntegrityError.
  • Tests: test_create_team_duplicate_name_rejected (same name same org → 400; same name different org → 201).

2026-06-18 — Visual QC: clean up team CTAs and member-role labels

Type: UX Fix

Summary:

  • The visual audit showed /settings/teams leaking two unfinished details: the main action rendered as + + Tạo nhóm, and organization-role badges stayed raw lowercase English on a Vietnamese page.
  • Team cards also looked passive even though the page supports expansion to manage members.
  • The surface now uses icon-led action buttons, strips stale + prefixes from translated labels, localizes org/team role badges, and adds a chevron affordance on team cards so expandability is legible from the screenshot alone.

Verification:

  • frontend/screenshots/13-teams/01-teams-list.png

2026-06-18 — Visual QC: member hierarchy and calmer team metadata

Type: UX Fix

Summary:

  • The screenshot audit showed the Teams page had the right information but weak hierarchy: role badges sat far from the member identity, and delete actions were louder than the team context.
  • Added a short page summary with team/member counts, converted team-card metadata into pill-style labels, and moved organization role + team memberships into the member identity block.
  • The destructive remove controls are now quieter outline buttons, which keeps the roster readable without hiding the action.

Verification:

  • frontend/screenshots/13-teams/01-teams-list.png

2026-06-12 — Add competitive positioning and success metrics to F03 spec

Type: Documentation Change

Summary:

  • Added §3 Competitive Positioning and §4 Success Metrics; renumbered §3–§13 to §5–§15. (Superseded by 2026-06-14 restructuring which moves these to §14–§15 appendix.)

2026-06-12 — Add Edge Cases (E1–E14) and Error States (ES1–ES12) to F03 spec

Type: Documentation Change

14. Competitive Positioning (Appendix)

vs Jira (project-hierarchy → people-first)

Jira organizes by project hierarchy: teams are labels on issues within a project board. A CEO cannot see “is sales healthy?” without navigating into a board and reading ticket statuses. Lumi teams are people-first: the team is the grouping dimension for pulses, tasks, risks, and commitments. The CEO sees the org through who is doing what — not which project board has which tickets.

vs Asana (workspaces → role-aware SME context)

Asana splits organizations into workspaces per team but has no SME role awareness — a “Sales workspace” has no concept of a manager accountable for signals. Lumi teams carry explicit admin/member semantics and map directly to the org structure.

vs Monday.com (boards → CEO-visibility)

Monday.com surfaces team work through boards that require active setup. Team health is invisible unless someone builds a dashboard for it. Lumi surfaces team health automatically: pulse completion rates, risk concentration, commitment tracking — all visible in the Brief and Today without configuring a single board.

Vietnamese SME advantage

Lumi teams are labels with an owner, not an org chart. Creating a team asks for exactly one thing — a name — and is complete in under two minutes. No hierarchy configuration, no permission matrix setup, no onboarding wizard.

15. Success Metrics (Appendix)

MetricTargetWhy it matters
Team creation time<2 min from first visitOne field (name); manager and members added after — zero ceremony matches SME pace
Member invitation acceptance rate>80%Org members are already known; adding them to a team removes the friction of re-inviting
Cross-team visibility usage>60% of CEOs use the team filter within first weekTeam filter must be discoverable and useful without training
Permission change error rate<1% of role-change operationsRole transitions must never accidentally demote the last admin
Pulse-team attribution accuracy100% of pulses attributed to correct teamOrganizationMember.team FK is authoritative; divergence with TeamMember M2M is documented debt
Zero-support team management<1% of support tickets about team CRUD or membershipOperations must be self-explanatory