# Index Search/Autocomplete Plan

Date: 2026-02-24

## Context

The index app (index.localhost:7800) is a Gradio dashboard showing running services. There is NO search — just visual cards grouped by category. The rivus project has ~300+ discoverable items across 7 content types. Adding search/autocomplete would make all this content discoverable from one place.

## Architecture

Two files + one modification:

1. **`index/catalog.py`** (new, ~250 lines) — Content catalog: scans all content types at startup, builds in-memory search index
2. **`index/app.py`** (modify, ~30 lines added) — Add search box at top, wire events, render results as HTML
3. No changes to `registry.py` — stays focused on service discovery

## Data Model

```python
@dataclass
class CatalogEntry:
    id: str                # "svc:brain", "skill:learn", "report:present/learning/report.html"
    title: str             # display name
    type: str              # "service", "static", "report", "skill", "cli", "doc", "database"
    description: str       # short description
    url: str | None        # clickable URL (https://brain.localhost, etc.)
    icon: str              # emoji
    tags: list[str]        # extra searchable tokens
    group: str             # functional group
    running: bool | None   # only for services
```

## Content Sources (~300+ entries)

| Source | Count | How Indexed |
|--------|-------|-------------|
| Services | ~20 | `discover_services()` + `SERVICE_METADATA` |
| Static dirs | ~50 | `find_share_files()` + `parse_share_config()` |
| HTML reports | ~100-150 | Scan curated dirs, reuse `extract_title()` from `present/build_index.py` |
| Skills | ~47 | Parse `~/.claude/skills/*/SKILL.md` frontmatter |
| CLI tools | ~10 | Hardcoded from CLAUDE.md CLI Tools table |
| CLAUDE.md docs | ~15 | `Glob("**/CLAUDE.md")` |
| Databases | ~6-10 | `.share` configs with `databases:` key |

## Search Algorithm

Weighted substring matching, no external dependencies:

```python
def search(query: str, catalog: list[CatalogEntry]) -> list[CatalogEntry]:
    # Scoring: title-starts-with=100, title-contains=60, desc-contains=30, tag-match=20
    # Multi-word: split on spaces, AND logic, minimum score across terms
    # Returns top 20 by score
```

## Gradio UI

- `gr.Textbox` at top of page with `.input()` event (fires per keystroke)
- Results in `gr.HTML` component below, hidden when empty
- Results grouped by type with colored badges
- Clickable links to *.localhost URLs (auto-rewritten to .jott.ninja by existing domain_rewrite JS)

## CSS

```css
.search-results { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.sr-group-label { grid-column: 1/-1; font-weight: 700; border-left: 3px solid; }
.sr-card { padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border); }
.sr-card:hover { border-color: var(--color-accent); }
```

## Freshness Strategy

- Build catalog at startup
- Rebuild on SIGHUP (existing `install_hup_handler` pattern) or after 5min
- Services get live status merged at search time (already on 5s timer)

## Key Files to Reuse

- `index/registry.py` — `discover_services()`, `SERVICE_METADATA`, `GROUPS`
- `static/share.py` — `find_share_files()`, `parse_share_config()`
- `present/build_index.py` — `extract_title()`, `TitleParser`, `SCAN_DIRS`
- `lib/gradio_utils.py` — `install_hup_handler()` pattern

## Implementation Order

1. Create `index/catalog.py` with `CatalogEntry` + `build_catalog()`
2. Implement scanner functions (`_scan_services`, `_scan_static_dirs`, `_scan_reports`, `_scan_skills`, `_scan_cli_tools`, `_scan_docs`, `_scan_databases`)
3. Implement `search()` + `render_search_results()`
4. Modify `index/app.py`: search box + event wiring + CSS
5. Test with `GRADIO_SERVER_PORT=7800 gradio index/app.py`

## Verification

- Type "brain" → should show brain service + brain CLI + brain-related reports
- Type "learn" → should show learning service + /learn skill + learning reports + principles doc
- Type "supply" → should show supplychain service + supplychain static dirs
- Empty search → results hidden, normal group view shows
- Click linked result → opens correct *.localhost URL
