Skip to content

feat(rm-server): POC-ready — autohydrate seed + Railway Dockerfile + $PORT bind#23

Merged
AdaWorldAPI merged 1 commit into
mainfrom
claude/home-page-and-new-issue-link
Jun 23, 2026
Merged

feat(rm-server): POC-ready — autohydrate seed + Railway Dockerfile + $PORT bind#23
AdaWorldAPI merged 1 commit into
mainfrom
claude/home-page-and-new-issue-link

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

What

Three pieces that together make redmine-rs deployable as a POC — cargo run -p rm-server lands on a populated browser (filter / sort / paginate actually has things to filter), docker build . produces a Railway-ready image, and $PORT is honored at the bind site.

1. Autohydrate (rm-store::seed)

New module — seeds a Redmine-shaped demo corpus on first boot if the store is empty.

pub fn seed_enabled_for(env: Option<&str>) -> bool         // pure, testable
pub fn seed_enabled() -> bool                              // reads RM_SEED
pub async fn hydrate_demo_data(&Store) -> Result<usize, _> // always seeds if empty
pub async fn hydrate_demo_data_on_boot(&Store) -> Result<usize, _>  // honors RM_SEED
rows
issues 27 — varied subjects so bug matches some-not-all (substring tests have real partitions); alphabetically spread so sort by subject produces visibly-different page-1 vs page-2
issue_statuses / trackers / priorities 4 / 3 / 4
projects / users / roles 3 / 4 / 3
news / wiki / time_entries / repositories / queries / relations 2 / 2 / 5 / 2 / 3 / 1

Idempotent. Empty store → seed runs (~52 rows). Anything already present → no-op. So a user creating an issue via the D1 form doesn't trigger a re-seed; production data is never touched.

Operator override. RM_SEED=off (or 0/false/no, case-insensitive) skips the seed. Default-on for POC ergonomics.

#![forbid(unsafe_code)]-safe. The pure seed_enabled_for(Option<&str>) helper is testable directly; the env read happens at one boot-time call site only. Same pattern as op-server #58.

2. $PORT bind (rm-server)

resolve_bind_addr(env_port, fallback) -> std::io::Result<SocketAddr> honors the PaaS pattern — Railway / Heroku / Cloud Run / Fly route their public 443/80 edge to $PORT internally; the container must bind 0.0.0.0:$PORT. Falls back to ServerConfig::bind for local. Whitespace-only treated as unset; malformed values yield a diagnostic naming the bad value.

Mirrors op-server #58's same-named helper — both ports now land the lance-graph CONSUMER_SCAN_TODO.md §B1 PaaS pattern.

3. Dockerfile + .dockerignore

Top-level multi-stage build:

  • rust:1.95-bookworm (builder) — cargo build --release -p rm-server
  • debian:bookworm-slim (runtime) — ships only the release binary + CA certs
  • Non-root UID 10001 (PaaS platforms reject root containers)
  • ENV PORT=3000, EXPOSE 3000, CMD ["/usr/local/bin/rm-server"] exec-form so PID-1 is the binary itself and SIGTERM reaches axum's graceful-shutdown

.dockerignore keeps target/, .git/, IDE noise, and the Dockerfile itself out of the build context.

docker build -t redmine-rs .
docker run --rm -p 3000:3000 -e RUST_LOG=info redmine-rs
# Railway: just push — $PORT injection picks up automatically

Also fixed

issues_form::router from #22 wasn't merged into build_router_with (oversight in that PR). Now wired alongside issues::router.

Build

  • Toolchain 1.95. cargo test --workspace → all green:
    • rm-store: 52 (was 46; +6 new — seed_enabled_for cases × 4, fresh_store_gets_a_useful_corpus, second_call_is_idempotent_no_op, corpus_has_subject_variety_for_filter_testing)
    • rm-server: 10 (was 6; +4 new — resolve_bind_addr cases)
    • rm-handlers: 89 unchanged
    • others unchanged
  • cargo fmt --all --check clean.
  • cargo clippy --all-targets --workspace -- -D warnings clean.

