opencode-dashboard-server 0.1.1__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opencode_dashboard_server-0.3.0/PKG-INFO +84 -0
- opencode_dashboard_server-0.3.0/README.md +70 -0
- {opencode_dashboard_server-0.1.1 → opencode_dashboard_server-0.3.0}/app.py +29 -12
- opencode_dashboard_server-0.3.0/cli.py +126 -0
- {opencode_dashboard_server-0.1.1 → opencode_dashboard_server-0.3.0}/db.py +6 -2
- opencode_dashboard_server-0.3.0/opencode_dashboard_server.egg-info/PKG-INFO +84 -0
- {opencode_dashboard_server-0.1.1 → opencode_dashboard_server-0.3.0}/opencode_dashboard_server.egg-info/SOURCES.txt +2 -0
- opencode_dashboard_server-0.3.0/opencode_dashboard_server.egg-info/entry_points.txt +2 -0
- {opencode_dashboard_server-0.1.1 → opencode_dashboard_server-0.3.0}/opencode_dashboard_server.egg-info/requires.txt +2 -0
- {opencode_dashboard_server-0.1.1 → opencode_dashboard_server-0.3.0}/opencode_dashboard_server.egg-info/top_level.txt +1 -0
- {opencode_dashboard_server-0.1.1 → opencode_dashboard_server-0.3.0}/pyproject.toml +9 -2
- {opencode_dashboard_server-0.1.1 → opencode_dashboard_server-0.3.0}/tests/test_app.py +12 -0
- opencode_dashboard_server-0.1.1/PKG-INFO +0 -54
- opencode_dashboard_server-0.1.1/README.md +0 -44
- opencode_dashboard_server-0.1.1/opencode_dashboard_server.egg-info/PKG-INFO +0 -54
- {opencode_dashboard_server-0.1.1 → opencode_dashboard_server-0.3.0}/aggregate.py +0 -0
- {opencode_dashboard_server-0.1.1 → opencode_dashboard_server-0.3.0}/opencode_dashboard_server.egg-info/dependency_links.txt +0 -0
- {opencode_dashboard_server-0.1.1 → opencode_dashboard_server-0.3.0}/setup.cfg +0 -0
- {opencode_dashboard_server-0.1.1 → opencode_dashboard_server-0.3.0}/tests/test_aggregate.py +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opencode-dashboard-server
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: FastAPI aggregator over opencode's SQLite storage
|
|
5
|
+
Project-URL: Homepage, https://github.com/GCS-ZHN/opencode-dashboard
|
|
6
|
+
Project-URL: Repository, https://github.com/GCS-ZHN/opencode-dashboard
|
|
7
|
+
Project-URL: Issues, https://github.com/GCS-ZHN/opencode-dashboard/issues
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: fastapi<1,>=0.115
|
|
11
|
+
Requires-Dist: uvicorn<1,>=0.30
|
|
12
|
+
Requires-Dist: platformdirs<5,>=4
|
|
13
|
+
Requires-Dist: PyYAML<7,>=6
|
|
14
|
+
|
|
15
|
+
# opencode-dashboard-server
|
|
16
|
+
|
|
17
|
+
FastAPI aggregation backend for the [opencode-dashboard](https://github.com/GCS-ZHN/opencode-dashboard)
|
|
18
|
+
project. Reads opencode's local SQLite storage via the `opencode db` CLI and exposes a JSON + SSE
|
|
19
|
+
API for the dashboard front end.
|
|
20
|
+
|
|
21
|
+
## Install (PyPI)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install opencode-dashboard-server
|
|
25
|
+
# or with uv:
|
|
26
|
+
uv tool install opencode-dashboard-server
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requires:
|
|
30
|
+
|
|
31
|
+
- **Python ≥ 3.10**
|
|
32
|
+
- **`opencode` CLI** on the same host — the server shells out to `opencode db "<SQL>"` and never
|
|
33
|
+
opens the SQLite file directly (it's WAL-mode and actively written while opencode runs).
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
Configure once (interactive; writes `~/.config/opencode-dashboard/server.yaml`), then serve:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
opencode-dashboard-server configure
|
|
41
|
+
opencode-dashboard-server serve
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`configure` walks you through `port` (default `8791`), `host` (default `0.0.0.0`),
|
|
45
|
+
`cors_origins` (comma-separated; empty = built-in loopback whitelist), `poll_seconds`
|
|
46
|
+
(default `5`), and `opencode_bin` (default `opencode`). Empty input keeps the default.
|
|
47
|
+
|
|
48
|
+
`serve` accepts overrides: `--port N`, `--host H`, `--config PATH` (instead of the XDG file).
|
|
49
|
+
Precedence for port/host: CLI flag > env `PORT`/`HOST` > config file > default.
|
|
50
|
+
|
|
51
|
+
Run one aggregator per opencode host:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uvicorn app:app --port 8791
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
or directly with uvicorn (still works — reads env/defaults only). Run on more hosts with
|
|
58
|
+
different ports (`8792`, `8793`, …) and list each in the front end's config (`opencode-dashboard configure`).
|
|
59
|
+
Environment switches (used when no config file value is set):
|
|
60
|
+
|
|
61
|
+
- `DASHBOARD_CORS_ORIGINS` — comma-separated CORS allow-list (defaults to the loopback dev origins `localhost/127.0.0.1:5173` and `:4173`; tighten or widen for your deployment).
|
|
62
|
+
- `DASHBOARD_POLL_SECONDS` — SSE poll interval in seconds (default `5`).
|
|
63
|
+
- `OPENCODE_BIN` — path to the `opencode` binary if it's not on `PATH`.
|
|
64
|
+
|
|
65
|
+
## API
|
|
66
|
+
|
|
67
|
+
- `GET /health` — liveness
|
|
68
|
+
- `GET /overview` — whole-host aggregate (tokens by type + cost, session counts, `updatedAt`)
|
|
69
|
+
- `GET /projects` — per-project rollups
|
|
70
|
+
- `GET /projects/{id}` — one project's sessions (parent/child tree)
|
|
71
|
+
- `GET /sessions/{id}` — per-model breakdown for one session (handles mid-conversation model switches)
|
|
72
|
+
- `GET /stream` — SSE: `updated` events when the DB changes (polled ~5s)
|
|
73
|
+
|
|
74
|
+
JSON is camelCase, timestamps are epoch-ms. Full contract in the repo's `API.md`.
|
|
75
|
+
|
|
76
|
+
## From source (development)
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
git clone https://github.com/GCS-ZHN/opencode-dashboard
|
|
80
|
+
cd opencode-dashboard/server
|
|
81
|
+
uv sync # install deps (fastapi, uvicorn) + dev (pytest, httpx)
|
|
82
|
+
uv run pytest # run tests
|
|
83
|
+
uvx ruff check . # lint
|
|
84
|
+
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# opencode-dashboard-server
|
|
2
|
+
|
|
3
|
+
FastAPI aggregation backend for the [opencode-dashboard](https://github.com/GCS-ZHN/opencode-dashboard)
|
|
4
|
+
project. Reads opencode's local SQLite storage via the `opencode db` CLI and exposes a JSON + SSE
|
|
5
|
+
API for the dashboard front end.
|
|
6
|
+
|
|
7
|
+
## Install (PyPI)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install opencode-dashboard-server
|
|
11
|
+
# or with uv:
|
|
12
|
+
uv tool install opencode-dashboard-server
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires:
|
|
16
|
+
|
|
17
|
+
- **Python ≥ 3.10**
|
|
18
|
+
- **`opencode` CLI** on the same host — the server shells out to `opencode db "<SQL>"` and never
|
|
19
|
+
opens the SQLite file directly (it's WAL-mode and actively written while opencode runs).
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
Configure once (interactive; writes `~/.config/opencode-dashboard/server.yaml`), then serve:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
opencode-dashboard-server configure
|
|
27
|
+
opencode-dashboard-server serve
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`configure` walks you through `port` (default `8791`), `host` (default `0.0.0.0`),
|
|
31
|
+
`cors_origins` (comma-separated; empty = built-in loopback whitelist), `poll_seconds`
|
|
32
|
+
(default `5`), and `opencode_bin` (default `opencode`). Empty input keeps the default.
|
|
33
|
+
|
|
34
|
+
`serve` accepts overrides: `--port N`, `--host H`, `--config PATH` (instead of the XDG file).
|
|
35
|
+
Precedence for port/host: CLI flag > env `PORT`/`HOST` > config file > default.
|
|
36
|
+
|
|
37
|
+
Run one aggregator per opencode host:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uvicorn app:app --port 8791
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
or directly with uvicorn (still works — reads env/defaults only). Run on more hosts with
|
|
44
|
+
different ports (`8792`, `8793`, …) and list each in the front end's config (`opencode-dashboard configure`).
|
|
45
|
+
Environment switches (used when no config file value is set):
|
|
46
|
+
|
|
47
|
+
- `DASHBOARD_CORS_ORIGINS` — comma-separated CORS allow-list (defaults to the loopback dev origins `localhost/127.0.0.1:5173` and `:4173`; tighten or widen for your deployment).
|
|
48
|
+
- `DASHBOARD_POLL_SECONDS` — SSE poll interval in seconds (default `5`).
|
|
49
|
+
- `OPENCODE_BIN` — path to the `opencode` binary if it's not on `PATH`.
|
|
50
|
+
|
|
51
|
+
## API
|
|
52
|
+
|
|
53
|
+
- `GET /health` — liveness
|
|
54
|
+
- `GET /overview` — whole-host aggregate (tokens by type + cost, session counts, `updatedAt`)
|
|
55
|
+
- `GET /projects` — per-project rollups
|
|
56
|
+
- `GET /projects/{id}` — one project's sessions (parent/child tree)
|
|
57
|
+
- `GET /sessions/{id}` — per-model breakdown for one session (handles mid-conversation model switches)
|
|
58
|
+
- `GET /stream` — SSE: `updated` events when the DB changes (polled ~5s)
|
|
59
|
+
|
|
60
|
+
JSON is camelCase, timestamps are epoch-ms. Full contract in the repo's `API.md`.
|
|
61
|
+
|
|
62
|
+
## From source (development)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/GCS-ZHN/opencode-dashboard
|
|
66
|
+
cd opencode-dashboard/server
|
|
67
|
+
uv sync # install deps (fastapi, uvicorn) + dev (pytest, httpx)
|
|
68
|
+
uv run pytest # run tests
|
|
69
|
+
uvx ruff check . # lint
|
|
70
|
+
```
|
|
@@ -6,6 +6,7 @@ Run: uv run uvicorn app:app --reload
|
|
|
6
6
|
import asyncio
|
|
7
7
|
import json
|
|
8
8
|
import logging
|
|
9
|
+
import os
|
|
9
10
|
import socket
|
|
10
11
|
import subprocess
|
|
11
12
|
from functools import lru_cache
|
|
@@ -19,28 +20,44 @@ from db import CliRunner
|
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger("dashboard")
|
|
21
22
|
|
|
23
|
+
# Comma-separated CORS allow-list; defaults to the loopback dev origins. Tight
|
|
24
|
+
# by default so a random webpage can't exfiltrate local project/session data.
|
|
25
|
+
def default_cors_origins() -> list[str]:
|
|
26
|
+
env = os.environ.get("DASHBOARD_CORS_ORIGINS", "").strip()
|
|
27
|
+
if env:
|
|
28
|
+
return [o.strip() for o in env.split(",") if o.strip()]
|
|
29
|
+
return [
|
|
30
|
+
"http://localhost:5173", "http://127.0.0.1:5173",
|
|
31
|
+
"http://localhost:4173", "http://127.0.0.1:4173",
|
|
32
|
+
]
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
|
|
35
|
+
@lru_cache(maxsize=8)
|
|
36
|
+
def opencode_version(executable: str = "opencode") -> str:
|
|
25
37
|
try:
|
|
26
38
|
return subprocess.run(
|
|
27
|
-
[
|
|
39
|
+
[executable, "--version"], capture_output=True, text=True, check=True
|
|
28
40
|
).stdout.strip()
|
|
29
41
|
except (OSError, subprocess.CalledProcessError):
|
|
30
42
|
return "unknown" # e.g. CI without the opencode CLI; don't fail the request
|
|
31
43
|
|
|
32
44
|
|
|
33
|
-
def create_app(runner=None) -> FastAPI:
|
|
34
|
-
|
|
45
|
+
def create_app(runner=None, cors_origins=None, poll_seconds=None, opencode_bin=None) -> FastAPI:
|
|
46
|
+
bin = opencode_bin or os.environ.get("OPENCODE_BIN") or "opencode"
|
|
47
|
+
if runner is None:
|
|
48
|
+
runner = CliRunner(executable=bin)
|
|
49
|
+
origins = cors_origins if cors_origins is not None else default_cors_origins()
|
|
50
|
+
if poll_seconds is None:
|
|
51
|
+
try:
|
|
52
|
+
poll_seconds = float(os.environ.get("DASHBOARD_POLL_SECONDS", "5"))
|
|
53
|
+
except ValueError:
|
|
54
|
+
poll_seconds = 5.0
|
|
35
55
|
app = FastAPI(title="opencode token dashboard")
|
|
36
56
|
# Loopback-only API; restrict origins so a random webpage can't exfiltrate
|
|
37
57
|
# local project/session data from the browser (the client runs from Vite).
|
|
38
58
|
app.add_middleware(
|
|
39
59
|
CORSMiddleware,
|
|
40
|
-
allow_origins=
|
|
41
|
-
"http://localhost:5173", "http://127.0.0.1:5173",
|
|
42
|
-
"http://localhost:4173", "http://127.0.0.1:4173",
|
|
43
|
-
],
|
|
60
|
+
allow_origins=origins,
|
|
44
61
|
allow_methods=["*"],
|
|
45
62
|
allow_headers=["*"],
|
|
46
63
|
)
|
|
@@ -56,14 +73,14 @@ def create_app(runner=None) -> FastAPI:
|
|
|
56
73
|
|
|
57
74
|
@app.get("/health")
|
|
58
75
|
def health():
|
|
59
|
-
return {"status": "ok", "version": opencode_version()}
|
|
76
|
+
return {"status": "ok", "version": opencode_version(bin)}
|
|
60
77
|
|
|
61
78
|
@app.get("/overview")
|
|
62
79
|
def overview():
|
|
63
80
|
def run():
|
|
64
81
|
data = aggregate.overview(runner)
|
|
65
82
|
data["host"] = socket.gethostname()
|
|
66
|
-
data["opencodeVersion"] = opencode_version()
|
|
83
|
+
data["opencodeVersion"] = opencode_version(bin)
|
|
67
84
|
return data
|
|
68
85
|
|
|
69
86
|
return handle(run)
|
|
@@ -116,7 +133,7 @@ def create_app(runner=None) -> FastAPI:
|
|
|
116
133
|
except Exception:
|
|
117
134
|
logger.exception("stream poll failed")
|
|
118
135
|
try:
|
|
119
|
-
await asyncio.wait_for(stop.wait(),
|
|
136
|
+
await asyncio.wait_for(stop.wait(), poll_seconds)
|
|
120
137
|
except asyncio.TimeoutError:
|
|
121
138
|
pass
|
|
122
139
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""opencode-dashboard-server CLI: interactive configure + serve.
|
|
2
|
+
|
|
3
|
+
Run: uv tool install opencode-dashboard-server
|
|
4
|
+
opencode-dashboard-server configure
|
|
5
|
+
opencode-dashboard-server serve [--port N] [--host H] [--config PATH]
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
from platformdirs import user_config_path
|
|
15
|
+
|
|
16
|
+
from app import create_app
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def config_path() -> Path:
|
|
20
|
+
# Force XDG semantics so the backend shares ~/.config/opencode-dashboard
|
|
21
|
+
# with the front end; platformdirs would otherwise use ~/Library/Application
|
|
22
|
+
# Support on macOS. setdefault keeps a user-set XDG_CONFIG_HOME intact.
|
|
23
|
+
os.environ.setdefault("XDG_CONFIG_HOME", str(Path.home() / ".config"))
|
|
24
|
+
return user_config_path("opencode-dashboard") / "server.yaml"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_config(path: Path) -> dict:
|
|
28
|
+
if not path.exists():
|
|
29
|
+
return {}
|
|
30
|
+
return yaml.safe_load(path.read_text()) or {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _prompt(label: str, current, default) -> str:
|
|
34
|
+
hint = current if current != "" else default
|
|
35
|
+
try:
|
|
36
|
+
value = input(f"{label} [{hint}]: ").strip()
|
|
37
|
+
except EOFError:
|
|
38
|
+
return hint
|
|
39
|
+
return value if value else hint
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _prompt_num(label: str, current, default, conv):
|
|
43
|
+
while True:
|
|
44
|
+
try:
|
|
45
|
+
return conv(_prompt(label, current, default))
|
|
46
|
+
except ValueError:
|
|
47
|
+
print(f"Invalid {label}; enter a number.")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def cmd_configure(args) -> None:
|
|
51
|
+
path = config_path()
|
|
52
|
+
existing = load_config(path)
|
|
53
|
+
cur = {
|
|
54
|
+
"port": existing.get("port", 8791),
|
|
55
|
+
"host": existing.get("host", "0.0.0.0"),
|
|
56
|
+
"cors_origins": existing.get("cors_origins", ""),
|
|
57
|
+
"poll_seconds": existing.get("poll_seconds", 5),
|
|
58
|
+
"opencode_bin": existing.get("opencode_bin", "opencode"),
|
|
59
|
+
}
|
|
60
|
+
if existing:
|
|
61
|
+
print(f"Current config at {path}:")
|
|
62
|
+
print(yaml.safe_dump(cur, sort_keys=False).rstrip())
|
|
63
|
+
print()
|
|
64
|
+
cur["port"] = _prompt_num("port", cur["port"], 8791, int)
|
|
65
|
+
cur["host"] = _prompt("host", cur["host"], "0.0.0.0")
|
|
66
|
+
cur["cors_origins"] = _prompt(
|
|
67
|
+
"cors_origins (comma-separated; empty = built-in loopback whitelist)",
|
|
68
|
+
cur["cors_origins"], "",
|
|
69
|
+
)
|
|
70
|
+
cur["poll_seconds"] = _prompt_num("poll_seconds", cur["poll_seconds"], 5, float)
|
|
71
|
+
cur["opencode_bin"] = _prompt("opencode_bin", cur["opencode_bin"], "opencode")
|
|
72
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
path.write_text(yaml.safe_dump(cur, sort_keys=False))
|
|
74
|
+
print(f"Wrote {path}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cmd_serve(args) -> None:
|
|
78
|
+
path = args.config or config_path()
|
|
79
|
+
cfg = load_config(path)
|
|
80
|
+
try:
|
|
81
|
+
port = args.port or int(os.environ.get("PORT") or cfg.get("port") or 8791)
|
|
82
|
+
except ValueError:
|
|
83
|
+
print(f"invalid port: {os.environ.get('PORT') or cfg.get('port')!r}")
|
|
84
|
+
sys.exit(2)
|
|
85
|
+
host = args.host or os.environ.get("HOST") or cfg.get("host") or "0.0.0.0"
|
|
86
|
+
|
|
87
|
+
import uvicorn
|
|
88
|
+
|
|
89
|
+
# cors_origins is stored comma-separated; create_app expects a list.
|
|
90
|
+
cors = [o.strip() for o in str(cfg.get("cors_origins", "")).split(",") if o.strip()]
|
|
91
|
+
|
|
92
|
+
uvicorn.run(
|
|
93
|
+
create_app(
|
|
94
|
+
cors_origins=cors or None,
|
|
95
|
+
poll_seconds=cfg.get("poll_seconds"),
|
|
96
|
+
opencode_bin=cfg.get("opencode_bin") or None,
|
|
97
|
+
),
|
|
98
|
+
host=host,
|
|
99
|
+
port=port,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def main(argv=None) -> int:
|
|
104
|
+
parser = argparse.ArgumentParser(
|
|
105
|
+
prog="opencode-dashboard-server",
|
|
106
|
+
description="opencode token dashboard aggregation backend",
|
|
107
|
+
)
|
|
108
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
109
|
+
sub.add_parser("configure", help="interactively write the XDG config file")
|
|
110
|
+
serve = sub.add_parser("serve", help="run the server (FastAPI + uvicorn)")
|
|
111
|
+
serve.add_argument("--config", metavar="PATH", help="config file (default: XDG server.yaml)")
|
|
112
|
+
serve.add_argument("--port", type=int, help="listen port")
|
|
113
|
+
serve.add_argument("--host", help="bind host")
|
|
114
|
+
args = parser.parse_args(argv)
|
|
115
|
+
if args.cmd == "configure":
|
|
116
|
+
cmd_configure(args)
|
|
117
|
+
elif args.cmd == "serve":
|
|
118
|
+
cmd_serve(args)
|
|
119
|
+
else:
|
|
120
|
+
parser.print_usage()
|
|
121
|
+
return 2
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
sys.exit(main())
|
|
@@ -11,8 +11,12 @@ Two interchangeable runners over the same SQL surface:
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import json
|
|
14
|
+
import os
|
|
14
15
|
import subprocess
|
|
15
16
|
|
|
17
|
+
# opencode CLI binary; override for a non-PATH install (e.g. a brew cellar path).
|
|
18
|
+
OPENCODE_BIN = os.environ.get("OPENCODE_BIN", "opencode")
|
|
19
|
+
|
|
16
20
|
|
|
17
21
|
def _inline(sql: str, params: tuple) -> str:
|
|
18
22
|
"""Inline bound params into SQL as safely-quoted literals (CliRunner has no
|
|
@@ -29,8 +33,8 @@ def _inline(sql: str, params: tuple) -> str:
|
|
|
29
33
|
|
|
30
34
|
|
|
31
35
|
class CliRunner:
|
|
32
|
-
def __init__(self, executable: str =
|
|
33
|
-
self._cmd = [executable, "db"]
|
|
36
|
+
def __init__(self, executable: str | None = None):
|
|
37
|
+
self._cmd = [executable or OPENCODE_BIN, "db"]
|
|
34
38
|
|
|
35
39
|
def query(self, sql: str, params: tuple = ()) -> list[dict]:
|
|
36
40
|
raw = subprocess.run(
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opencode-dashboard-server
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: FastAPI aggregator over opencode's SQLite storage
|
|
5
|
+
Project-URL: Homepage, https://github.com/GCS-ZHN/opencode-dashboard
|
|
6
|
+
Project-URL: Repository, https://github.com/GCS-ZHN/opencode-dashboard
|
|
7
|
+
Project-URL: Issues, https://github.com/GCS-ZHN/opencode-dashboard/issues
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: fastapi<1,>=0.115
|
|
11
|
+
Requires-Dist: uvicorn<1,>=0.30
|
|
12
|
+
Requires-Dist: platformdirs<5,>=4
|
|
13
|
+
Requires-Dist: PyYAML<7,>=6
|
|
14
|
+
|
|
15
|
+
# opencode-dashboard-server
|
|
16
|
+
|
|
17
|
+
FastAPI aggregation backend for the [opencode-dashboard](https://github.com/GCS-ZHN/opencode-dashboard)
|
|
18
|
+
project. Reads opencode's local SQLite storage via the `opencode db` CLI and exposes a JSON + SSE
|
|
19
|
+
API for the dashboard front end.
|
|
20
|
+
|
|
21
|
+
## Install (PyPI)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install opencode-dashboard-server
|
|
25
|
+
# or with uv:
|
|
26
|
+
uv tool install opencode-dashboard-server
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requires:
|
|
30
|
+
|
|
31
|
+
- **Python ≥ 3.10**
|
|
32
|
+
- **`opencode` CLI** on the same host — the server shells out to `opencode db "<SQL>"` and never
|
|
33
|
+
opens the SQLite file directly (it's WAL-mode and actively written while opencode runs).
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
Configure once (interactive; writes `~/.config/opencode-dashboard/server.yaml`), then serve:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
opencode-dashboard-server configure
|
|
41
|
+
opencode-dashboard-server serve
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`configure` walks you through `port` (default `8791`), `host` (default `0.0.0.0`),
|
|
45
|
+
`cors_origins` (comma-separated; empty = built-in loopback whitelist), `poll_seconds`
|
|
46
|
+
(default `5`), and `opencode_bin` (default `opencode`). Empty input keeps the default.
|
|
47
|
+
|
|
48
|
+
`serve` accepts overrides: `--port N`, `--host H`, `--config PATH` (instead of the XDG file).
|
|
49
|
+
Precedence for port/host: CLI flag > env `PORT`/`HOST` > config file > default.
|
|
50
|
+
|
|
51
|
+
Run one aggregator per opencode host:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uvicorn app:app --port 8791
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
or directly with uvicorn (still works — reads env/defaults only). Run on more hosts with
|
|
58
|
+
different ports (`8792`, `8793`, …) and list each in the front end's config (`opencode-dashboard configure`).
|
|
59
|
+
Environment switches (used when no config file value is set):
|
|
60
|
+
|
|
61
|
+
- `DASHBOARD_CORS_ORIGINS` — comma-separated CORS allow-list (defaults to the loopback dev origins `localhost/127.0.0.1:5173` and `:4173`; tighten or widen for your deployment).
|
|
62
|
+
- `DASHBOARD_POLL_SECONDS` — SSE poll interval in seconds (default `5`).
|
|
63
|
+
- `OPENCODE_BIN` — path to the `opencode` binary if it's not on `PATH`.
|
|
64
|
+
|
|
65
|
+
## API
|
|
66
|
+
|
|
67
|
+
- `GET /health` — liveness
|
|
68
|
+
- `GET /overview` — whole-host aggregate (tokens by type + cost, session counts, `updatedAt`)
|
|
69
|
+
- `GET /projects` — per-project rollups
|
|
70
|
+
- `GET /projects/{id}` — one project's sessions (parent/child tree)
|
|
71
|
+
- `GET /sessions/{id}` — per-model breakdown for one session (handles mid-conversation model switches)
|
|
72
|
+
- `GET /stream` — SSE: `updated` events when the DB changes (polled ~5s)
|
|
73
|
+
|
|
74
|
+
JSON is camelCase, timestamps are epoch-ms. Full contract in the repo's `API.md`.
|
|
75
|
+
|
|
76
|
+
## From source (development)
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
git clone https://github.com/GCS-ZHN/opencode-dashboard
|
|
80
|
+
cd opencode-dashboard/server
|
|
81
|
+
uv sync # install deps (fastapi, uvicorn) + dev (pytest, httpx)
|
|
82
|
+
uv run pytest # run tests
|
|
83
|
+
uvx ruff check . # lint
|
|
84
|
+
```
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
aggregate.py
|
|
3
3
|
app.py
|
|
4
|
+
cli.py
|
|
4
5
|
db.py
|
|
5
6
|
pyproject.toml
|
|
6
7
|
opencode_dashboard_server.egg-info/PKG-INFO
|
|
7
8
|
opencode_dashboard_server.egg-info/SOURCES.txt
|
|
8
9
|
opencode_dashboard_server.egg-info/dependency_links.txt
|
|
10
|
+
opencode_dashboard_server.egg-info/entry_points.txt
|
|
9
11
|
opencode_dashboard_server.egg-info/requires.txt
|
|
10
12
|
opencode_dashboard_server.egg-info/top_level.txt
|
|
11
13
|
tests/test_aggregate.py
|
|
@@ -4,20 +4,27 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "opencode-dashboard-server"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "FastAPI aggregator over opencode's SQLite storage"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
dependencies = [
|
|
12
12
|
"fastapi>=0.115,<1",
|
|
13
13
|
"uvicorn>=0.30,<1",
|
|
14
|
+
"platformdirs>=4,<5",
|
|
15
|
+
"PyYAML>=6,<7",
|
|
14
16
|
]
|
|
15
17
|
|
|
16
18
|
[project.urls]
|
|
17
19
|
Homepage = "https://github.com/GCS-ZHN/opencode-dashboard"
|
|
20
|
+
Repository = "https://github.com/GCS-ZHN/opencode-dashboard"
|
|
21
|
+
Issues = "https://github.com/GCS-ZHN/opencode-dashboard/issues"
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
opencode-dashboard-server = "cli:main"
|
|
18
25
|
|
|
19
26
|
[tool.setuptools]
|
|
20
|
-
py-modules = ["app", "aggregate", "db"]
|
|
27
|
+
py-modules = ["app", "aggregate", "db", "cli"]
|
|
21
28
|
|
|
22
29
|
[dependency-groups]
|
|
23
30
|
dev = [
|
|
@@ -46,3 +46,15 @@ def test_foreign_origin_not_allowed_by_cors():
|
|
|
46
46
|
"Access-Control-Request-Method": "GET",
|
|
47
47
|
})
|
|
48
48
|
assert "access-control-allow-origin" not in r.headers
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_cors_origins_injection():
|
|
52
|
+
conn = sqlite3.connect(":memory:", check_same_thread=False)
|
|
53
|
+
conn.executescript(SCHEMA)
|
|
54
|
+
seed(conn)
|
|
55
|
+
app = appmod.create_app(SqliteRunner(conn), cors_origins=["http://a.com"])
|
|
56
|
+
c = TestClient(app)
|
|
57
|
+
ok = c.options("/projects", headers={"Origin": "http://a.com", "Access-Control-Request-Method": "GET"})
|
|
58
|
+
assert ok.headers.get("access-control-allow-origin") == "http://a.com"
|
|
59
|
+
nope = c.options("/projects", headers={"Origin": "http://a.co", "Access-Control-Request-Method": "GET"})
|
|
60
|
+
assert "access-control-allow-origin" not in nope.headers
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: opencode-dashboard-server
|
|
3
|
-
Version: 0.1.1
|
|
4
|
-
Summary: FastAPI aggregator over opencode's SQLite storage
|
|
5
|
-
Project-URL: Homepage, https://github.com/GCS-ZHN/opencode-dashboard
|
|
6
|
-
Requires-Python: >=3.10
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
|
-
Requires-Dist: fastapi<1,>=0.115
|
|
9
|
-
Requires-Dist: uvicorn<1,>=0.30
|
|
10
|
-
|
|
11
|
-
# opencode-dashboard-server
|
|
12
|
-
|
|
13
|
-
FastAPI aggregation backend for the [opencode-dashboard](https://github.com/GCS-ZHN/opencode-dashboard)
|
|
14
|
-
project. Reads opencode's local SQLite storage via the `opencode db` CLI and exposes a JSON + SSE
|
|
15
|
-
API for the dashboard front end.
|
|
16
|
-
|
|
17
|
-
## Install
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
pip install opencode-dashboard-server
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Requires Python ≥ 3.10 and the `opencode` CLI on the same host (the server shells out to
|
|
24
|
-
`opencode db "<SQL>"` — it never opens the SQLite file directly, which is WAL-mode and actively
|
|
25
|
-
written while opencode runs).
|
|
26
|
-
|
|
27
|
-
## Usage
|
|
28
|
-
|
|
29
|
-
On each host that runs opencode:
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
uvicorn app:app --port 8791
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Run on more hosts with different ports (`8792`, `8793`, …) and point the front end at each.
|
|
36
|
-
|
|
37
|
-
## API
|
|
38
|
-
|
|
39
|
-
- `GET /health` — liveness
|
|
40
|
-
- `GET /overview` — whole-host aggregate (tokens by type + cost, session counts, `updatedAt`)
|
|
41
|
-
- `GET /projects` — per-project rollups
|
|
42
|
-
- `GET /projects/{id}` — one project's sessions (parent/child tree)
|
|
43
|
-
- `GET /sessions/{id}` — per-model breakdown for one session (handles mid-conversation model switches)
|
|
44
|
-
- `GET /stream` — SSE: `updated` events when the DB changes (polled ~5s)
|
|
45
|
-
|
|
46
|
-
JSON is camelCase, timestamps are epoch-ms. Full contract in the repo's `API.md`.
|
|
47
|
-
|
|
48
|
-
## Development
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
uv sync # install deps (fastapi, uvicorn) + dev (pytest, httpx)
|
|
52
|
-
uv run pytest # run tests
|
|
53
|
-
uvx ruff check . # lint
|
|
54
|
-
```
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# opencode-dashboard-server
|
|
2
|
-
|
|
3
|
-
FastAPI aggregation backend for the [opencode-dashboard](https://github.com/GCS-ZHN/opencode-dashboard)
|
|
4
|
-
project. Reads opencode's local SQLite storage via the `opencode db` CLI and exposes a JSON + SSE
|
|
5
|
-
API for the dashboard front end.
|
|
6
|
-
|
|
7
|
-
## Install
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
pip install opencode-dashboard-server
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Requires Python ≥ 3.10 and the `opencode` CLI on the same host (the server shells out to
|
|
14
|
-
`opencode db "<SQL>"` — it never opens the SQLite file directly, which is WAL-mode and actively
|
|
15
|
-
written while opencode runs).
|
|
16
|
-
|
|
17
|
-
## Usage
|
|
18
|
-
|
|
19
|
-
On each host that runs opencode:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
uvicorn app:app --port 8791
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Run on more hosts with different ports (`8792`, `8793`, …) and point the front end at each.
|
|
26
|
-
|
|
27
|
-
## API
|
|
28
|
-
|
|
29
|
-
- `GET /health` — liveness
|
|
30
|
-
- `GET /overview` — whole-host aggregate (tokens by type + cost, session counts, `updatedAt`)
|
|
31
|
-
- `GET /projects` — per-project rollups
|
|
32
|
-
- `GET /projects/{id}` — one project's sessions (parent/child tree)
|
|
33
|
-
- `GET /sessions/{id}` — per-model breakdown for one session (handles mid-conversation model switches)
|
|
34
|
-
- `GET /stream` — SSE: `updated` events when the DB changes (polled ~5s)
|
|
35
|
-
|
|
36
|
-
JSON is camelCase, timestamps are epoch-ms. Full contract in the repo's `API.md`.
|
|
37
|
-
|
|
38
|
-
## Development
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
uv sync # install deps (fastapi, uvicorn) + dev (pytest, httpx)
|
|
42
|
-
uv run pytest # run tests
|
|
43
|
-
uvx ruff check . # lint
|
|
44
|
-
```
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: opencode-dashboard-server
|
|
3
|
-
Version: 0.1.1
|
|
4
|
-
Summary: FastAPI aggregator over opencode's SQLite storage
|
|
5
|
-
Project-URL: Homepage, https://github.com/GCS-ZHN/opencode-dashboard
|
|
6
|
-
Requires-Python: >=3.10
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
|
-
Requires-Dist: fastapi<1,>=0.115
|
|
9
|
-
Requires-Dist: uvicorn<1,>=0.30
|
|
10
|
-
|
|
11
|
-
# opencode-dashboard-server
|
|
12
|
-
|
|
13
|
-
FastAPI aggregation backend for the [opencode-dashboard](https://github.com/GCS-ZHN/opencode-dashboard)
|
|
14
|
-
project. Reads opencode's local SQLite storage via the `opencode db` CLI and exposes a JSON + SSE
|
|
15
|
-
API for the dashboard front end.
|
|
16
|
-
|
|
17
|
-
## Install
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
pip install opencode-dashboard-server
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Requires Python ≥ 3.10 and the `opencode` CLI on the same host (the server shells out to
|
|
24
|
-
`opencode db "<SQL>"` — it never opens the SQLite file directly, which is WAL-mode and actively
|
|
25
|
-
written while opencode runs).
|
|
26
|
-
|
|
27
|
-
## Usage
|
|
28
|
-
|
|
29
|
-
On each host that runs opencode:
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
uvicorn app:app --port 8791
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Run on more hosts with different ports (`8792`, `8793`, …) and point the front end at each.
|
|
36
|
-
|
|
37
|
-
## API
|
|
38
|
-
|
|
39
|
-
- `GET /health` — liveness
|
|
40
|
-
- `GET /overview` — whole-host aggregate (tokens by type + cost, session counts, `updatedAt`)
|
|
41
|
-
- `GET /projects` — per-project rollups
|
|
42
|
-
- `GET /projects/{id}` — one project's sessions (parent/child tree)
|
|
43
|
-
- `GET /sessions/{id}` — per-model breakdown for one session (handles mid-conversation model switches)
|
|
44
|
-
- `GET /stream` — SSE: `updated` events when the DB changes (polled ~5s)
|
|
45
|
-
|
|
46
|
-
JSON is camelCase, timestamps are epoch-ms. Full contract in the repo's `API.md`.
|
|
47
|
-
|
|
48
|
-
## Development
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
uv sync # install deps (fastapi, uvicorn) + dev (pytest, httpx)
|
|
52
|
-
uv run pytest # run tests
|
|
53
|
-
uvx ruff check . # lint
|
|
54
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|