Spaces:
Sleeping
feat(deploy): make fsr Docker Space ready
Browse filesAdd 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.
- .dockerignore +9 -0
- .gitignore +2 -0
- Dockerfile +36 -0
- Makefile +184 -0
- README.md +118 -12
|
@@ -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
|
|
@@ -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
|
|
@@ -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"]
|
|
@@ -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)"
|
|
@@ -1,32 +1,99 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
## Versioning
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
| 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.
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
## Quick Start
|
| 18 |
|
| 19 |
-
```
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
| 25 |
```
|
| 26 |
|
| 27 |
## Development
|
| 28 |
|
| 29 |
-
```
|
| 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 |
+
```
|