Skip to content
Merged
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
126 changes: 126 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Contributing

Thanks for considering a patch. This repo is a small Flask app plus a hash-routed SPA and a CLI export script. Keep changes focused and tested.

## Development setup

### Prerequisites

- **Python 3.12** (matches CI)
- **Node 20+** (only if you change `static/js/` or run frontend unit tests)

### Bootstrap (Windows PowerShell)

```powershell
git clone https://github.com/cppalliance/claude-code-chat-browser.git
cd claude-code-chat-browser
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements-dev.txt
```

### Bootstrap (macOS / Linux)

```bash
git clone https://github.com/cppalliance/claude-code-chat-browser.git
cd claude-code-chat-browser
python -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt
```

### Run the dev server

```bash
python app.py --port 5000
# Open http://127.0.0.1:5000
```

Useful flags:

- `--base-dir PATH` — point at a different `projects/` tree (for tests or fixtures)
- `--exclude-rules PATH` — session exclusion rules file
- `--host 0.0.0.0` — listen on all interfaces (use only on trusted networks)

## Running tests

### Python

```bash
pytest -q # full suite + coverage (see pyproject.toml)
pytest tests/test_api_integration.py -v
pytest tests/test_search.py -v
pytest tests/test_api_routes.py -v
pytest tests/test_error_codes.py -v
```

### JavaScript (vitest)

Only needed when editing `static/js/`:

```bash
npm ci
npm test
npm run test:coverage # optional
```

`node_modules/` is gitignored — run `npm ci` after clone.

## Code style and conventions

