Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions docs/architecture-audit-2026-07-23/TeamInbox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Architecture Audit — Team Inbox

**Scope:** Team Inbox TypeScript domain/UI/data source, managed-cloud mention RPC client, project-management SQLite projection, Tauri commands, Sidebar and Chat Panel tab integration.
**Date:** 2026-07-23

## Layer 1 — Compilation correctness

- TypeScript `tsc --noEmit`: passed.
- Tauri application `cargo check -p org2`: passed.
- Focused Rust Team Inbox tests: 12 passed.

## Layer 2 — Dead code and structural deduplication

- Production entry path is Sidebar row → singleton Team Inbox tab → connected view → shared cache/data source → local Tauri projection plus managed-cloud mention RPC.
- Sidebar badge and rendered page consume the same cache; no second unread query implementation remains.
- Local assignment reads remain in SQLite; the frontend does not rescan every project Work Item.
- Mention response mapping is centralized in the Team Inbox data source; sorting/filtering/deduplication remain pure domain selectors.

## Layer 3 — Naming consistency

- Wire `work_item_assigned` is mapped once to UI `assigned_work_item`; names are explicit at the boundary.
- `viewerMemberIds` is used consistently for the local viewer identity. The cloud RPC deliberately accepts no viewer ID because JWT identity is authoritative.
- Sidebar/menu/tab terms consistently use `team-inbox` / `Team Inbox`.

## Layer 4 — Semantic overloading

| Term | Meaning in this change | Verdict |
| ------------ | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| viewer | Explicit local project member IDs, or managed-cloud JWT subject | Kept separate at transport boundaries; never inferred from an agent/session ID. |
| read receipt | SQLite viewer-scoped receipt for local assignment; endpoint+user+org scoped persisted receipt for cloud mention | Separate storage owners with one UI read state. |
| projectId | Project slug for project-store navigation; empty for standalone Work Items | Boundary is explicit and standalone navigation uses the standalone API. |

## Layer 5 — Default branch analysis

- Item-kind branching uses discriminated unions with explicit mention/assignment cases; unsupported wire combinations throw.
- Local mentions filter returns an explicit empty page rather than falling through to assignments.
- Cloud RPC failure degrades to local items only; it does not fabricate mention data or scan comment bodies.

## Layer 6 — Cross-domain concept leakage

- Project-management owns only local assigned Work Item projection and receipt DDL.
- Managed-cloud mention transport remains under `features/Org2Cloud`.
- Presentation consumes a transport-independent Team Inbox domain contract.

## Layer 7 — New developer confusion test

- `ConnectedTeamInboxView` identifies the production-wired surface; `TeamInboxView` remains injectable for tests/reuse.
- `useTeamInboxDataSource` names local/cloud composition and identity resolution explicitly.
- `useTeamInboxNavigation` separates Session comment navigation from project/standalone Work Item navigation.

## Layer 8 — Wire protocol and serialization

- Local DTOs use serde-tagged target/payload variants and camelCase fields, covered by Rust serialization tests.
- Cloud request body contains only `p_org_id`, `p_cursor`, and `p_limit`; tests assert no caller-supplied viewer/user ID.
- Cloud response is Zod-validated; malformed counts and pagination input are rejected.

## Layer 9 — Init parity

| Entry point | Canonical schema init | Explicit viewer | Blocking DB isolation |
| ------------- | --------------------: | --------------: | --------------------: |
| list page | yes | yes | `spawn_blocking` |
| unread count | yes | yes | `spawn_blocking` |
| mark read | yes | yes | `spawn_blocking` |
| mark all read | yes | yes | `spawn_blocking` |
| mark unread | yes | yes | `spawn_blocking` |

All five commands (`team_inbox_list_page`, `team_inbox_unread_count`, `team_inbox_mark_read`, `team_inbox_mark_all_read`, `team_inbox_mark_unread`) are registered in the same Tauri handler list.

## Layer 10 — Resolver symmetry

- Local viewer identity uses the same current-user member resolver for list, single read, and bulk read.
- Cloud cache and persisted receipt keys use the same endpoint + authenticated user + org scope.
- Project and standalone navigation both resolve raw Work Item data through the same adapter chain before opening the canonical Chat Panel Work Item tab.

## Completion verdict

- Canonical DDL changed directly; no `ALTER TABLE` compatibility path was introduced.
- Local cursor ordering and viewer-scoped receipt idempotence are tested.
- Cloud receipt storage is bounded to 1,000 entries.
- No timer or polling loop was introduced; refresh is driven by initial demand, existing project-change signals, cloud comment signals, and mutations.

