Optimal resource scheduling over a rolling horizon, powered by CP-SAT — equipment, staff, or both
Project description
Optimal Task Planner
Optimal resource scheduling over a rolling horizon, powered by Google OR-Tools CP-SAT.
You describe your resource pool — equipment, staff, or both — your tasks and their constraints, and Optimal Task Planner computes a provably optimal schedule and shows it as an interactive Gantt chart. Everything runs locally: a small FastAPI backend plus a dependency-free vanilla-JS frontend.
Features
- Optimal, not heuristic — CP-SAT minimises makespan, then maximises preferred-slot usage, then respects task priority (lexicographic objective).
- 30-minute resolution over a rolling horizon (14 days by default, configurable).
- Rich task constraints — duration, work-hours-only, continue-on-next-day splitting, hard deadlines, earliest and pinned starts, task dependencies, per-slot preferred/unavailable painting, drag-to-reorder priorities.
- Re-planning aware — mark tasks done or in-progress: the solver drops finished work and freezes running tasks on their current units and times while re-planning the rest. When nothing fits, the solver returns ranked hints naming which constraints to relax.
- Responsive at scale — solves run as cancellable background jobs with live progress (elapsed time and best makespan so far); the conflict model is pruned so large projects stay solvable. Time limit, parallel workers and horizon length are per-project settings.
- Resource pool with per-unit availability — equipment (e.g.
VSG-1) or people (e.g. a named technician): mark a unit unavailable (maintenance, leave, a booking) and the solver will never assign it during that window. Units can carry custom names (serial numbers, brands, or a person's name) instead of automatic numbering. - Configurable working calendar — work start/end times in 30-minute steps, plus public
holidays: pick dates manually or auto-fill any country's official holidays
(via the
holidayspackage). - Full-screen schedule view — zoomable SVG Gantt with rich hover tooltips, a start/end details table, and a one-click export to a self-contained interactive HTML report that keeps the same zoom.
- Shareable read-only link — publish the current schedule to a stable
/share/<token>URL that anyone who can reach the server can open (view-only, no editing UI). Republish after a re-solve to update the same link, unpublish any time. To share beyond your own machine, start the server with--host 0.0.0.0. - Insights — a reporting tab derived from the solved schedule: KPI tiles (makespan, late tasks, average utilisation, busiest unit), per-unit and per-type utilisation bars, a units×days load heatmap, and bottleneck/deadline callouts.
- Bilingual UI with dark mode — English and Turkish out of the box; adding a language is one JSON file. Keyboard- and touch-friendly (focus traps, ARIA roles, pointer-event painting).
- Multiple projects with data safety — switch between named projects from the header, export/import them as JSON, undo/redo any change (Ctrl+Z/Y), and restore automatic backup snapshots. Data files are schema-versioned and migrate forward automatically.
- Zero database — every project is one human-readable JSON file under
data/projects/.
Tour
Define your resource pool and tasks, hit Solve schedule and watch the Gantt update live as CP-SAT finds better and better schedules ("best so far 286 h" → 94 h), explore the result with hover tooltips, check utilisation in Insights, and publish a read-only share link for your team.
More screenshots — Tasks, Resources, Insights, dark mode, and the shared schedule page
| Tasks — constraints & slot painting | Resources — pool, calendar & availability |
|---|---|
| Insights — utilisation & bottlenecks | Dark mode |
|---|---|
The published read-only page a share link opens — no editing UI, just the plan:
Quick start
Python users — install from PyPI (or run without installing via uv):
pip install optimal-task-planner # or: pipx install optimal-task-planner
optimal-task-planner
uvx optimal-task-planner # zero-install one-liner
The server starts, prints its data directory, and opens the UI in your browser (http://127.0.0.1:8000). A sample project is created on first run.
Docker — run it as a small (LAN) server:
docker run -d -p 8000:8000 -v otp-data:/data ghcr.io/mavrikant/optimal-task-planner
or use the docker-compose.yml in this repo: docker compose up -d.
Projects persist in the /data volume.
Windows, no Python — download optimal-task-planner-X.Y.Z-windows-x64.exe
from the latest release
and double-click it. A console window shows the server log and the UI opens in
your browser. (The executable is unsigned, so SmartScreen may warn on first
run — choose "More info" → "Run anyway".)
From a source checkout (development):
python -m venv .venv
# Windows: .venv\Scripts\activate Linux/macOS: source .venv/bin/activate
pip install -e .
optimal-task-planner
Usage
- Resources tab — define your resource types (equipment, people, or both) and unit counts, set working hours, add public holidays, and paint per-unit unavailability windows.
- Tasks tab — add tasks, set durations and constraints, paint preferred/unavailable slots, and drag tasks to set priority (top = most important).
- Press Solve schedule — the schedule opens full-screen with a Gantt chart per physical unit, hover tooltips, a details table and an Export HTML button.
Configuration
Server-level settings come from CLI flags or environment variables:
| CLI flag | Environment variable | Default | Description |
|---|---|---|---|
--host |
OPTIMAL_TASK_PLANNER_HOST |
127.0.0.1 |
Bind address |
--port |
OPTIMAL_TASK_PLANNER_PORT |
8000 |
Port |
--data-dir |
OPTIMAL_TASK_PLANNER_DATA_DIR |
(see below) | Where projects & backups live |
--days |
OPTIMAL_TASK_PLANNER_DAYS |
14 |
Default horizon length for new projects |
--no-browser |
OPTIMAL_TASK_PLANNER_NO_BROWSER |
off | Don't open the UI in a browser on startup |
| — | OPTIMAL_TASK_PLANNER_SOLVER_TIME_LIMIT |
20 |
Default CP-SAT time limit for new projects (seconds) |
The default data directory is the per-user platform data dir —
%LOCALAPPDATA%\optimal-task-planner (Windows),
~/.local/share/optimal-task-planner (Linux),
~/Library/Application Support/optimal-task-planner (macOS) — unless a
./data directory already exists in the working directory (the pre-0.2
default), which then takes precedence. The resolved path is printed at
startup.
Horizon length, CP-SAT time limit and parallel workers are project settings (gear icon next to Solve); new projects inherit the CLI/env defaults above. Working hours and holidays are project settings too — everything lives in each project's JSON file.
REST API
The UI talks to a small JSON API you can also use directly
(interactive docs at /docs):
| Method & path | Description |
|---|---|
GET /api/projects |
List projects (id, name, updated) |
POST /api/projects |
Create a project {name} |
POST /api/projects/import |
Import a full project JSON as a new project |
PATCH /api/projects/{id} |
Rename {name} |
DELETE /api/projects/{id} |
Delete (a final backup snapshot is kept) |
POST /api/projects/{id}/duplicate |
Duplicate |
GET /api/projects/{id} |
Project data + horizon info |
PUT /api/projects/{id} |
Replace project data (validated) |
POST /api/projects/{id}/solve |
Start a background solve, returns {job_id} |
GET /api/solve/{job_id} |
Solve status, progress and result when done |
POST /api/solve/{job_id}/cancel |
Cancel a running solve (keeps best found) |
POST /api/projects/{id}/share |
Publish the schedule page, returns {token, path} |
DELETE /api/projects/{id}/share |
Unpublish the schedule page |
GET /share/{token} |
The published read-only schedule page |
GET /api/projects/{id}/backups |
List automatic backup snapshots |
POST /api/projects/{id}/backups/{name}/restore |
Restore a snapshot |
GET /api/holidays/countries |
Countries supported for holiday auto-fill |
GET /api/holidays?country=TR&year=2026 |
Official holidays for a country/year |
GET /api/health |
Liveness + version |
How it works
For every task the solver enumerates all feasible start slots together with the exact set of 30-minute slots each start would occupy — this cleanly models work-hours-only tasks and next-day continuation splits. CP-SAT then picks one start per task and assigns physical units such that no unit is double-booked and no unit is used during its unavailability windows. The objective is lexicographic through weighting:
- minimise makespan,
- maximise preferred-slot usage,
- schedule higher-priority tasks earlier.
See src/optimal_task_planner/solver.py — the solver is a pure function
of the project data and an injected now timestamp, which keeps it fully unit-testable.
Development
pip install -e .[dev]
ruff check . # lint
ruff format --check . # formatting
mypy # type check
pytest # tests
optimal-task-planner --reload
Contributions welcome — see CONTRIBUTING.md. If you're an AI coding agent (or configuring one), start with AGENTS.md for the repo map, conventions and commands. Cutting a release? See RELEASING.md.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file optimal_task_planner-0.2.0.tar.gz.
File metadata
- Download URL: optimal_task_planner-0.2.0.tar.gz
- Upload date:
- Size: 448.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6accaafafe8ce1c3a106ba73effa5a443e7cd5f260fe3b07f6ceebd47bdf13e0
|
|
| MD5 |
e528356989afda70bce91d53c1ec62a6
|
|
| BLAKE2b-256 |
6c3540bbeaa52396917da8d353b6bae76ac407cb76479ea3dc562fe25676924c
|
Provenance
The following attestation bundles were made for optimal_task_planner-0.2.0.tar.gz:
Publisher:
release.yml on Mavrikant/OptimalTaskPlanner
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
optimal_task_planner-0.2.0.tar.gz -
Subject digest:
6accaafafe8ce1c3a106ba73effa5a443e7cd5f260fe3b07f6ceebd47bdf13e0 - Sigstore transparency entry: 2154275630
- Sigstore integration time:
-
Permalink:
Mavrikant/OptimalTaskPlanner@afac5afb3d0e234c03c226a4c85caa32065c5119 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Mavrikant
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@afac5afb3d0e234c03c226a4c85caa32065c5119 -
Trigger Event:
push
-
Statement type:
File details
Details for the file optimal_task_planner-0.2.0-py3-none-any.whl.
File metadata
- Download URL: optimal_task_planner-0.2.0-py3-none-any.whl
- Upload date:
- Size: 441.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2f99b94d2a8a990340a6d1ea4dc86f4c533f9de258683a5d4b732db28febd388
|
|
| MD5 |
2f12456d66e9233ef9807864edce170f
|
|
| BLAKE2b-256 |
39f9621c8a8d9d6cafd0f920b3c0176e1adecd80cff74e1bde81b862aa51f591
|
Provenance
The following attestation bundles were made for optimal_task_planner-0.2.0-py3-none-any.whl:
Publisher:
release.yml on Mavrikant/OptimalTaskPlanner
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
optimal_task_planner-0.2.0-py3-none-any.whl -
Subject digest:
2f99b94d2a8a990340a6d1ea4dc86f4c533f9de258683a5d4b732db28febd388 - Sigstore transparency entry: 2154275641
- Sigstore integration time:
-
Permalink:
Mavrikant/OptimalTaskPlanner@afac5afb3d0e234c03c226a4c85caa32065c5119 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Mavrikant
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@afac5afb3d0e234c03c226a4c85caa32065c5119 -
Trigger Event:
push
-
Statement type: