opencode-dashboard-server 0.2.0__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.
Files changed (16) hide show
  1. {opencode_dashboard_server-0.2.0 → opencode_dashboard_server-0.3.0}/PKG-INFO +22 -3
  2. {opencode_dashboard_server-0.2.0 → opencode_dashboard_server-0.3.0}/README.md +19 -2
  3. {opencode_dashboard_server-0.2.0 → opencode_dashboard_server-0.3.0}/app.py +18 -13
  4. opencode_dashboard_server-0.3.0/cli.py +126 -0
  5. {opencode_dashboard_server-0.2.0 → opencode_dashboard_server-0.3.0}/opencode_dashboard_server.egg-info/PKG-INFO +22 -3
  6. {opencode_dashboard_server-0.2.0 → opencode_dashboard_server-0.3.0}/opencode_dashboard_server.egg-info/SOURCES.txt +2 -0
  7. opencode_dashboard_server-0.3.0/opencode_dashboard_server.egg-info/entry_points.txt +2 -0
  8. {opencode_dashboard_server-0.2.0 → opencode_dashboard_server-0.3.0}/opencode_dashboard_server.egg-info/requires.txt +2 -0
  9. {opencode_dashboard_server-0.2.0 → opencode_dashboard_server-0.3.0}/opencode_dashboard_server.egg-info/top_level.txt +1 -0
  10. {opencode_dashboard_server-0.2.0 → opencode_dashboard_server-0.3.0}/pyproject.toml +7 -2
  11. {opencode_dashboard_server-0.2.0 → opencode_dashboard_server-0.3.0}/tests/test_app.py +12 -0
  12. {opencode_dashboard_server-0.2.0 → opencode_dashboard_server-0.3.0}/aggregate.py +0 -0
  13. {opencode_dashboard_server-0.2.0 → opencode_dashboard_server-0.3.0}/db.py +0 -0
  14. {opencode_dashboard_server-0.2.0 → opencode_dashboard_server-0.3.0}/opencode_dashboard_server.egg-info/dependency_links.txt +0 -0
  15. {opencode_dashboard_server-0.2.0 → opencode_dashboard_server-0.3.0}/setup.cfg +0 -0
  16. {opencode_dashboard_server-0.2.0 → opencode_dashboard_server-0.3.0}/tests/test_aggregate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencode-dashboard-server
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: FastAPI aggregator over opencode's SQLite storage
5
5
  Project-URL: Homepage, https://github.com/GCS-ZHN/opencode-dashboard
6
6
  Project-URL: Repository, https://github.com/GCS-ZHN/opencode-dashboard
@@ -9,6 +9,8 @@ Requires-Python: >=3.10
9
9
  Description-Content-Type: text/markdown
10
10
  Requires-Dist: fastapi<1,>=0.115
11
11
  Requires-Dist: uvicorn<1,>=0.30
12
+ Requires-Dist: platformdirs<5,>=4
13
+ Requires-Dist: PyYAML<7,>=6
12
14
 
13
15
  # opencode-dashboard-server
14
16
 
@@ -20,6 +22,8 @@ API for the dashboard front end.
20
22
 
21
23
  ```bash
22
24
  pip install opencode-dashboard-server
25
+ # or with uv:
26
+ uv tool install opencode-dashboard-server
23
27
  ```
24
28
 
25
29
  Requires:
@@ -30,14 +34,29 @@ Requires:
30
34
 
31
35
  ## Quick start
32
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
+
33
51
  Run one aggregator per opencode host:
34
52
 
35
53
  ```bash
36
54
  uvicorn app:app --port 8791
37
55
  ```
38
56
 
39
- Run on more hosts with different ports (`8792`, `8793`, ) and list each in the front end's
40
- `dashboard.yaml`. Environment switches:
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):
41
60
 
42
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).
43
62
  - `DASHBOARD_POLL_SECONDS` — SSE poll interval in seconds (default `5`).
@@ -8,6 +8,8 @@ API for the dashboard front end.
8
8
 
9
9
  ```bash
10
10
  pip install opencode-dashboard-server
11
+ # or with uv:
12
+ uv tool install opencode-dashboard-server
11
13
  ```
12
14
 
13
15
  Requires:
@@ -18,14 +20,29 @@ Requires:
18
20
 
19
21
  ## Quick start
20
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
+
21
37
  Run one aggregator per opencode host:
22
38
 
23
39
  ```bash
24
40
  uvicorn app:app --port 8791
25
41
  ```
26
42
 
27
- Run on more hosts with different ports (`8792`, `8793`, ) and list each in the front end's
28
- `dashboard.yaml`. Environment switches:
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):
29
46
 
30
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).
31
48
  - `DASHBOARD_POLL_SECONDS` — SSE poll interval in seconds (default `5`).
@@ -22,7 +22,7 @@ logger = logging.getLogger("dashboard")
22
22
 
23
23
  # Comma-separated CORS allow-list; defaults to the loopback dev origins. Tight
24
24
  # by default so a random webpage can't exfiltrate local project/session data.
25
- def cors_origins() -> list[str]:
25
+ def default_cors_origins() -> list[str]:
26
26
  env = os.environ.get("DASHBOARD_CORS_ORIGINS", "").strip()
27
27
  if env:
28
28
  return [o.strip() for o in env.split(",") if o.strip()]
@@ -32,27 +32,32 @@ def cors_origins() -> list[str]:
32
32
  ]
33
33
 
34
34
 
35
- POLL_SECONDS = float(os.environ.get("DASHBOARD_POLL_SECONDS", "5"))
36
-
37
-
38
- @lru_cache(maxsize=1)
39
- def opencode_version() -> str:
35
+ @lru_cache(maxsize=8)
36
+ def opencode_version(executable: str = "opencode") -> str:
40
37
  try:
41
38
  return subprocess.run(
42
- ["opencode", "--version"], capture_output=True, text=True, check=True
39
+ [executable, "--version"], capture_output=True, text=True, check=True
43
40
  ).stdout.strip()
44
41
  except (OSError, subprocess.CalledProcessError):
45
42
  return "unknown" # e.g. CI without the opencode CLI; don't fail the request
46
43
 
47
44
 
48
- def create_app(runner=None) -> FastAPI:
49
- runner = runner or CliRunner()
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
50
55
  app = FastAPI(title="opencode token dashboard")
51
56
  # Loopback-only API; restrict origins so a random webpage can't exfiltrate
52
57
  # local project/session data from the browser (the client runs from Vite).
53
58
  app.add_middleware(
54
59
  CORSMiddleware,
55
- allow_origins=cors_origins(),
60
+ allow_origins=origins,
56
61
  allow_methods=["*"],
57
62
  allow_headers=["*"],
58
63
  )
@@ -68,14 +73,14 @@ def create_app(runner=None) -> FastAPI:
68
73
 
69
74
  @app.get("/health")
70
75
  def health():
71
- return {"status": "ok", "version": opencode_version()}
76
+ return {"status": "ok", "version": opencode_version(bin)}
72
77
 
73
78
  @app.get("/overview")
74
79
  def overview():
75
80
  def run():
76
81
  data = aggregate.overview(runner)
77
82
  data["host"] = socket.gethostname()
78
- data["opencodeVersion"] = opencode_version()
83
+ data["opencodeVersion"] = opencode_version(bin)
79
84
  return data
80
85
 
81
86
  return handle(run)
@@ -128,7 +133,7 @@ def create_app(runner=None) -> FastAPI:
128
133
  except Exception:
129
134
  logger.exception("stream poll failed")
130
135
  try:
131
- await asyncio.wait_for(stop.wait(), POLL_SECONDS)
136
+ await asyncio.wait_for(stop.wait(), poll_seconds)
132
137
  except asyncio.TimeoutError:
133
138
  pass
134
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())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencode-dashboard-server
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: FastAPI aggregator over opencode's SQLite storage
5
5
  Project-URL: Homepage, https://github.com/GCS-ZHN/opencode-dashboard
6
6
  Project-URL: Repository, https://github.com/GCS-ZHN/opencode-dashboard
@@ -9,6 +9,8 @@ Requires-Python: >=3.10
9
9
  Description-Content-Type: text/markdown
10
10
  Requires-Dist: fastapi<1,>=0.115
11
11
  Requires-Dist: uvicorn<1,>=0.30
12
+ Requires-Dist: platformdirs<5,>=4
13
+ Requires-Dist: PyYAML<7,>=6
12
14
 
13
15
  # opencode-dashboard-server
14
16
 
@@ -20,6 +22,8 @@ API for the dashboard front end.
20
22
 
21
23
  ```bash
22
24
  pip install opencode-dashboard-server
25
+ # or with uv:
26
+ uv tool install opencode-dashboard-server
23
27
  ```
24
28
 
25
29
  Requires:
@@ -30,14 +34,29 @@ Requires:
30
34
 
31
35
  ## Quick start
32
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
+
33
51
  Run one aggregator per opencode host:
34
52
 
35
53
  ```bash
36
54
  uvicorn app:app --port 8791
37
55
  ```
38
56
 
39
- Run on more hosts with different ports (`8792`, `8793`, ) and list each in the front end's
40
- `dashboard.yaml`. Environment switches:
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):
41
60
 
42
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).
43
62
  - `DASHBOARD_POLL_SECONDS` — SSE poll interval in seconds (default `5`).
@@ -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
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ opencode-dashboard-server = cli:main
@@ -1,2 +1,4 @@
1
1
  fastapi<1,>=0.115
2
2
  uvicorn<1,>=0.30
3
+ platformdirs<5,>=4
4
+ PyYAML<7,>=6
@@ -4,13 +4,15 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "opencode-dashboard-server"
7
- version = "0.2.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]
@@ -18,8 +20,11 @@ Homepage = "https://github.com/GCS-ZHN/opencode-dashboard"
18
20
  Repository = "https://github.com/GCS-ZHN/opencode-dashboard"
19
21
  Issues = "https://github.com/GCS-ZHN/opencode-dashboard/issues"
20
22
 
23
+ [project.scripts]
24
+ opencode-dashboard-server = "cli:main"
25
+
21
26
  [tool.setuptools]
22
- py-modules = ["app", "aggregate", "db"]
27
+ py-modules = ["app", "aggregate", "db", "cli"]
23
28
 
24
29
  [dependency-groups]
25
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