**Architecture verdict: pass for the audited Team Inbox scope.**
50 changes: 50 additions & 0 deletions docs/frontend-ui-audit-2026-07-23/TeamInbox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Frontend UI Audit — Team Inbox

**Files:** `src/modules/MainApp/TeamInbox/**/*.tsx`
**Date:** 2026-07-23
**Auditor:** ORGII implementation session

## D1 — Raw HTML vs Design System

| Line | Element | Verdict | Reason | Suggested change |
|---|---|---|---|---|
| `TeamInboxRow.tsx:42` | raw `<button>` listbox option | keep with reason | The row implements a multi-line `role="option"` with roving tab index and a forwarded focus ref. `Button` does not cover this listbox-row contract; visual state is sourced from `getListItemClasses`. | — |
| `TeamInboxList.tsx` | filter controls | fixed | The previous implementation manually mapped three DS Buttons into a segmented filter. | Replaced with shared `TabPill` inside `ListPanelTabPillRow`. |
| `TeamInboxList.tsx` | list header and refresh action | fixed | The previous implementation rebuilt the panel header and used a custom labelled refresh button. | Replaced with `PanelHeader`, `PANEL_HEADER_TOKENS.actionButton`, and `PanelRefreshButton`. |
| `AssignedWorkItemDetail.tsx`, `CommentMentionDetail.tsx` | duplicated detail shell | fixed | Both files rebuilt header, scroll area, width container, metadata, and bottom navigation independently. | Both now compose `TeamInboxDetailLayout`, which uses `DetailPanelContainer`, `PanelHeader`, `DETAIL_PANEL_TOKENS`, `InfoCard`, and `PanelFooter`. |

## D2 — Arbitrary Tailwind Value vs Token

| Line | Value | Verdict | Reason | Suggested change |
|---|---|---|---|---|
| All audited files | CSS-variable / raw-color arbitrary values | keep | No arbitrary CSS-variable, hex, rgb, or hsl Tailwind values remain. | — |

## D3 — Hardcoded Sizes / Colors

| Line | Value | Verdict | Reason | Suggested change |
|---|---|---|---|---|
| `TeamInboxView.tsx` | split widths `320/240/420` | fixed | The Team Inbox had invented wider master/detail geometry instead of matching the existing Inbox surface. | Replaced with the established Inbox `200/160` geometry and `SplitViewLayout` defaults. |
| detail components | `p-5`, `max-w-3xl`, `gap-4`, bordered card shell | fixed | These duplicated spacing, width, and card decisions already encoded by detail-panel tokens. | Replaced with `DETAIL_PANEL_TOKENS`, `CARD_ROW_TOKENS`, and `InfoCard`. |
| compact icons | `size={14}` | keep with reason | 14px is the established compact list/action optical size and is also used by the shared Inbox/ListPanel patterns. Header action icons use `PANEL_HEADER_TOKENS`. | — |
| Team Inbox colors | semantic color classes | keep with reason | Remaining colors are semantic project tokens (`primary`, `success`, `text`, `border`) used to distinguish mention and assignment item kinds. No raw color values exist. | — |

## D4 — Accessibility

| Line | Element | Verdict | Reason | Suggested change |
|---|---|---|---|---|
| `TeamInboxList.tsx` | filter tabs | fixed | Shared `TabPill` now owns segmented-control semantics and interaction instead of a local button group. | — |
| `TeamInboxList.tsx` | listbox | keep with reason | The list has an accessible name, active descendant, and Arrow/Home/End keyboard navigation. | — |
| `TeamInboxRow.tsx` | option row | keep with reason | Each row has `role="option"`, `aria-selected`, an explicit read-state accessible name, and roving tab index. | — |
| header actions | icon-only controls | fixed | Refresh and mark-all now use shared header action components/tokens with explicit titles and accessible labels. | — |

## D5 — Visual Patterns Observed

- The duplicated Work Item / mention detail shell occurred twice, below the global three-site abstraction threshold, but was abstracted locally because both implementations were in the same feature and already shared an identical contract.
- The segmented filter, refresh action, panel header, detail scroll shell, metadata card, and footer are existing cross-repo patterns; Team Inbox now consumes those primitives rather than creating new variants.
- No new global design-system abstraction is required.

## Summary

- 7 fixes completed
- 4 kept with documented reason
- 0 remaining abstract candidates
2 changes: 2 additions & 0 deletions src-tauri/crates/project-management/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! This crate contains project-management functionality:
//! - `projects`: Pure-SQLite project & work item store at
//! `~/.orgii/projects/projects.db`. Single source of truth.
//! - `team_inbox`: Viewer-scoped projection of assigned Work Items.
//! - `orchestrator`: Workflow orchestration state machine.
//! - `lineage`: Code lineage tracking and analysis.
//! - `sync`: Pluggable sync framework — outbox + adapters draining through
Expand All @@ -12,6 +13,7 @@ pub mod lineage;
pub mod orchestrator;
pub mod projects;
pub mod sync;
pub mod team_inbox;

