Skip to content

feat: run the management dashboard from osa start --with-ui - #17

Merged
rorybyrne merged 12 commits into
mainfrom
feat/dashboard-integration
Jul 29, 2026
Merged

feat: run the management dashboard from osa start --with-ui#17
rorybyrne merged 12 commits into
mainfrom
feat/dashboard-integration

Conversation

@rorybyrne

Copy link
Copy Markdown
Contributor

Closes #16.

Wires the self-hostable management dashboard into the CLI stack so osa start --with-ui brings it up alongside the server, pre-configured — no manual setup.

Changes

  • Compose template (osa/cli/templates/docker-compose.yml): a dashboard service in the ui profile, pulling ghcr.io/opensciencearchive/osa-dashboard:${OSA_IMAGE_VERSION}, depends_on: server (healthy), port ${DASHBOARD_PORT:-8081}:3000. It's fed the shared JWT_SECRET (so the SUPERADMIN token it mints validates against the server), SESSION_SECRET, DASHBOARD_USERNAME/PASSWORD, and OSA_API_URL: http://server:8000.
  • .env template (_ENV_TEMPLATE): a # === Dashboard === block with DASHBOARD_USERNAME, DASHBOARD_PASSWORD, SESSION_SECRET. OSA_DEV_MODE=true 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.

Why it just works

The dashboard mints its archive token exactly like the CLI's _mint_dev_token — HS256 over the shared JWT_SECRET, sub = the seeded admin@osa.local. So no change to the CLI's own auth is needed; the two minters coexist.

Tests

  • .env template carries the dashboard credentials.
  • Compose template wires the dashboard to the shared JWT_SECRET and the server.

Dependency

Requires the osa-dashboard image published by opensciencearchive/server#179 (the platform half). Pinned to OSA_IMAGE_VERSION, so it stays capability-matched to the server.

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.
@rorybyrne
rorybyrne changed the base branch from main to feat/stop-wipe-data July 27, 2026 11:39
@greptile-apps

greptile-apps Bot commented Jul 27, 2026

Copy link
Copy Markdown

Greptile Summary

Adds the management dashboard to the default local CLI stack.

  • Generates per-project database, JWT, session, and dashboard secrets in an atomically replaced owner-only .env.
  • Starts the dashboard through the Compose ui profile and supports source builds from a monorepo sibling.
  • Adds osa dashboard for short-lived browser authentication handoff and --no-ui for API-only startup.
  • Updates dashboard configuration, endpoint reporting, and tests.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failures remain.

Files Needing Attention: No files require attention.

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex produced a proof for a posted P1 finding; the reviewer comment contains the finding details.
  • The contract-validation run contrasted dashboard-generated-01-before.log with dashboard-generated-02-after.log to verify credentials and service wiring, confirmed unique secrets and that .env is mode 0600, and it recorded that the start command failed before changes with No such option: --with-ui and exit code 2, while the post-change start proceeds to service startup and then stops due to Docker being absent.
  • The validation run reported template and full-instance test results, with 6 tests passed in the template suite and 90 tests passed in the full-instance suite.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

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
Loading

Comments Outside Diff (1)

  1. General comment

    P1 Documented osa start --with-ui flow is rejected by the CLI

    • Bug
      • A freshly initialized project tells users in its generated .env that the dashboard is started with osa 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 plain osa start now enables the UI by default, removing the affirmative flag breaks the stated command and compatibility with the earlier interface.
    • Cause
      • osa/cli/main.py replaced the with_ui: --with-ui option with the inverse no_ui: --no-ui option, while osa/cli/instance.py and the intended workflow still reference osa start --with-ui.
    • Fix
      • Retain --with-ui as 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.

    T-Rex Ran code and verified through T-Rex

Reviews (10): Last reviewed commit: "feat: --source builds the dashboard from..." | Re-trigger Greptile

Comment thread osa/cli/instance.py Outdated
Comment on lines +247 to +249
DASHBOARD_USERNAME=admin
DASHBOARD_PASSWORD=osa-local-dev-dashboard-password-CHANGE-ME
SESSION_SECRET=osa-local-dev-session-secret-CHANGE-ME-min-32-chars

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread osa/cli/instance.py Outdated
Comment on lines +440 to +442
if with_ui:
ui.info(f"Web UI {LOCAL_WEB_URL}")
ui.info(f"Dashboard {LOCAL_DASHBOARD_URL} (login: admin)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Startup ignores configured UI endpoints

When WEB_PORT, DASHBOARD_PORT, or DASHBOARD_USERNAME is overridden in .env, these hardcoded values still report ports 8080/8081 and username admin, directing users to an inactive endpoint or the wrong dashboard login.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Comment thread osa/cli/instance.py Outdated
Comment on lines +440 to +442
# 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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

View artifacts

T-Rex Ran code and verified through T-Rex

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`.
Base automatically changed from feat/stop-wipe-data to main July 27, 2026 13:20
# Conflicts:
#	osa/cli/instance.py
#	osa/cli/main.py
#	tests/test_instance.py
Comment thread osa/cli/instance.py Outdated
# 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())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security 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

Repro: real CLI harness that initializes a project under umask 022, sanitizes secret values, checks permissions, and asserts against non-owner readability

  • Evidence file captured while the check ran.

Repro: captured command output showing successful initialization, mode 0644, redacted generated secret fields, and the failing security assertion

  • The full error output from the failing run.

View artifacts

T-Rex Ran code and verified through T-Rex

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Comment thread osa/cli/instance.py Outdated
Comment on lines +301 to +306
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security 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

Repro: repeatable force-init permission-window harness using the production initialization path and a distinct local UID

  • Evidence file captured while the check ran.

Repro: successful execution log showing mode 0644 and generated secrets read by UID 65534 before the final chmod

  • The full command output behind this check.

View artifacts

T-Rex Ran code and verified through T-Rex

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'.
@rorybyrne
rorybyrne merged commit 0e168e8 into main Jul 29, 2026
4 checks passed
@rorybyrne
rorybyrne deleted the feat/dashboard-integration branch July 29, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: wire the self-hostable dashboard into osa start

1 participant