blackopsrepl commited on
Commit
c216121
·
1 Parent(s): 1d65c62

feat(deploy): make fsr Docker Space ready

Browse files

Add a self-contained multi-stage Docker build for the field-service routing app.
The image builds the release binary from the repository root, copies the static
workspace and SolverForge configuration into the runtime image, exposes the
Space port, and starts solverforge_fsr directly.

Add a Makefile command surface for local development, validation, Docker image
builds, and Hugging Face Space-equivalent checks. The ci-local and pre-release
targets make the deployment contract explicit by running formatting, clippy,
release build, tests, frontend syntax validation, and the Space image build.

Update the README with Hugging Face Docker Space metadata, current runtime and
frontend dependency versions, quick-start commands, project structure, API
surface, and validation instructions. Ignore Playwright/test output and exclude
build-only artifacts from the Docker context so clean Space builds stay focused
on committed source and registry dependencies.

Files changed (5) hide show
  1. .dockerignore +9 -0
  2. .gitignore +2 -0
  3. Dockerfile +36 -0
  4. Makefile +184 -0
  5. README.md +118 -12
.dockerignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ target/
2
+ .git/
3
+ .osm_cache/
4
+ test-results/
5
+ playwright-report/
6
+ *.rs.bk
7
+ /app-session-*.js
8
+ /comment-preload.js
9
+ /main-*.js
.gitignore CHANGED
@@ -1,6 +1,8 @@
1
  /target
2
  .osm_cache/
3
  **/*.rs.bk
 
 
4
  /app-session-*.js
5
  /comment-preload.js
6
  /main-*.js
 
1
  /target
2
  .osm_cache/
3
  **/*.rs.bk
4
+ test-results/
5
+ playwright-report/
6
  /app-session-*.js
7
  /comment-preload.js
8
  /main-*.js