#[cfg(test)]
mod test_support;
1 change: 1 addition & 0 deletions src-tauri/crates/project-management/src/projects/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use rusqlite::{Connection, Result as SqliteResult};
/// connection pool. Safe to invoke against an existing DB.
pub fn init_project_tables(conn: &Connection) -> SqliteResult<()> {
init_local_tables(conn)?;
crate::team_inbox::schema::init_team_inbox_tables(conn)?;
init_outbox_table(conn)?;
init_webhook_secrets_table(conn)?;
init_import_progress_table(conn)?;
Expand Down
65 changes: 65 additions & 0 deletions src-tauri/crates/project-management/src/team_inbox/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use super::{
list_page, mark_all_read, mark_read, mark_unread, unread_count, TeamInboxCursor,
TeamInboxFilter, TeamInboxListOptions, TeamInboxPage,
};

#[tauri::command]
pub async fn team_inbox_list_page(
viewer_member_ids: Vec<String>,
filter: Option<TeamInboxFilter>,
cursor: Option<TeamInboxCursor>,
limit: Option<usize>,
) -> Result<TeamInboxPage, String> {
tokio::task::spawn_blocking(move || {
list_page(TeamInboxListOptions {
viewer_member_ids,
filter: filter.unwrap_or_default(),
cursor,
limit: limit.unwrap_or(50),
})
})
.await
.map_err(|error| format!("Task join error: {error}"))?
}

#[tauri::command]
pub async fn team_inbox_unread_count(
viewer_member_ids: Vec<String>,
filter: Option<TeamInboxFilter>,
) -> Result<u64, String> {
tokio::task::spawn_blocking(move || unread_count(viewer_member_ids, filter.unwrap_or_default()))
.await
.map_err(|error| format!("Task join error: {error}"))?
}

#[tauri::command]
pub async fn team_inbox_mark_read(
viewer_member_ids: Vec<String>,
item_id: String,
) -> Result<bool, String> {
tokio::task::spawn_blocking(move || mark_read(viewer_member_ids, &item_id))
.await
.map_err(|error| format!("Task join error: {error}"))?
}

#[tauri::command]
pub async fn team_inbox_mark_all_read(
viewer_member_ids: Vec<String>,
filter: Option<TeamInboxFilter>,
) -> Result<u64, String> {
tokio::task::spawn_blocking(move || {
mark_all_read(viewer_member_ids, filter.unwrap_or_default())
})
.await
.map_err(|error| format!("Task join error: {error}"))?
}

#[tauri::command]
pub async fn team_inbox_mark_unread(
viewer_member_ids: Vec<String>,
item_id: String,
) -> Result<bool, String> {
tokio::task::spawn_blocking(move || mark_unread(viewer_member_ids, &item_id))
.await
.map_err(|error| format!("Task join error: {error}"))?
}
18 changes: 18 additions & 0 deletions src-tauri/crates/project-management/src/team_inbox/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//! Team Inbox read model for the global project store.
//!
//! The local project database currently contributes assigned Work Items. The
//! wire contract also reserves the comment-mention variant so cloud/session
//! comment sources can be merged by a higher layer without changing the DTO.

pub mod commands;
pub mod schema;
mod store;
mod types;

pub use store::{
list_page, mark_all_read, mark_read, mark_unread, unread_count, TeamInboxListOptions,
};
pub use types::*;

#[cfg(test)]
mod tests;
23 changes: 23 additions & 0 deletions src-tauri/crates/project-management/src/team_inbox/schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use rusqlite::{Connection, Result as SqliteResult};

/// Canonical read receipts for Team Inbox projections.
///
/// A receipt belongs to an explicit viewer identity and a stable source item.
/// Source rows remain authoritative; deleting a Work Item cascades neither
/// business state nor unrelated viewers' receipts.
pub fn init_team_inbox_tables(conn: &Connection) -> SqliteResult<()> {
conn.execute_batch(
r#"
CREATE TABLE IF NOT EXISTS team_inbox_read_receipts (
viewer_member_id TEXT NOT NULL,
source_kind TEXT NOT NULL,
source_id TEXT NOT NULL,
read_at INTEGER NOT NULL,
PRIMARY KEY (viewer_member_id, source_kind, source_id)
);
CREATE INDEX IF NOT EXISTS idx_team_inbox_receipts_source
ON team_inbox_read_receipts(source_kind, source_id);
"#,
)?;
Ok(())
}
Loading
Loading