---
phase: 05-data-surface-stats-bar-detail-panel
plan: "01"
type: execute
wave: 1
depends_on: []
files_modified:
  - possession/models.py
  - possession/tui/widgets/statsbar.py
  - possession/tui/widgets/detailpanel.py
autonomous: true
requirements: [STAT-01, PANEL-01]

must_haves:
  truths:
    - "get_stats() returns a dict with item_count, room_count, container_count, total_value from a single SQL query"
    - "StatsBar widget renders 4 labeled columns (Items, Rooms, Containers, Value) in a 2-row label/value layout"
    - "DetailPanel widget renders all 7 item fields (name as bold title, then Room, Container, Category, Description, Acquired, Cost) with muted labels"
    - "DetailPanel.show_item(item_dict) updates all field Static widgets with correct values or fallback (— / $0.00)"
    - "StatsBar.refresh_stats(db_path) queries via get_stats() and updates all 4 column Static widgets"
  artifacts:
    - path: "possession/models.py"
      provides: "get_stats() aggregate query function"
      contains: "def get_stats"
    - path: "possession/tui/widgets/statsbar.py"
      provides: "StatsBar widget with 4 column layout and refresh_stats() method"
      exports: ["StatsBar"]
    - path: "possession/tui/widgets/detailpanel.py"
      provides: "DetailPanel widget with show_item() method"
      exports: ["DetailPanel"]
  key_links:
    - from: "possession/tui/widgets/statsbar.py"
      to: "possession/models.py"
      via: "from possession.models import get_stats"
      pattern: "from possession\\.models import get_stats"
    - from: "possession/tui/widgets/statsbar.py"
      to: "stat-val-items, stat-val-rooms, stat-val-containers, stat-val-value Static widgets"
      via: "self.query_one('#stat-val-*', Static).update(...)"
      pattern: "query_one.*stat-val"
    - from: "possession/tui/widgets/detailpanel.py"
      to: "#panel-title and #field-* Static widgets"
      via: "show_item() calling self.query_one(...).update(...)"
      pattern: "query_one.*panel-title|query_one.*field-"
---

<objective>
Create the backend aggregate query and both new display widgets: StatsBar and DetailPanel. These are standalone files that do not yet touch MainScreen — they establish the building blocks that Plan 02 will wire together.

Purpose: Isolate widget authoring from integration so each can be verified independently and Plan 02 has clean, tested components to compose.
Output: possession/models.py (get_stats added), possession/tui/widgets/statsbar.py (new), possession/tui/widgets/detailpanel.py (new)
</objective>

<execution_context>
@/Users/paritoshpatil/.claude/get-shit-done/workflows/execute-plan.md
@/Users/paritoshpatil/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/05-data-surface-stats-bar-detail-panel/05-CONTEXT.md
@.planning/phases/05-data-surface-stats-bar-detail-panel/05-RESEARCH.md
@possession/models.py
@possession/tui/widgets/quickadd.py
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add get_stats() aggregate query to models.py</name>
  <files>possession/models.py</files>
  <action>
Append a new function `get_stats(db_path: Path) -> dict` to `possession/models.py` after the existing `list_items()` function. Follow the exact per-call connection pattern used throughout this file (open/operate/close in try/finally).

The function uses a single SQL query:
```sql
SELECT
  COUNT(*) AS item_count,
  COUNT(DISTINCT room_id) AS room_count,
  COUNT(DISTINCT container_id) AS container_count,
  COALESCE(SUM(cost), 0.0) AS total_value
FROM items
```