Dockerfile ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-stage build for solverforge-fsr.
2
+ #
3
+ # The app depends on published crates.io packages, so the repository root is the
4
+ # complete Docker build context:
5
+ # docker build -f Dockerfile -t solverforge-fsr .
6
+
7
+ FROM rust:1.95-alpine AS builder
8
+
9
+ RUN apk add --no-cache musl-dev
10
+
11
+ WORKDIR /build
12
+
13
+ COPY Cargo.toml Cargo.lock ./
14
+ COPY src/ ./src/
15
+ COPY static/ ./static/
16
+ COPY solver.toml ./solver.toml
17
+ COPY solverforge.app.toml ./solverforge.app.toml
18
+
19
+ RUN cargo build --release --target x86_64-unknown-linux-musl
20
+
21
+ FROM alpine:latest
22
+
23
+ RUN apk add --no-cache ca-certificates
24
+
25
+ WORKDIR /app
26
+
27
+ COPY --from=builder /build/target/x86_64-unknown-linux-musl/release/solverforge_fsr ./solverforge_fsr
28
+ COPY --from=builder /build/static/ ./static/
29
+ COPY --from=builder /build/solver.toml ./solver.toml
30
+ COPY --from=builder /build/solverforge.app.toml ./solverforge.app.toml
31
+
32
+ ENV PORT=7860
33
+
34
+ EXPOSE 7860
35
+
36
+ CMD ["./solverforge_fsr"]
Makefile ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SolverForge FSR Makefile v0.1.0
2
+ # Rust + frontend syntax + Space-oriented local build system.
3
+
4
+ GREEN := \033[92m
5
+ CYAN := \033[96m
6
+ YELLOW := \033[93m
7
+ RED := \033[91m
8
+ GRAY := \033[90m
9
+ BOLD := \033[1m
10
+ RESET := \033[0m
11
+
12
+ CHECK := OK
13
+ CROSS := FAIL
14
+ ARROW := =>
15
+ PROGRESS := ..
16
+
17
+ APP_NAME := solverforge_fsr
18
+ PACKAGE_NAME := solverforge-fsr
19
+ VERSION := $(shell sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)
20
+ RUST_VERSION := 1.95+
21
+ PORT ?= 7860
22
+ DOCKER_IMAGE ?= $(PACKAGE_NAME)
23
+ DOCKER_CONTEXT ?= .
24
+ DOCKERFILE_PATH := Dockerfile
25
+
26
+ .PHONY: help doctor build build-release run run-release test test-rust \
27
+ test-frontend-syntax lint fmt fmt-check clippy check ci-local \
28
+ space-ci space-build space-run docker-build docker-run pre-release \
29
+ version clean watch require-node require-docker
30
+
31
+ .DEFAULT_GOAL := help
32
+
33
+ require-node:
34
+ @command -v node >/dev/null 2>&1 || (printf "$(RED)$(CROSS) node is required for frontend validation$(RESET)\n" && exit 1)
35
+
36
+ require-docker:
37
+ @command -v docker >/dev/null 2>&1 || (printf "$(RED)$(CROSS) docker is required for Space/Docker targets$(RESET)\n" && exit 1)
38
+
39
+ doctor:
40
+ @printf "$(CYAN)$(BOLD)Environment Check$(RESET)\n\n"
41
+ @missing=0; \
42
+ if command -v cargo >/dev/null 2>&1; then \
43
+ printf "$(GREEN)$(CHECK) cargo: $$(cargo --version)$(RESET)\n"; \
44
+ else \
45
+ printf "$(RED)$(CROSS) cargo not found$(RESET)\n"; missing=1; \
46
+ fi; \
47
+ if command -v rustc >/dev/null 2>&1; then \
48
+ printf "$(GREEN)$(CHECK) rustc: $$(rustc --version)$(RESET)\n"; \
49
+ else \
50
+ printf "$(RED)$(CROSS) rustc not found$(RESET)\n"; missing=1; \
51
+ fi; \
52
+ if command -v node >/dev/null 2>&1; then \
53
+ printf "$(GREEN)$(CHECK) node: $$(node --version)$(RESET)\n"; \
54
+ else \
55
+ printf "$(YELLOW)! node not found; frontend syntax validation will be unavailable$(RESET)\n"; \
56
+ fi; \
57
+ if command -v docker >/dev/null 2>&1; then \
58
+ printf "$(GREEN)$(CHECK) docker: $$(docker --version)$(RESET)\n"; \
59
+ else \
60
+ printf "$(YELLOW)! docker not found; Space/Docker targets will be unavailable$(RESET)\n"; \
61
+ fi; \
62
+ printf "$(GRAY)Docker build context: $(DOCKER_CONTEXT)$(RESET)\n"; \
63
+ printf "$(GRAY)Default app port: $(PORT)$(RESET)\n"; \
64
+ if [ $$missing -ne 0 ]; then exit 1; fi
65
+
66
+ build:
67
+ @printf "$(ARROW) Building $(PACKAGE_NAME)...\n"
68
+ @cargo build --bin $(APP_NAME)
69
+
70
+ build-release:
71
+ @printf "$(ARROW) Building release binary...\n"
72
+ @cargo build --release --bin $(APP_NAME)
73
+
74
+ run:
75
+ @printf "$(ARROW) Running $(PACKAGE_NAME) on port $(PORT)...\n"
76
+ @PORT=$(PORT) cargo run --bin $(APP_NAME)
77
+
78
+ run-release:
79
+ @printf "$(ARROW) Running release build on port $(PORT)...\n"
80
+ @PORT=$(PORT) cargo run --release --bin $(APP_NAME)
81
+
82
+ test: test-rust test-frontend-syntax
83
+ @printf "\n$(GREEN)$(BOLD)$(CHECK) Standard validation passed$(RESET)\n\n"
84
+
85
+ test-rust:
86
+ @printf "$(PROGRESS) Running cargo test --quiet...\n"
87
+ @cargo test --quiet
88
+
89
+ test-frontend-syntax: require-node
90
+ @printf "$(PROGRESS) Checking frontend module syntax...\n"
91
+ @find static -name '*.js' -print0 | xargs -0 -n1 node --check
92
+
93
+ fmt:
94
+ @printf "$(PROGRESS) Formatting Rust code...\n"
95
+ @cargo fmt
96
+
97
+ fmt-check:
98
+ @printf "$(PROGRESS) Checking Rust formatting...\n"
99
+ @cargo fmt --check
100
+
101
+ clippy:
102
+ @printf "$(PROGRESS) Running clippy...\n"
103
+ @cargo clippy --all-targets -- -D warnings
104
+
105
+ lint: fmt-check clippy test-frontend-syntax
106
+ @printf "\n$(GREEN)$(BOLD)$(CHECK) Lint checks passed$(RESET)\n\n"
107
+
108
+ check: lint test
109
+
110
+ docker-build: require-docker
111
+ @printf "$(PROGRESS) Building Docker image $(DOCKER_IMAGE)...\n"
112
+ @docker build -f "$(DOCKERFILE_PATH)" -t "$(DOCKER_IMAGE)" "$(DOCKER_CONTEXT)"
113
+
114
+ docker-run: require-docker
115
+ @printf "$(ARROW) Running $(DOCKER_IMAGE) on port $(PORT)...\n"
116
+ @docker run --rm -it -e PORT=$(PORT) -p $(PORT):$(PORT) "$(DOCKER_IMAGE)"
117
+
118
+ space-build: docker-build
119
+
120
+ space-run: space-build
121
+ @printf "$(GREEN)$(CHECK) Starting local container that mirrors the Space image$(RESET)\n"
122
+ @$(MAKE) docker-run --no-print-directory PORT=$(PORT) DOCKER_IMAGE=$(DOCKER_IMAGE)
123
+
124
+ space-ci: ci-local
125
+
126
+ ci-local:
127
+ @printf "$(CYAN)$(BOLD)Local Space Validation Pipeline$(RESET)\n\n"
128
+ @printf "$(PROGRESS) Step 1/5: Format check...\n"
129
+ @$(MAKE) fmt-check --no-print-directory
130
+ @printf "$(PROGRESS) Step 2/5: Clippy...\n"
131
+ @$(MAKE) clippy --no-print-directory
132
+ @printf "$(PROGRESS) Step 3/5: Release build...\n"
133
+ @$(MAKE) build-release --no-print-directory
134
+ @printf "$(PROGRESS) Step 4/5: Standard test surface...\n"
135
+ @$(MAKE) test --no-print-directory
136
+ @printf "$(PROGRESS) Step 5/5: Docker/Space image build...\n"
137
+ @$(MAKE) space-build --no-print-directory
138
+ @printf "\n$(GREEN)$(BOLD)$(CHECK) LOCAL SPACE VALIDATION PASSED$(RESET)\n\n"
139
+
140
+ pre-release: ci-local
141
+ @printf "$(GREEN)$(BOLD)$(CHECK) Ready for Hugging Face Space update$(RESET)\n\n"
142
+
143
+ version:
144
+ @printf "$(CYAN)Current version:$(RESET) $(YELLOW)$(BOLD)$(VERSION)$(RESET)\n"
145
+ @printf "$(CYAN)Default port:$(RESET) $(YELLOW)$(BOLD)$(PORT)$(RESET)\n"
146
+
147
+ clean:
148
+ @printf "$(ARROW) Cleaning build artifacts...\n"
149
+ @cargo clean
150
+
151
+ watch:
152
+ @printf "$(ARROW) Watching and rerunning the app on port $(PORT)...\n"
153
+ @cargo watch --version >/dev/null 2>&1 || \
154
+ (printf "$(RED)$(CROSS) cargo-watch is required for make watch$(RESET)\n" && exit 1)
155
+ @PORT=$(PORT) cargo watch -x "run --bin $(APP_NAME)"
156
+
157
+ help:
158
+ @/bin/echo -e "$(CYAN)$(BOLD)Build & Run:$(RESET)"
159
+ @/bin/echo -e " $(GREEN)make build$(RESET) - Build the app in debug mode"
160
+ @/bin/echo -e " $(GREEN)make build-release$(RESET) - Build the app in release mode"
161
+ @/bin/echo -e " $(GREEN)make run$(RESET) - Run locally on port $(PORT)"
162
+ @/bin/echo -e " $(GREEN)make run-release$(RESET) - Run the release build on port $(PORT)"
163
+ @/bin/echo -e ""
164
+ @/bin/echo -e "$(CYAN)$(BOLD)Tests & Validation:$(RESET)"
165
+ @/bin/echo -e " $(GREEN)make test$(RESET) - Run Rust tests and frontend syntax checks"
166
+ @/bin/echo -e " $(GREEN)make lint$(RESET) - Run fmt-check, clippy, and frontend syntax checks"
167
+ @/bin/echo -e " $(GREEN)make ci-local$(RESET) - Run local Space validation pipeline"
168
+ @/bin/echo -e " $(GREEN)make pre-release$(RESET) - Run all local Space readiness checks"
169
+ @/bin/echo -e ""
170
+ @/bin/echo -e "$(CYAN)$(BOLD)Space & Docker:$(RESET)"
171
+ @/bin/echo -e " $(GREEN)make space-build$(RESET) - Build the Docker image used for Space deployment"
172
+ @/bin/echo -e " $(GREEN)make space-run$(RESET) - Build and run that image locally on port $(PORT)"
173
+ @/bin/echo -e " $(GREEN)make docker-build$(RESET) - Build the Docker image directly"
174
+ @/bin/echo -e " $(GREEN)make docker-run$(RESET) - Run the Docker image directly"
175
+ @/bin/echo -e ""
176
+ @/bin/echo -e "$(CYAN)$(BOLD)Other:$(RESET)"
177
+ @/bin/echo -e " $(GREEN)make doctor$(RESET) - Check local cargo/rustc/node readiness"
178
+ @/bin/echo -e " $(GREEN)make fmt$(RESET) - Format Rust code"
179
+ @/bin/echo -e " $(GREEN)make version$(RESET) - Show version and default port"
180
+ @/bin/echo -e " $(GREEN)make clean$(RESET) - Clean build artifacts"
181
+ @/bin/echo -e " $(GREEN)make watch$(RESET) - Watch source files and rerun the app"
182
+ @/bin/echo -e ""
183
+ @/bin/echo -e "$(GRAY)Rust version required: $(RUST_VERSION)$(RESET)"
184
+ @/bin/echo -e "$(GRAY)Current version: v$(VERSION)$(RESET)"
README.md CHANGED
@@ -1,32 +1,99 @@
1
- # solverforge-fsr
 
 
 
 
 
 
 
 
 
 
2
 
3
- A SolverForge constraint optimization project (scaffold: `neutral scaffold`).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  ## Versioning
6
 
7
- - CLI version used to scaffold this project: `2.0.2`
8
- - SolverForge runtime target for this scaffold: `solverforge 0.10.0`
 
 
9
  - SolverForge UI target for this scaffold: `solverforge-ui 0.6.5`
10
  - SolverForge maps target for this scaffold: `solverforge-maps 2.1.4`
11
- - Runtime dependency currently wired into `Cargo.toml`: `crates.io: solverforge 0.10.0`
12
  - Frontend UI dependency currently wired into `Cargo.toml`: `crates.io: solverforge-ui 0.6.5`
13
  - Maps dependency currently wired into `Cargo.toml`: `crates.io: solverforge-maps 2.1.4`
14
 
15
- This project was scaffolded by `solverforge-cli`, and it currently targets `SolverForge crate target 0.10.0` through the configured crate dependency targets.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  ## Quick Start
18
 
19
- ```bash
20
- # Start the solver server
21
- solverforge server
 
 
22
 
23
- # Or run directly
24
- cargo run --release
 
 
25
  ```
26
 
27
  ## Development
28
 
29
- ```bash
30
  # Add a new constraint
31
  solverforge generate constraint my_rule --unary --hard
32
 
@@ -49,5 +116,44 @@ solverforge destroy constraint my_rule
49
  | `src/solver/` | Solver service and configuration |
50
  | `src/api/` | HTTP routes and DTOs |
51
  | `src/data/` | Data loading and generation |
 
 
 
52
  | `solverforge.app.toml` | Scaffolded app/domain contract |
53
  | `solver.toml` | Solver configuration (termination, phases) |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SolverForge Field Service Routing
3
+ emoji: 🧰
4
+ colorFrom: indigo
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ license: apache-2.0
10
+ short_description: SolverForge field-service routing example
11
+ ---
12
 
13
+ # SolverForge FSR
14
+
15
+ `solverforge-fsr` is a SolverForge field-service routing example with retained
16
+ jobs, route geometry, technician schedules, and a browser map workspace.
17
+
18
+ It answers one concrete question:
19
+
20
+ "Given technicians, service visits, skills, parts, shifts, territories, and
21
+ road-network travel, which technician should serve each visit and in what
22
+ order?"
23
+
24
+ ## Hugging Face Space Deployment
25
+
26
+ This repo is Docker-Space ready. Hugging Face reads the README front matter,
27
+ builds `Dockerfile`, and expects the app to bind `PORT=7860`.
28
+
29
+ Local Space-equivalent commands:
30
+
31
+ ```sh
32
+ make space-build
33
+ make space-run
34
+ ```
35
+
36
+ Full local readiness check:
37
+
38
+ ```sh
39
+ make ci-local
40
+ ```
41
+
42
+ The Space image is self-contained: it builds against published crates.io
43
+ packages, copies `static/`, `solver.toml`, and `solverforge.app.toml`, and runs
44
+ the release binary `solverforge_fsr`.
45
 
46
  ## Versioning
47
 
48
+ The app package version is `0.1.0`; the release binary is `solverforge_fsr`.
49
+
50
+ - CLI version used to scaffold this project: `2.0.3`
51
+ - SolverForge runtime target for this scaffold: `solverforge 0.11.0`
52
  - SolverForge UI target for this scaffold: `solverforge-ui 0.6.5`
53
  - SolverForge maps target for this scaffold: `solverforge-maps 2.1.4`
54
+ - Runtime dependency currently wired into `Cargo.toml`: `crates.io: solverforge 0.11.0`
55
  - Frontend UI dependency currently wired into `Cargo.toml`: `crates.io: solverforge-ui 0.6.5`
56
  - Maps dependency currently wired into `Cargo.toml`: `crates.io: solverforge-maps 2.1.4`
57
 
58
+ This project was scaffolded by `solverforge-cli`, and it currently targets `SolverForge crate target 0.11.0` through the configured crate dependency targets.
59
+
60
+ `solverforge-core` is a direct dependency because the 0.11 custom
61
+ incremental-constraint API exposes `ConstraintRef`, while the top-level facade
62
+ does not re-export that type.
63
+
64
+ ## What SolverForge Is Doing Here
65
+
66
+ - `ServiceVisit` is a problem fact: a customer job the solver must place in a
67
+ technician route.
68
+ - `TechnicianRoute` is the planning entity: each technician owns one mutable
69
+ route.
70
+ - `TechnicianRoute.visits` is the list planning variable.
71
+ - `FieldServicePlan` is the planning solution.
72
+ - Constraints score assignment coverage, route reachability, shift capacity,
73
+ required skills, required parts, time windows, priority slack, territory
74
+ affinity, travel time, and workload balance.
75
+ - `solver.toml` selects the construction and local-search policy.
76
+
77
+ The app ships one standard Bergamo field-service dataset and prepares full
78
+ routing data when a job is created so snapshots can expose per-segment geometry.
79
 
80
  ## Quick Start
81
 
82
+ ```sh
83
+ make run-release
84
+ ```
85
+
86
+ Then open `http://localhost:7860`.
87
 
88
+ To inspect the command surface:
89
+
90
+ ```sh
91
+ make help
92
  ```
93
 
94
  ## Development
95
 
96
+ ```sh
97
  # Add a new constraint
98
  solverforge generate constraint my_rule --unary --hard
99
 
 
116
  | `src/solver/` | Solver service and configuration |
117
  | `src/api/` | HTTP routes and DTOs |
118
  | `src/data/` | Data loading and generation |
119
+ | `static/` | Browser UI, SolverForge UI config, and generated UI model |
120
+ | `Dockerfile` | Multi-stage Rust 1.95 Alpine build for the Hugging Face Docker Space |
121
+ | `Makefile` | Local development, validation, and Space image commands |
122
  | `solverforge.app.toml` | Scaffolded app/domain contract |
123
  | `solver.toml` | Solver configuration (termination, phases) |
124
+
125
+ ## REST API
126
+
127
+ - `GET /health`
128
+ - `GET /info`
129
+ - `GET /demo-data`
130
+ - `GET /demo-data/{id}`
131
+ - `POST /jobs`
132
+ - `GET /jobs/{id}`
133
+ - `GET /jobs/{id}/status`
134
+ - `GET /jobs/{id}/snapshot`
135
+ - `GET /jobs/{id}/analysis`
136
+ - `GET /jobs/{id}/routes`
137
+ - `POST /jobs/{id}/pause`
138
+ - `POST /jobs/{id}/resume`
139
+ - `POST /jobs/{id}/cancel`
140
+ - `DELETE /jobs/{id}`
141
+ - `GET /jobs/{id}/events`
142
+
143
+ Route geometry is returned by `/jobs/{id}/routes` with segment-level geometry
144
+ status. Unreachable road-network legs are reported as non-routed segments, so
145
+ one infeasible leg does not hide the rest of the snapshot geometry.
146
+
147
+ ## Validation
148
+
149
+ Standard validation:
150
+
151
+ ```sh
152
+ make test
153
+ ```
154
+
155
+ Full local Space validation:
156
+
157
+ ```sh
158
+ make ci-local
159
+ ```