feat: run the management dashboard from osa start --with-ui - #17
Conversation
By default `osa stop` runs `docker compose down`, keeping the named Postgres volume so the DB survives stop/start. `--wipe-data` additionally passes `--volumes` (drops the DB volume) and removes the `./.data` host bind mount (deposited files + hook artifacts), so local state is fully cleared. A failed `down` aborts before any deletion, and a missing `.data` dir is tolerated, so the flag never destroys data on an unsuccessful stop.
Wire the self-hostable dashboard into the CLI stack so `osa start --with-ui`
brings it up alongside the server, pre-configured:
- compose template: a `dashboard` service in the `ui` profile, pulling
osa-dashboard:${OSA_IMAGE_VERSION}, fed the SHARED JWT_SECRET (so its minted
SUPERADMIN token validates) and pointed at the server.
- .env template: DASHBOARD_USERNAME/PASSWORD + SESSION_SECRET. OSA_DEV_MODE
already seeds admin@osa.local, and JWT_SECRET is already shared, so login
works out of the box.
- osa start prints the Web UI and Dashboard URLs when --with-ui is set.
Requires the osa-dashboard image published by opensciencearchive/server#179.
Greptile SummaryAdds the management dashboard to the default local CLI stack.
Confidence Score: 5/5The PR appears safe to merge. No blocking failures remain. Files Needing Attention: No files require attention.
What T-Rex did
|
| Filename | Overview |
|---|---|
| osa/cli/instance.py | Generates private project secrets, configures source dashboard builds, reports the dashboard endpoint, and implements browser authentication handoff. |
| osa/cli/main.py | Makes UI startup the default, adds the API-only opt-out, and introduces the dashboard command. |
| osa/cli/templates/docker-compose.yml | Adds the loopback-bound dashboard service and separates the outdated web application into its own profile. |
| tests/test_instance.py | Covers generated secret strength and permissions, dashboard Compose wiring, source builds, UI startup behavior, and handoff-token creation. |
Sequence Diagram
sequenceDiagram
participant User
participant CLI as osa CLI
participant Compose as Docker Compose
participant Dashboard
participant Server
User->>CLI: osa start
CLI->>Compose: Start server + ui profile
Compose->>Server: Start with shared JWT_SECRET
Compose->>Dashboard: Start with JWT_SECRET and SESSION_SECRET
User->>CLI: osa dashboard
CLI->>CLI: Mint short-lived handoff token
CLI->>Dashboard: "Open /api/auth/handoff?t=token"
Dashboard->>User: Establish dashboard session
Dashboard->>Server: Access API with shared-secret token
Comments Outside Diff (1)
-
General comment
Documented osa start --with-ui flow is rejected by the CLI
- Bug
- A freshly initialized project tells users in its generated
.envthat the dashboard is started withosa start --with-ui, and the validation request identifies that as the intended flow. Running that exact command exits with code 2 before Compose is invoked:No such option: --with-ui. Although plainosa startnow enables the UI by default, removing the affirmative flag breaks the stated command and compatibility with the earlier interface.
- A freshly initialized project tells users in its generated
- Cause
osa/cli/main.pyreplaced thewith_ui: --with-uioption with the inverseno_ui: --no-uioption, whileosa/cli/instance.pyand the intended workflow still referenceosa start --with-ui.
- Fix
- Retain
--with-uias an accepted compatibility option, optionally hidden/deprecated, while keeping UI enabled by default and supporting--no-ui. Alternatively, update every generated instruction and explicitly revise the requested/public CLI contract, but accepting the old affirmative flag is safer for scripts and existing users.
- Retain
- Bug
Reviews (10): Last reviewed commit: "feat: --source builds the dashboard from..." | Re-trigger Greptile
| DASHBOARD_USERNAME=admin | ||
| DASHBOARD_PASSWORD=osa-local-dev-dashboard-password-CHANGE-ME | ||
| SESSION_SECRET=osa-local-dev-session-secret-CHANGE-ME-min-32-chars |
There was a problem hiding this comment.
Default credentials expose admin access
When a newly initialized instance runs with --with-ui, the fixed dashboard credentials are exposed through a port bound on every host interface, allowing any network-reachable user who knows these repository defaults to log in and obtain the dashboard's shared-secret SUPERADMIN access.
There was a problem hiding this comment.
Fixed in 0da687a. The dashboard port is now bound to 127.0.0.1 in the compose template — this is a local dev stack with default credentials, so it must not be network-reachable. Operators who need LAN/remote access edit that one line. (The server is deliberately loopback-only for the same reason.)
There was a problem hiding this comment.
Also addressed at the root in 8266613: osa init now generates unique random secrets (JWT/SESSION/DASHBOARD_PASSWORD/POSTGRES) into .env instead of shipping repo-known defaults — so even on loopback there's no guessable credential. Combined with the 127.0.0.1 binding, there's no default-cred exposure.
| if with_ui: | ||
| ui.info(f"Web UI {LOCAL_WEB_URL}") | ||
| ui.info(f"Dashboard {LOCAL_DASHBOARD_URL} (login: admin)") |
There was a problem hiding this comment.
Fixed in 0da687a. osa start --with-ui now reads WEB_PORT, DASHBOARD_PORT and DASHBOARD_USERNAME from .env and prints the actual configured URLs/login instead of the hardcoded defaults. Added a test asserting overridden values are reported.
) Two Greptile findings on the dashboard CLI integration: - Default dashboard credentials were exposed on every interface. Bind the dashboard port to 127.0.0.1 in the compose template — it's a local dev stack with default credentials, so it must not be network-reachable; operators edit the line for LAN access. - `osa start --with-ui` printed hardcoded ports/login. Read WEB_PORT, DASHBOARD_PORT and DASHBOARD_USERNAME from .env so the reported URLs match the operator's actual config.
| # operator may have overridden them in .env (which docker compose reads). | ||
| env = _read_env_file(project_dir / ".env") | ||
| web_port = env.get("WEB_PORT", "8080") |
There was a problem hiding this comment.
Empty values bypass report defaults
When WEB_PORT, DASHBOARD_PORT, or DASHBOARD_USERNAME is present but empty in .env, env.get(...) preserves the empty value while Docker Compose's :- interpolation selects 8080, 8081, or admin. Startup therefore prints an incomplete URL or blank login that differs from the running service configuration.
Artifacts
Repro: focused executable harness for empty UI configuration values
- Evidence file captured while the check ran.
Repro: failing execution output showing blank CLI report values and concrete Compose defaults
- The full error output from the failing run.
There was a problem hiding this comment.
Fixed in de8f065 — switched to env.get(key) or default so a present-but-empty port falls back like compose's ${PORT:-8080} instead of printing a blank. Added a test with empty WEB_PORT/DASHBOARD_PORT.
…less open Security + DX for the local stack: - `osa init` now generates unique random secrets (POSTGRES_PASSWORD, JWT_SECRET, SESSION_SECRET, DASHBOARD_PASSWORD) into .env instead of shipping repository- known placeholders. Consistent end-to-end because the CLI mints and the server validates against the same .env JWT_SECRET. - Drop the built-in DEV_JWT_SECRET fallback: a missing JWT_SECRET now fails loudly (run `osa init`) rather than silently using a known-weak secret. - Add `osa dashboard`: mints a short-lived handoff proof from SESSION_SECRET and opens the browser at the dashboard's /api/auth/handoff route, which sets the session cookie — signed in with no password typed and the secret never leaving .env. `osa start --with-ui` now points at it.
env.get(key, default) kept a present-but-empty value; compose's ${PORT:-8080}
falls back on empty too. Use `env.get(key) or default` so printed URLs never
show a blank port.
Starting the dashboard is the point of the local stack, so it shouldn't be opt-in. `osa start` now brings up the API + web UI + dashboard by default; `--no-ui` starts only the API (headless/CI/resource-constrained). Replaces the old opt-in `--with-ui`.
# Conflicts: # osa/cli/instance.py # osa/cli/main.py # tests/test_instance.py
| # Write .env with generated secrets | ||
| (project_dir / ".env").write_text(_ENV_TEMPLATE) | ||
| # Write .env with freshly generated per-project secrets. | ||
| (project_dir / ".env").write_text(_render_env_template()) |
There was a problem hiding this comment.
Generated secrets remain locally readable
When osa init runs with a common 022 umask on a multi-user machine, Path.write_text creates .env with mode 0644. Other local users can then read the generated JWT, session, dashboard, and database credentials and use them to obtain administrative access, so randomizing the previous default credentials does not fully prevent their disclosure.
Artifacts
- Evidence file captured while the check ran.
- The full error output from the failing run.
There was a problem hiding this comment.
Fixed in 25d29a4. .env is now written by a helper that creates it 0600 up front (os.open(..., 0o600), so there's no briefly-world-readable window) and re-chmods to 0600 on --force (O_CREAT won't tighten an existing file). Tests assert the mode is 0600 on fresh init and after --force over a 0644 file.
Path.write_text created .env at 0644 under a default umask, so other local users could read the generated JWT/session/dashboard/DB secrets. Write it via a helper that creates the file 0600 up front (no world-readable window) and re-chmods on --force. Tests assert the mode on fresh init and --force.
| fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) | ||
| try: | ||
| os.write(fd, content.encode()) | ||
| finally: | ||
| os.close(fd) | ||
| path.chmod(0o600) |
There was a problem hiding this comment.
Replacement secrets retain loose permissions
When osa init --force overwrites an existing .env with mode 0644, O_TRUNC preserves that mode while this helper writes the newly generated secrets and restricts the file only afterward. Other local users can therefore read partial or complete JWT, session, dashboard, and database credentials during the replacement interval.
Artifacts
- Evidence file captured while the check ran.
- The full command output behind this check.
There was a problem hiding this comment.
Fixed in 557204e. Switched to an atomic write: the new secrets go to a mkstemp 0600 temp file in the same directory, then os.replace renames it over .env. So even on --force over a 0644 file, the secrets are never present in a world-readable file (the old inode is unlinked; the new .env inherits the temp's 0600). Test asserts 0600 after --force and no temp leftover.
…rce (Greptile) The prior write-then-chmod briefly left new secrets in a 0644 file when --force overwrote an existing .env (O_TRUNC keeps the old mode). Write to a mkstemp 0600 temp in the same dir and os.replace it over .env — the secrets are never present in a world-readable file. Test asserts 0600 after --force + no temp leftover.
Guards that --source (server-from-source build) keeps the web UI + dashboard (ui profile), independent of the source build.
… version warning
- osa start --source now builds the dashboard from the monorepo sibling
(apps/dashboard) for the host's native platform, instead of pulling a
published tag that may not exist yet (manifest unknown).
- Exclude the out-of-date web app from osa start: it moves to its own 'web'
compose profile (default 'ui' profile = server + dashboard).
- Default ${OSA_IMAGE_VERSION:-latest} in the template so non-start commands
(logs/stop/status) don't warn 'variable not set'.
Closes #16.
Wires the self-hostable management dashboard into the CLI stack so
osa start --with-uibrings it up alongside the server, pre-configured — no manual setup.Changes
osa/cli/templates/docker-compose.yml): adashboardservice in theuiprofile, pullingghcr.io/opensciencearchive/osa-dashboard:${OSA_IMAGE_VERSION},depends_on: server (healthy), port${DASHBOARD_PORT:-8081}:3000. It's fed the sharedJWT_SECRET(so the SUPERADMIN token it mints validates against the server),SESSION_SECRET,DASHBOARD_USERNAME/PASSWORD, andOSA_API_URL: http://server:8000..envtemplate (_ENV_TEMPLATE): a# === Dashboard ===block withDASHBOARD_USERNAME,DASHBOARD_PASSWORD,SESSION_SECRET.OSA_DEV_MODE=truealready seedsadmin@osa.local, andJWT_SECRETis already shared, so login works out of the box.osa start: prints the Web UI and Dashboard URLs when--with-uiis set.Why it just works
The dashboard mints its archive token exactly like the CLI's
_mint_dev_token— HS256 over the sharedJWT_SECRET,sub= the seededadmin@osa.local. So no change to the CLI's own auth is needed; the two minters coexist.Tests
.envtemplate carries the dashboard credentials.JWT_SECRETand the server.Dependency
Requires the
osa-dashboardimage published by opensciencearchive/server#179 (the platform half). Pinned toOSA_IMAGE_VERSION, so it stays capability-matched to the server.