Implementation:
- Set `conn.row_factory = sqlite3.Row` before executing
- Execute the query, fetchone(), return `dict(row)`
- `COALESCE(SUM(cost), 0.0)` returns 0.0 when no items have cost — correct behavior
- `COUNT(DISTINCT room_id)` excludes NULL room_id — also correct (unassigned items don't inflate room count)
- No imports needed beyond what already exists in models.py (sqlite3, Path, get_connection are all present)

Add a docstring: "Return aggregate inventory stats in a single query. Returns dict with keys: item_count, room_count, container_count, total_value."
  </action>
  <verify>
    <automated>cd /Users/paritoshpatil/Personal/claude-projects/possession-terminal-2 && python -c "from possession.models import get_stats; from pathlib import Path; import tempfile, possession.db; p=Path(tempfile.mktemp(suffix='.db')); possession.db.init_db(p); r=get_stats(p); assert set(r.keys())=={'item_count','room_count','container_count','total_value'},'wrong keys'; assert r['item_count']==0,'count should be 0'; assert r['total_value']==0.0,'value should be 0.0'; print('PASS:', r)"</automated>
    <manual>Confirm get_stats returns correct zero-state dict with all four keys on an empty database.</manual>
  </verify>
  <done>get_stats(db_path) importable from possession.models; returns dict with item_count=0, room_count=0, container_count=0, total_value=0.0 on an empty DB.</done>
</task>

<task type="auto">
  <name>Task 2: Create StatsBar widget (statsbar.py)</name>
  <files>possession/tui/widgets/statsbar.py</files>
  <action>
Create `possession/tui/widgets/statsbar.py` as a new file. Implement `StatsBar` as a `Widget` subclass that renders 4 labeled stat columns in a 2-row format.

**Layout (user-locked):** Two rows — label row on top, value row below — as 4 equal-width columns:
```
Items  Rooms  Containers  Value
42     8      12          $1,240.00
```

**Widget structure:**
- `StatsBar(Widget)` with `DEFAULT_CSS` setting `height: 2` (do NOT dock — must appear in normal flow between topbar and main body in MainScreen.compose())
- `compose()` yields a `Horizontal` container; inside it, 4 `Vertical(classes="stats-column")` contexts, each yielding:
  - `Static("Label", classes="stats-label", id="stat-label-{name}")` — label row
  - `Static("0", classes="stats-value", id="stat-val-{name}")` — value row

**IDs:** `stat-val-items`, `stat-val-rooms`, `stat-val-containers`, `stat-val-value`
**Label IDs:** `stat-label-items`, `stat-label-rooms`, `stat-label-containers`, `stat-label-value`

**DEFAULT_CSS:**
```css
StatsBar {
    height: 2;
    background: $surface-darken-1;
}
StatsBar Horizontal {
    height: 2;
}
.stats-column {
    width: 1fr;
    height: 2;
    padding: 0 1;
    content-align: center middle;
}
.stats-label {
    height: 1;
    color: $text-muted;
    text-align: center;
}
.stats-value {
    height: 1;
    text-align: center;
}
```

**`refresh_stats(self, db_path) -> None` method:**
- Import `get_stats` lazily inside the method: `from possession.models import get_stats`
- Call `stats = get_stats(db_path)`
- Update the four value widgets:
  - `self.query_one("#stat-val-items", Static).update(str(stats["item_count"]))`
  - `self.query_one("#stat-val-rooms", Static).update(str(stats["room_count"]))`
  - `self.query_one("#stat-val-containers", Static).update(str(stats["container_count"]))`
  - `self.query_one("#stat-val-value", Static).update(f"${stats['total_value']:.2f}")`

**Imports needed:**
```python
from textual.app import ComposeResult
from textual.containers import Horizontal, Vertical
from textual.widget import Widget
from textual.widgets import Static
```

Do NOT dock StatsBar — this is critical (see research pitfall 2: double-docking with topbar causes overlap). MainScreen will yield it in normal compose flow.
  </action>
  <verify>
    <automated>cd /Users/paritoshpatil/Personal/claude-projects/possession-terminal-2 && python -c "from possession.tui.widgets.statsbar import StatsBar; print('PASS: StatsBar importable')"</automated>
    <manual>Confirm file exists, StatsBar imports cleanly, has refresh_stats method.</manual>
  </verify>
  <done>StatsBar importable from possession.tui.widgets.statsbar; has compose() with 8 Static children (4 labels + 4 values) and refresh_stats(db_path) method that calls get_stats and updates value widgets.</done>
</task>

<task type="auto">
  <name>Task 3: Create DetailPanel widget (detailpanel.py)</name>
  <files>possession/tui/widgets/detailpanel.py</files>
  <action>
Create `possession/tui/widgets/detailpanel.py` as a new file. Implement `DetailPanel` as a `Widget` subclass that displays all fields for one inventory item.

**User-locked layout:**
- Item name as bold panel title at the top
- Below: one line per field — `Room`, `Container`, `Category`, `Description`, `Acquired`, `Cost`
- Format: `[dim]Label:[/dim] value` on same line (Rich markup for muted label)
- Empty optional fields: `[dim]Label:[/dim] —` (dash)
- Cost when not set: `[dim]Cost:[/dim] $0.00`

**Field order in FIELDS class attribute (list of (field_key, label) tuples):**
```python
FIELDS = [
    ("room_name",      "Room"),
    ("container_name", "Container"),
    ("category_name",  "Category"),
    ("description",    "Description"),
    ("purchase_date",  "Acquired"),
    ("cost",           "Cost"),
]
```

**Widget structure:**
- `compose()` yields:
  1. `Static("", id="panel-title")` — updated by show_item() with bold name
  2. `VerticalScroll()` context containing one `Static` per field with `id=f"field-{field_key}"`

**`show_item(self, item: dict) -> None` method:**
- Updates `#panel-title` with `f"[bold]{item.get('name', '')}[/bold]"`
- Iterates over `FIELDS`; for each:
  - If `field_key == "cost"`: `val = item.get("cost")`, display = `f"${val:.2f}"` if val is not None else `"$0.00"`
  - Else: `val = item.get(field_key)`, display = `val` if val else `"—"`
  - Calls `self.query_one(f"#field-{field_key}", Static).update(f"[dim]{label}:[/dim] {display}")`

**DEFAULT_CSS:**
```css
DetailPanel {
    height: 1fr;
    padding: 1;
    border-left: solid $primary-darken-2;
}
#panel-title {
    text-style: bold;
    margin-bottom: 1;
    height: 1;
}
VerticalScroll {
    height: 1fr;
}
```

**Imports needed:**
```python
from textual.app import ComposeResult
from textual.containers import VerticalScroll
from textual.widget import Widget
from textual.widgets import Static
```

Note: `field_key` values like `room_name` contain underscores — these are valid CSS ID characters. Do NOT use dots in IDs.
  </action>
  <verify>
    <automated>cd /Users/paritoshpatil/Personal/claude-projects/possession-terminal-2 && python -c "from possession.tui.widgets.detailpanel import DetailPanel; print('PASS: DetailPanel importable')"</automated>
    <manual>Confirm file exists and DetailPanel imports cleanly with show_item method present.</manual>
  </verify>
  <done>DetailPanel importable from possession.tui.widgets.detailpanel; has compose() yielding panel-title Static and VerticalScroll with 6 field Statics; has show_item(item_dict) method that updates all field content with correct fallbacks.</done>
</task>

</tasks>

<verification>
After all 3 tasks:
1. `python -c "from possession.models import get_stats; print('get_stats OK')"` — no error
2. `python -c "from possession.tui.widgets.statsbar import StatsBar; print('StatsBar OK')"` — no error
3. `python -c "from possession.tui.widgets.detailpanel import DetailPanel; print('DetailPanel OK')"` — no error
4. `python -c "from possession.tui.widgets.statsbar import StatsBar; s = StatsBar(); print(hasattr(s, 'refresh_stats'))"` — prints True
5. `python -c "from possession.tui.widgets.detailpanel import DetailPanel; p = DetailPanel(); print(hasattr(p, 'show_item'))"` — prints True
</verification>

<success_criteria>
- `possession/models.py` contains `get_stats()` returning a 4-key dict from a single SQL query
- `possession/tui/widgets/statsbar.py` exists with StatsBar widget that has 4 labeled columns and refresh_stats()
- `possession/tui/widgets/detailpanel.py` exists with DetailPanel widget that has show_item() populating all 7 fields
- All three files importable without error
- No modifications to main.py (Plan 02 handles that)
</success_criteria>

<output>
After completion, create `.planning/phases/05-data-surface-stats-bar-detail-panel/05-01-SUMMARY.md`
</output>