Demo flow that works after this lands

$ cargo run -p rm-server                          # boots on 0.0.0.0:3000
INFO seeding demo data (store empty)…
INFO rows=52 demo data seeded
INFO bind=0.0.0.0:3000 rm-server listening

$ open http://localhost:3000/issues               # 27 seeded issues, paginated
$ open http://localhost:3000/issues?q=bug         # filter to ~3 rows
$ open http://localhost:3000/issues?sort=subject:desc&per_page=10  # sort + page
$ open http://localhost:3000/issues/new           # create form
  → fill subject, submit                          # 303 → /issues/<id>
$ open http://localhost:3000/projects             # 3 seeded projects
$ open http://localhost:3000/time_entries         # 5 seeded entries

This is the iconic Redmine demo loop, working end-to-end, against seeded data, in a Railway-deployable container.

…RT bind

Three pieces that together make redmine-rs deployable as a POC:

1) Autohydrate (rm-store::seed)
   ─────────────────────────────
   New module `crates/rm-store/src/seed.rs` seeds a Redmine-shaped demo
   corpus on first boot when the store is empty. Idempotent — subsequent
   boots no-op so user-created rows are never duplicated.

     pub fn seed_enabled_for(env: Option<&str>) -> bool     # pure, testable
     pub fn seed_enabled() -> bool                          # reads RM_SEED
     pub async fn hydrate_demo_data(&Store) -> Result<usize, _>
     pub async fn hydrate_demo_data_on_boot(&Store) -> Result<usize, _>

   Demo corpus: ~50 rows shaped so the D2 filter/sort/paginate chrome has
   real work to do — 27 issues (mixed subjects: "bug" matches some-not-all
   for substring tests; spread alphabetically for visible sort), 4
   statuses, 3 trackers, 4 priorities, 3 projects, 4 users, 3 roles, 2
   news, 2 wiki pages, 5 time entries, 2 repositories, 3 queries, 1
   relation.

   `RM_SEED=off` (also `0`/`false`/`no`, case-insensitive) disables the
   seed. Default-on for POC ergonomics.

   The crate is #![forbid(unsafe_code)]; `std::env::set_var` is unsafe in
   recent Rust. Tests target the pure `seed_enabled_for(Option<&str>)`
   helper directly — same pattern as op-server #58.

2) $PORT bind (rm-server)
   ──────────────────────
   `resolve_bind_addr(env_port, fallback)` honors $PORT for PaaS deploys
   (Railway, Heroku, Cloud Run, Fly route their public 443/80 edge to
   $PORT internally; the container must bind 0.0.0.0:$PORT). Falls back
   to ServerConfig::bind for local. Whitespace-only $PORT treated as
   unset; malformed $PORT yields a diagnostic error naming the bad value.

   Mirrors op-server #58's same-named helper — both ports now land the
   lance-graph CONSUMER_SCAN_TODO.md §B1 PaaS pattern.

3) Dockerfile (top-level) + .dockerignore
   ───────────────────────────────────────
   Multi-stage build: rust:1.95-bookworm → debian:bookworm-slim. Ships
   only the rm-server release binary + CA certs. Runs as non-root
   (UID 10001). Default ENV PORT=3000, EXPOSE 3000; PaaS overrides
   $PORT at deploy time. CMD uses exec-form so PID-1 is rm-server itself
   and SIGTERM reaches axum's graceful-shutdown handler.

   .dockerignore keeps target/, .git/, IDE noise, and the Dockerfile
   itself out of the build context.

Also wires issues_form::router into rm-server's build_router_with — #22
created the module but didn't merge its route (oversight in that PR).

cargo +1.95 test --workspace clean: rm-store 52 (+6 new seed), rm-server
10 (+4 new bind), rm-handlers 89 unchanged, plus the rest. fmt --all
clean, clippy --all-targets --workspace -- -D warnings clean.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@AdaWorldAPI AdaWorldAPI merged commit 56da8d1 into main Jun 23, 2026
1 check passed
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.

2 participants