| Area | Convention |
|------|------------|
| **API errors** | Use `error_response()` from [`api/error_codes.py`](api/error_codes.py). Do not call `jsonify({"error": ...})` without a `code` field. Add new members to `ErrorCode` and a row in `tests/test_error_codes.py`. |
| **Exception leakage** | `5xx` bodies are generic messages only. Log full tracebacks with `current_app.logger.exception(...)`. Never put `str(e)` or class names in HTTP JSON (issue #25). |
| **Path safety** | Use `safe_join()` from `utils/session_path.py` for any path built from URL segments. |
| **Imports** | stdlib → third-party → local, blank line between groups. |
| **Line length** | ~100 characters; no enforced formatter yet. |

## Tests required for common changes

| Change | Add or update |
|--------|----------------|
| New HTTP route | Happy + error path in `tests/test_api_routes.py` or `tests/test_api_integration.py` |
| New `ErrorCode` | Parametrized row in `tests/test_error_codes.py` |
| Search / limit validation | `tests/test_search.py` |
| New `_parse_tool_result` dispatch entry | Fixture + assertion in `tests/test_jsonl_parser.py` |
| CLI behavior | `tests/test_cli_e2e.py` (subprocess) or `tests/test_cli_args.py` (parser only) |
| Frontend shared module | `static/js/shared/*.test.js` (vitest) |
| Error response shape | `tests/test_error_propagation.py` regression |

## Branching and pull requests

- Default branch: **`master`**. Do not push directly to `master`.
- Branch names: `feat/<topic>`, `fix/<topic>`, `test/<topic>`, `chore/<topic>`, `docs/<topic>`.
- One logical change per PR when possible.
- PR checklist:
- [ ] `pytest -q` green locally
- [ ] `npm test` green if JS changed
- [ ] CI jobs green (`pytest`, `integration-tests`, `js-tests`, `prod-install-smoke`)
- [ ] PR description includes a **Test plan** section
- [ ] API changes update [`docs/api-reference.md`](docs/api-reference.md) if behavior or errors change

## Where things live

| Task | Location |
|------|----------|
| Add HTTP route | `api/<area>.py`, register blueprint in [`app.py`](app.py) |
| Add stable error code | [`api/error_codes.py`](api/error_codes.py) |
| Parse JSONL / tool results | [`utils/jsonl_parser.py`](utils/jsonl_parser.py) — see [dispatch table notes](docs/architecture.md#dispatch-table) |
| Project/session discovery | [`utils/session_path.py`](utils/session_path.py) |
| Session statistics | [`utils/session_stats.py`](utils/session_stats.py) |
| Bulk / per-session export | [`api/export_api.py`](api/export_api.py), [`utils/md_exporter.py`](utils/md_exporter.py) |
| Export state on disk | [`utils/export_state_store.py`](utils/export_state_store.py) |
| Exclusion rules | [`utils/exclusion_rules.py`](utils/exclusion_rules.py) |
| CLI export | [`scripts/export.py`](scripts/export.py) |
| SPA shell + routing | [`static/index.html`](static/index.html), [`static/js/app.js`](static/js/app.js) |
| Shared frontend utilities | [`static/js/shared/`](static/js/shared/) |
| API documentation | [`docs/api-reference.md`](docs/api-reference.md) |

## Architecture

See [`docs/architecture.md`](docs/architecture.md) for data flow, export state machine, and component diagram.

## Getting help

Open an issue with a clear repro or propose a draft PR early for CI feedback.
35 changes: 18 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,11 @@ Browse and export Claude Code chat history — Web GUI and CLI.
- **Per-model badges** in session header
- **Bulk export** — download all sessions, incremental updates, or latest-day slice as a zip; if there is nothing to export, the API returns **422** with JSON body `{"error": "Nothing to export", "code": "EXPORT_NOTHING_TO_EXPORT", "since": "<mode>"}` (the `since` field echoes your request: `"all"`, `"last"`, or `"incremental"`) instead of an empty zip

### API error codes
### API

JSON error responses include a machine-readable `"code"` (stable `UPPER_SNAKE_CASE`) and a human-readable `"error"` message. Common codes:
REST endpoints for projects, sessions, search, and export are documented in **[`docs/api-reference.md`](docs/api-reference.md)**.

| Code | Typical HTTP | Meaning |
|------|--------------|---------|
| `SEARCH_INVALID_LIMIT` | 400 | Query param `limit` is not a positive integer |
| `INVALID_PATH` | 400 | Path traversal or unsafe project/session path |
| `SESSION_NOT_FOUND` | 404 | Session file missing or excluded |
| `INVALID_REQUEST_BODY` | 400 | POST body is not a JSON object |
| `INVALID_SINCE_MODE` | 400 | Bulk export `since` is not `all`, `last`, or `incremental` |
| `EXPORT_NOTHING_TO_EXPORT` | 422 | No sessions matched the export scope |
| `PARSE_ERROR` | 500 | Session file could not be parsed |
| `INTERNAL_ERROR` | 500 | Unexpected failure (e.g. stats computation) |
JSON error responses include a machine-readable `"code"` (stable `UPPER_SNAKE_CASE`) and a human-readable `"error"` message. See the [error code catalog](docs/api-reference.md#error-code-catalog) for the full table.

### CLI Export
- Standalone script to export all sessions to Markdown with YAML frontmatter
Expand Down Expand Up @@ -105,14 +96,20 @@ Reads from `~/.claude/projects/` which contains JSONL session files created by C

## Project Structure

```
See **[`docs/architecture.md`](docs/architecture.md)** for layered design, data flow, and the dispatch-table ordering rationale.

```text
claude-code-chat-browser/
├── app.py # Flask entry point (default port 5000)
├── api/
│ ├── error_codes.py # ErrorCode enum + error_response() helper
│ ├── projects.py # Project listing & session counts
│ ├── sessions.py # Session parsing & message delivery
│ ├── search.py # Full-text search across sessions
│ └── export_api.py # Bulk zip and per-session Markdown export
├── docs/
│ ├── api-reference.md # HTTP API reference (routes, errors, examples)
│ └── architecture.md # Component diagram and data flow
├── utils/
│ ├── session_path.py # OS-aware path detection & project naming
│ ├── jsonl_parser.py # JSONL session parser with tool result classification
Expand All @@ -122,24 +119,28 @@ claude-code-chat-browser/
├── static/
│ ├── index.html # SPA entry point (Inter font, minimal markup)
│ ├── css/style.css # Dark/light theme, responsive, animations
│ └── js/app.js # Hash-based routing, rendering, UI components
│ └── js/ # ES modules (app.js, route handlers, shared/)
├── CONTRIBUTING.md # Dev setup, tests, PR conventions
└── tests/
```

## Development

To run the test suite, install the dev requirements (Flask + pytest):
See **[`CONTRIBUTING.md`](CONTRIBUTING.md)** for full setup, conventions, and where to change each layer.

Quick start:

```bash
pip install -r requirements-dev.txt
pytest
npm ci && npm test # only if you changed static/js/
```

`requirements.txt` carries only the runtime dep (Flask); `requirements-dev.txt` pulls it in via `-r` and adds pytest.
`requirements.txt` carries only the runtime dep (Flask); `requirements-dev.txt` pulls it in via `-r` and adds pytest (+ coverage). Frontend tests use vitest (`package.json`).

## Continuous integration

Every push and pull request runs **`pytest`** on **Ubuntu** (Python 3.12) via [`.github/workflows/ci.yml`](.github/workflows/ci.yml). A separate job verifies that `pip install -r requirements.txt` (production-only) is sufficient to import and boot the app.
Every push and pull request runs **`pytest`**, **API integration tests**, and **vitest** on **Ubuntu** (Python 3.12, Node 20) via [`.github/workflows/ci.yml`](.github/workflows/ci.yml). A separate job verifies that `pip install -r requirements.txt` (production-only) is sufficient to import and boot the app.

## Exported Markdown Format

Expand Down
3 changes: 2 additions & 1 deletion api/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from flask import Blueprint, current_app

from api._flask_types import FlaskReturn, json_error, json_response
from api.error_codes import ErrorCode, error_response
from models.project import ProjectSessionRowDict, SessionListItemDict
from models.session import SessionDict
from utils.session_path import get_claude_projects_dir, list_projects, list_sessions, safe_join
Expand Down Expand Up @@ -76,7 +77,7 @@ def get_project_sessions(project_name: str) -> FlaskReturn:
try:
project_dir = safe_join(base, project_name)
except ValueError:
return json_response([]), 400
return error_response(ErrorCode.INVALID_PATH, "Invalid path", 400)
sessions = list_sessions(project_dir)
# Add summary preview for each session
from utils.jsonl_parser import parse_session
Expand Down
7 changes: 7 additions & 0 deletions api/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ def get_session_stats(project_name: str, session_id: str) -> FlaskReturn:

try:
session = parse_session(filepath)
rules = current_app.config.get("EXCLUSION_RULES") or []
if is_session_excluded(rules, session, project_name):
return error_response(
ErrorCode.SESSION_NOT_FOUND,
"Session not found",
404,
)
except _PARSE_ERRORS:
current_app.logger.exception("Failed to parse session %s", session_id)
return error_response(
Expand Down
Loading
Loading