duplix 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. duplix-0.1.0/.gitignore +8 -0
  2. duplix-0.1.0/LICENSE +21 -0
  3. duplix-0.1.0/PKG-INFO +61 -0
  4. duplix-0.1.0/README.md +40 -0
  5. duplix-0.1.0/pyproject.toml +42 -0
  6. duplix-0.1.0/src/duplix/__init__.py +7 -0
  7. duplix-0.1.0/src/duplix/__main__.py +62 -0
  8. duplix-0.1.0/src/duplix/_template/.gitignore +3 -0
  9. duplix-0.1.0/src/duplix/_template/README.md +154 -0
  10. duplix-0.1.0/src/duplix/_template/configs/config.yml +256 -0
  11. duplix-0.1.0/src/duplix/_template/configs/shift_limits.json +28 -0
  12. duplix-0.1.0/src/duplix/_template/pyproject.toml +22 -0
  13. duplix-0.1.0/src/duplix/_template/requirements.txt +17 -0
  14. duplix-0.1.0/src/duplix/_template/src/__init__.py +0 -0
  15. duplix-0.1.0/src/duplix/_template/src/allocator/__init__.py +6 -0
  16. duplix-0.1.0/src/duplix/_template/src/allocator/caps.py +418 -0
  17. duplix-0.1.0/src/duplix/_template/src/allocator/eligibility.py +358 -0
  18. duplix-0.1.0/src/duplix/_template/src/allocator/pair_validation.py +108 -0
  19. duplix-0.1.0/src/duplix/_template/src/allocator/pairings.py +554 -0
  20. duplix-0.1.0/src/duplix/_template/src/allocator/postpass_intl.py +502 -0
  21. duplix-0.1.0/src/duplix/_template/src/allocator/postpass_p2f.py +731 -0
  22. duplix-0.1.0/src/duplix/_template/src/allocator/postsolve.py +589 -0
  23. duplix-0.1.0/src/duplix/_template/src/allocator/recommender.py +362 -0
  24. duplix-0.1.0/src/duplix/_template/src/allocator/windows.py +353 -0
  25. duplix-0.1.0/src/duplix/_template/src/cli.py +41 -0
  26. duplix-0.1.0/src/duplix/_template/src/config.py +147 -0
  27. duplix-0.1.0/src/duplix/_template/src/io/__init__.py +0 -0
  28. duplix-0.1.0/src/duplix/_template/src/io/export.py +272 -0
  29. duplix-0.1.0/src/duplix/_template/src/io/readers.py +884 -0
  30. duplix-0.1.0/src/duplix/_template/src/plan.py +200 -0
  31. duplix-0.1.0/src/duplix/_template/src/recommender_staffing.py +329 -0
  32. duplix-0.1.0/src/duplix/_template/src/schemas.py +1243 -0
  33. duplix-0.1.0/src/duplix/_template/src/solver/__init__.py +0 -0
  34. duplix-0.1.0/src/duplix/_template/src/solver/allocator_cpsat.py +1250 -0
  35. duplix-0.1.0/src/duplix/_template/src/staged_overrides.py +315 -0
  36. duplix-0.1.0/src/duplix/_template/src/state.py +291 -0
  37. duplix-0.1.0/src/duplix/_template/src/step1_clean_flights.py +301 -0
  38. duplix-0.1.0/src/duplix/_template/src/step2_extract_roster.py +322 -0
  39. duplix-0.1.0/src/duplix/_template/src/step3_allocate_flights.py +1617 -0
  40. duplix-0.1.0/src/duplix/_template/src/web/__init__.py +47 -0
  41. duplix-0.1.0/src/duplix/_template/src/web/__main__.py +9 -0
  42. duplix-0.1.0/src/duplix/_template/src/web/api/__init__.py +53 -0
  43. duplix-0.1.0/src/duplix/_template/src/web/api/export.py +27 -0
  44. duplix-0.1.0/src/duplix/_template/src/web/api/inputs.py +62 -0
  45. duplix-0.1.0/src/duplix/_template/src/web/api/override_rows.py +138 -0
  46. duplix-0.1.0/src/duplix/_template/src/web/api/pages.py +30 -0
  47. duplix-0.1.0/src/duplix/_template/src/web/api/readbacks.py +71 -0
  48. duplix-0.1.0/src/duplix/_template/src/web/api/recommender.py +72 -0
  49. duplix-0.1.0/src/duplix/_template/src/web/api/runs.py +78 -0
  50. duplix-0.1.0/src/duplix/_template/src/web/api/settings.py +103 -0
  51. duplix-0.1.0/src/duplix/_template/src/web/core/__init__.py +5 -0
  52. duplix-0.1.0/src/duplix/_template/src/web/core/responses.py +89 -0
  53. duplix-0.1.0/src/duplix/_template/src/web/core/router.py +167 -0
  54. duplix-0.1.0/src/duplix/_template/src/web/core/static_files.py +85 -0
  55. duplix-0.1.0/src/duplix/_template/src/web/overrides/__init__.py +58 -0
  56. duplix-0.1.0/src/duplix/_template/src/web/overrides/airports.py +181 -0
  57. duplix-0.1.0/src/duplix/_template/src/web/overrides/config_yaml.py +21 -0
  58. duplix-0.1.0/src/duplix/_template/src/web/overrides/filters.py +187 -0
  59. duplix-0.1.0/src/duplix/_template/src/web/overrides/rows.py +110 -0
  60. duplix-0.1.0/src/duplix/_template/src/web/readback/__init__.py +65 -0
  61. duplix-0.1.0/src/duplix/_template/src/web/readback/common.py +68 -0
  62. duplix-0.1.0/src/duplix/_template/src/web/readback/dashboard.py +86 -0
  63. duplix-0.1.0/src/duplix/_template/src/web/readback/planning.py +242 -0
  64. duplix-0.1.0/src/duplix/_template/src/web/readback/session.py +157 -0
  65. duplix-0.1.0/src/duplix/_template/src/web/readback/tables.py +163 -0
  66. duplix-0.1.0/src/duplix/_template/src/web/runner.py +168 -0
  67. duplix-0.1.0/src/duplix/_template/src/web/server.py +180 -0
  68. duplix-0.1.0/src/duplix/_template/src/web/static/css/base.css +17 -0
  69. duplix-0.1.0/src/duplix/_template/src/web/static/css/components/cards.css +166 -0
  70. duplix-0.1.0/src/duplix/_template/src/web/static/css/components/drawer.css +80 -0
  71. duplix-0.1.0/src/duplix/_template/src/web/static/css/components/filters.css +40 -0
  72. duplix-0.1.0/src/duplix/_template/src/web/static/css/components/forms.css +69 -0
  73. duplix-0.1.0/src/duplix/_template/src/web/static/css/components/modal.css +98 -0
  74. duplix-0.1.0/src/duplix/_template/src/web/static/css/components/run-pill.css +16 -0
  75. duplix-0.1.0/src/duplix/_template/src/web/static/css/components/run-status.css +31 -0
  76. duplix-0.1.0/src/duplix/_template/src/web/static/css/components/tables.css +58 -0
  77. duplix-0.1.0/src/duplix/_template/src/web/static/css/features/allocations.css +16 -0
  78. duplix-0.1.0/src/duplix/_template/src/web/static/css/features/chart-pie.css +29 -0
  79. duplix-0.1.0/src/duplix/_template/src/web/static/css/features/chart-shift-summary.css +26 -0
  80. duplix-0.1.0/src/duplix/_template/src/web/static/css/features/chart-trend.css +161 -0
  81. duplix-0.1.0/src/duplix/_template/src/web/static/css/features/chart-workload-dist.css +57 -0
  82. duplix-0.1.0/src/duplix/_template/src/web/static/css/features/coverage.css +48 -0
  83. duplix-0.1.0/src/duplix/_template/src/web/static/css/features/dashboard.css +163 -0
  84. duplix-0.1.0/src/duplix/_template/src/web/static/css/features/inputs.css +80 -0
  85. duplix-0.1.0/src/duplix/_template/src/web/static/css/features/recommender.css +53 -0
  86. duplix-0.1.0/src/duplix/_template/src/web/static/css/features/roster.css +58 -0
  87. duplix-0.1.0/src/duplix/_template/src/web/static/css/features/setup-sidebar.css +42 -0
  88. duplix-0.1.0/src/duplix/_template/src/web/static/css/features/shifts.css +21 -0
  89. duplix-0.1.0/src/duplix/_template/src/web/static/css/features/unallocated.css +65 -0
  90. duplix-0.1.0/src/duplix/_template/src/web/static/css/features/workload.css +20 -0
  91. duplix-0.1.0/src/duplix/_template/src/web/static/css/layout.css +189 -0
  92. duplix-0.1.0/src/duplix/_template/src/web/static/css/tokens.css +35 -0
  93. duplix-0.1.0/src/duplix/_template/src/web/static/index.html +66 -0
  94. duplix-0.1.0/src/duplix/_template/src/web/static/js/charts/flight-trend.js +329 -0
  95. duplix-0.1.0/src/duplix/_template/src/web/static/js/charts/flights-pie.js +78 -0
  96. duplix-0.1.0/src/duplix/_template/src/web/static/js/charts/roster.js +70 -0
  97. duplix-0.1.0/src/duplix/_template/src/web/static/js/charts/shift-summary.js +102 -0
  98. duplix-0.1.0/src/duplix/_template/src/web/static/js/charts/workload-dist.js +82 -0
  99. duplix-0.1.0/src/duplix/_template/src/web/static/js/core/api.js +82 -0
  100. duplix-0.1.0/src/duplix/_template/src/web/static/js/core/constants.js +48 -0
  101. duplix-0.1.0/src/duplix/_template/src/web/static/js/core/dom.js +36 -0
  102. duplix-0.1.0/src/duplix/_template/src/web/static/js/core/store.js +142 -0
  103. duplix-0.1.0/src/duplix/_template/src/web/static/js/drawer/airports.js +74 -0
  104. duplix-0.1.0/src/duplix/_template/src/web/static/js/drawer/index.js +143 -0
  105. duplix-0.1.0/src/duplix/_template/src/web/static/js/drawer/rows.js +294 -0
  106. duplix-0.1.0/src/duplix/_template/src/web/static/js/drawer/shift-limits.js +132 -0
  107. duplix-0.1.0/src/duplix/_template/src/web/static/js/drawer/staged-forms.js +222 -0
  108. duplix-0.1.0/src/duplix/_template/src/web/static/js/main.js +97 -0
  109. duplix-0.1.0/src/duplix/_template/src/web/static/js/panels/date-control.js +94 -0
  110. duplix-0.1.0/src/duplix/_template/src/web/static/js/panels/handlers.js +100 -0
  111. duplix-0.1.0/src/duplix/_template/src/web/static/js/panels/input-slots.js +156 -0
  112. duplix-0.1.0/src/duplix/_template/src/web/static/js/panels/inputs.js +27 -0
  113. duplix-0.1.0/src/duplix/_template/src/web/static/js/panels/plan.js +54 -0
  114. duplix-0.1.0/src/duplix/_template/src/web/static/js/panels/run.js +175 -0
  115. duplix-0.1.0/src/duplix/_template/src/web/static/js/panels/staffing.js +68 -0
  116. duplix-0.1.0/src/duplix/_template/src/web/static/js/panels/zc-selector.js +77 -0
  117. duplix-0.1.0/src/duplix/_template/src/web/static/js/sidebar/index.js +88 -0
  118. duplix-0.1.0/src/duplix/_template/src/web/static/js/ui/confirm.js +64 -0
  119. duplix-0.1.0/src/duplix/_template/src/web/static/js/ui/layout.js +70 -0
  120. duplix-0.1.0/src/duplix/_template/src/web/static/js/ui/nominate.js +86 -0
  121. duplix-0.1.0/src/duplix/_template/src/web/static/js/ui/tabs.js +57 -0
  122. duplix-0.1.0/src/duplix/_template/src/web/static/js/views/allocations.js +167 -0
  123. duplix-0.1.0/src/duplix/_template/src/web/static/js/views/dashboard.js +227 -0
  124. duplix-0.1.0/src/duplix/_template/src/web/static/js/views/index.js +32 -0
  125. duplix-0.1.0/src/duplix/_template/src/web/static/js/views/pairs.js +109 -0
  126. duplix-0.1.0/src/duplix/_template/src/web/static/js/views/unallocated.js +280 -0
  127. duplix-0.1.0/src/duplix/_template/src/web/static/js/views/warnings.js +72 -0
  128. duplix-0.1.0/src/duplix/_template/src/web/static/js/views/workload.js +117 -0
  129. duplix-0.1.0/src/duplix/_template/src/web/static/styles.css +42 -0
  130. duplix-0.1.0/src/duplix/_template/start.bat +109 -0
  131. duplix-0.1.0/src/duplix/core.py +6 -0
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
8
+ *.egg
duplix-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bhishan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
duplix-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.5
2
+ Name: duplix
3
+ Version: 0.1.0
4
+ Summary: Scaffold a new project by copying a bundled template project.
5
+ Project-URL: Homepage, https://github.com/BhishanSharma/duplix
6
+ Project-URL: Issues, https://github.com/BhishanSharma/duplix/issues
7
+ Author-email: Bhishan <you@example.com>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: cli,scaffold,template
11
+ Classifier: Environment :: Console
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Requires-Python: >=3.8
16
+ Provides-Extra: dev
17
+ Requires-Dist: build; extra == 'dev'
18
+ Requires-Dist: pytest>=7.0; extra == 'dev'
19
+ Requires-Dist: twine; extra == 'dev'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # duplix
23
+
24
+ Scaffold a new project by copying a bundled template project, exactly as it is.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install duplix
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```bash
35
+ python -m duplix my_new_project
36
+ # or, after install:
37
+ duplix my_new_project
38
+ ```
39
+
40
+ This copies duplix's bundled template folder verbatim into `./my_new_project/`
41
+ (files, code, and structure unchanged - nothing is renamed or rewritten).
42
+
43
+ ### Options
44
+
45
+ ```bash
46
+ python -m duplix my_new_project --dir ./somewhere
47
+ ```
48
+
49
+ ## Development
50
+
51
+ ```bash
52
+ pip install -e ".[dev]"
53
+ pytest
54
+ ```
55
+
56
+ ## Publishing
57
+
58
+ ```bash
59
+ python -m build
60
+ python -m twine upload dist/*
61
+ ```
duplix-0.1.0/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # duplix
2
+
3
+ Scaffold a new project by copying a bundled template project, exactly as it is.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install duplix
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ python -m duplix my_new_project
15
+ # or, after install:
16
+ duplix my_new_project
17
+ ```
18
+
19
+ This copies duplix's bundled template folder verbatim into `./my_new_project/`
20
+ (files, code, and structure unchanged - nothing is renamed or rewritten).
21
+
22
+ ### Options
23
+
24
+ ```bash
25
+ python -m duplix my_new_project --dir ./somewhere
26
+ ```
27
+
28
+ ## Development
29
+
30
+ ```bash
31
+ pip install -e ".[dev]"
32
+ pytest
33
+ ```
34
+
35
+ ## Publishing
36
+
37
+ ```bash
38
+ python -m build
39
+ python -m twine upload dist/*
40
+ ```
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "duplix"
7
+ version = "0.1.0"
8
+ description = "Scaffold a new project by copying a bundled template project."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Bhishan", email = "you@example.com" }
14
+ ]
15
+ keywords = ["scaffold", "template", "cli"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Environment :: Console",
21
+ ]
22
+ dependencies = []
23
+
24
+ [project.optional-dependencies]
25
+ dev = [
26
+ "pytest>=7.0",
27
+ "build",
28
+ "twine",
29
+ ]
30
+
31
+ [project.scripts]
32
+ duplix = "duplix.__main__:main"
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/BhishanSharma/duplix"
36
+ Issues = "https://github.com/BhishanSharma/duplix/issues"
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["src/duplix"]
40
+
41
+ [tool.hatch.build.targets.sdist]
42
+ include = ["src/duplix/**", "README.md", "LICENSE"]
@@ -0,0 +1,7 @@
1
+ """duplix - scaffold a new project by copying a bundled template project.
2
+
3
+ Usage:
4
+ python -m duplix <project_name>
5
+ """
6
+
7
+ __version__ = "0.1.0"
@@ -0,0 +1,62 @@
1
+ """Command-line entry point for duplix.
2
+
3
+ Running `python -m duplix <project_name>` copies duplix's bundled template
4
+ project, as-is, into a new folder named <project_name> in the current
5
+ directory (or --dir). No files are renamed or rewritten - it's a plain
6
+ copy of the template's files and code.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import importlib.resources
13
+ import shutil
14
+ import sys
15
+ from pathlib import Path
16
+
17
+
18
+ def get_template_dir() -> Path:
19
+ """Locate the bundled template directory inside the installed package."""
20
+ template = importlib.resources.files("duplix").joinpath("_template")
21
+ return Path(str(template))
22
+
23
+
24
+ def scaffold(project_name: str, target_dir: Path) -> Path:
25
+ dest = target_dir / project_name
26
+ if dest.exists():
27
+ raise FileExistsError(f"'{dest}' already exists.")
28
+
29
+ template_dir = get_template_dir()
30
+ if not template_dir.exists():
31
+ raise FileNotFoundError(
32
+ f"Bundled template not found at '{template_dir}'. "
33
+ "The duplix package may be installed incorrectly."
34
+ )
35
+
36
+ shutil.copytree(template_dir, dest)
37
+ return dest
38
+
39
+
40
+ def main(argv: list[str] | None = None) -> int:
41
+ parser = argparse.ArgumentParser(
42
+ prog="duplix",
43
+ description="Copy duplix's bundled template project into a new folder.",
44
+ )
45
+ parser.add_argument("project_name", help="Name of the new project folder to create")
46
+ parser.add_argument(
47
+ "-d", "--dir", default=".", help="Directory to create the project in (default: current dir)"
48
+ )
49
+ args = parser.parse_args(argv)
50
+
51
+ try:
52
+ dest = scaffold(args.project_name, Path(args.dir).resolve())
53
+ except (FileExistsError, FileNotFoundError) as exc:
54
+ print(f"Error: {exc}", file=sys.stderr)
55
+ return 1
56
+
57
+ print(f"Copied template to: {dest}")
58
+ return 0
59
+
60
+
61
+ if __name__ == "__main__":
62
+ raise SystemExit(main())
@@ -0,0 +1,3 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
@@ -0,0 +1,154 @@
1
+ # Flight Allocation
2
+
3
+ Engine + web console for IndiGo CLC flight allocation. Python 3.11–3.14,
4
+ no database, no Docker, no Node — the UI is a zero-dependency stdlib
5
+ `http.server` app on `127.0.0.1:8765` with plain HTML/CSS/JS under
6
+ `src/web/static/`.
7
+
8
+ **Everything goes in and comes out through the browser.** The three
9
+ input files are uploaded in the browser and held in the server's memory
10
+ for the session; results are read back as JSON and, if you need a file
11
+ to send on, downloaded as a one-off `.xlsx`. There is no console
12
+ workbook, no `excel/` folder to manage, and no per-date file tree.
13
+
14
+ The files are split by how often they change. The flight schedule is a
15
+ fresh export every morning, so it sits on the dashboard. The two rosters
16
+ cover a whole period and are set once, in the **Setup** sidebar.
17
+
18
+ ## Quick start (Windows)
19
+
20
+ 1. Double-click `start.bat`.
21
+ 2. First run takes ~1 minute: it creates a `.venv` and installs
22
+ `requirements.txt`. Every run after that opens the browser straight
23
+ to http://127.0.0.1:8765/.
24
+
25
+ ## Manual setup (Mac/Linux, or to run it yourself)
26
+
27
+ ```
28
+ python3.11 -m venv .venv
29
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
30
+ pip install -r requirements.txt
31
+ python -m src.cli # --port 9000 --no-browser also work
32
+ ```
33
+
34
+ ## Daily flow
35
+
36
+ 0. **Once per period** — open **Setup** (top right) and upload the two
37
+ roster files. They stay loaded for the session, so this is not part
38
+ of the daily flow:
39
+
40
+ | Slot | What it is |
41
+ |---|---|
42
+ | Staff roster | regular crew roster |
43
+ | AM + ZC roster | AM and ZC roster |
44
+
45
+ 1. **Upload** today's flight schedule (the SV portal export) on the
46
+ dashboard's *Today's flight schedule* card.
47
+
48
+ Any sheet name works — each reader takes its canonical sheet if
49
+ present and otherwise falls back to the first sheet in the file.
50
+
51
+ 2. Check the allocation date (top right). It is prefilled from the
52
+ **server's** date, not the browser's, so a machine on a stale clock
53
+ can't plan the wrong day. Change it to back-fill another date.
54
+ 3. Click **Plan** — cleans the flights and extracts the roster, ~3s.
55
+ 4. Review the dashboard: per-shift stats, charts, roster by shift,
56
+ staffing recommendation.
57
+ 5. Click **Allocate** — CP-SAT solves, typically 30–90s on a first run.
58
+ 6. Adjust anything that needs it from the **Override** drawer:
59
+ - `p2f` / `norse` — handler nominations
60
+ - `sick` — pull someone out of the day
61
+ - `change_role` — promote STAFF→ZC, or push someone to AM
62
+ - `max_flights` / `cutoff_time` — per-staff caps
63
+ - add / remove a flight or a staff member
64
+ 7. Click **Allocate** again. Prior assignments are pinned and used as a
65
+ CP-SAT warm start, so only the affected staff's flights move and the
66
+ re-solve takes seconds rather than a minute. Rows whose staff changed
67
+ are flagged in the Allocations tab.
68
+ 8. **Download .xlsx** when you want a file to email or print.
69
+
70
+ ## Session lifetime
71
+
72
+ State lives in the server process. Restarting it clears the uploads,
73
+ the overrides and the results — start again by uploading the three
74
+ files. **Reset** is narrower: it clears the results but keeps your
75
+ uploads and override rows, so you can go straight back to Plan.
76
+
77
+ ## Layout
78
+
79
+ ```
80
+ src/
81
+ state.py the in-memory store everything reads and writes
82
+ plan.py Plan stage: step 1 + step 2 + the summary
83
+ step1_clean_flights.py clean the schedule into ops classes
84
+ step2_extract_roster.py rosters -> availability
85
+ step3_allocate_flights.py solve, post-passes, outputs
86
+ staged_overrides.py apply drawer mutations to the working data
87
+ recommender_staffing.py per-shift required headcount
88
+ schemas.py the typed row models the whole app shares
89
+ allocator/ eligibility, caps, pairings, post-passes
90
+ solver/ CP-SAT model
91
+ io/
92
+ readers.py parse the uploaded workbooks + override rows
93
+ export.py render the state as a downloadable .xlsx
94
+ web/
95
+ server.py the request loop + launcher (no endpoint logic)
96
+ core/ Response, the Router, static file serving
97
+ api/ one module per concern; each registers routes
98
+ readback/ state -> the JSON the UI renders
99
+ overrides/ override + config.yml mutations
100
+ runner.py runs a stage on a background thread
101
+ static/
102
+ index.html a shell; views bring their own markup
103
+ js/core/ dom, api, store, shared constants
104
+ js/ui/ tab registry, layout mounting, modals
105
+ js/views/ one module per tab (views/index.js registers)
106
+ js/charts/ one module per dashboard chart
107
+ js/panels/ dashboard blocks with their own load cycle
108
+ js/drawer/ the Override drawer and its sections
109
+ js/sidebar/ the Setup sidebar (the set-once roster files)
110
+ css/ partials, listed in styles.css
111
+ configs/
112
+ config.yml column maps, status aliases, solver knobs
113
+ shift_limits.json per-(shift, role) preferred / acceptable / cap
114
+ ```
115
+
116
+ ```stdlib http.server web UI.
117
+
118
+ Routes (JSON unless noted):
119
+
120
+ GET / text/html — single-page app
121
+ GET /static/<path> the JS module / CSS partial tree
122
+
123
+ GET /api/inputs which of the 3 files are uploaded,
124
+ grouped daily (schedule) / setup (rosters)
125
+ POST /api/inputs/<kind> upload one (raw .xlsx body)
126
+ DEL /api/inputs/<kind> drop one
127
+ GET /api/server_date the operating date, per the server's clock
128
+ GET /api/export.xlsx download the current results
129
+
130
+ GET /api/dashboard headline counts + run status
131
+ GET /api/allocations every allocation row
132
+ GET /api/workload per-staff workload summary
133
+ GET /api/pairs pair map
134
+ GET /api/warnings warnings
135
+ GET /api/unallocated unallocated flights
136
+ GET /api/recommendations Phase-R suggestions per unallocated
137
+ GET /api/plan plan summary payload
138
+ GET /api/handlers P2F / NORSE handler picture
139
+ GET /api/staffing staffing recommendation
140
+ GET /api/staff_names roster names for the drawer dropdowns
141
+ GET /api/shift_limits current (shift, role) bands
142
+
143
+ GET /api/overrides override rows
144
+ POST /api/overrides append one {values: [...]}
145
+ PUT /api/overrides/<i> overwrite row i
146
+ DEL /api/overrides/<i> delete row i
147
+ POST /api/overrides/apply_staged apply staged rows to the data
148
+
149
+ POST /api/run {date, step} — plan | all | reset | …
150
+ GET /api/run/status current run state
151
+
152
+ Bind: 127.0.0.1:8765. Everything lives in memory for the life of the
153
+ process; there is no workbook and no per-date file.
154
+ ```
@@ -0,0 +1,256 @@
1
+ # Flight Allocation system — runtime config
2
+ # All maps live here so onboarding a slightly different export shape only
3
+ # requires editing this file, not Python source. See plan §1.5.1.
4
+
5
+ allocation:
6
+ # Cycle year used to resolve year-less roster headers ("Mon,30Mar" -> 30-Mar-<cycle_year>).
7
+ cycle_year: 2026
8
+
9
+ # Default integration-run date. The launcher and CLI take --date to override.
10
+ # Sample SV portal contains only this date; AM roster covers it; staff roster
11
+ # ends 26-Apr so STAFF availability for this D will be empty (W010).
12
+ default_date: 2026-04-30
13
+
14
+ # Step 3 solver parameters.
15
+ solver:
16
+ # 2026-05-28 (user direction — restoring distribution quality): 350s
17
+ # was too aggressive. The solver was reaching FEASIBLE early but the
18
+ # S1b bucket-spread polish phase (which equalizes per-shift staff
19
+ # counts) kept getting cut short, producing -7 / -5 / -19 outliers in
20
+ # otherwise healthy shifts. 700s gives the solver the late-phase time
21
+ # it needs to drive max-min toward 0 across each bucket.
22
+ #
23
+ # Auto-retry (3 attempts in runner.py) is the safety net when a
24
+ # single attempt leaves UNALLOCATED >= 15 — worst-case total
25
+ # wall-clock = 3 * (700s + post-pass) ~ 40 min, ~12 min nominal.
26
+ # Never set to 0 — caused CP-SAT to search indefinitely on a prior
27
+ # attempt (99 min CPU, 4.6 GB RAM).
28
+ #
29
+ # Soft objective drives equality:
30
+ # - S1 below-preferred: 100 per missing flight (pushes count up)
31
+ # - S1 above-preferred: 50 / 500 / 1500 marginal (cap at +1/+2/+3)
32
+ # - S1b bucket-spread: 600 per unit of (max - min) inside bucket
33
+ # (bumped from 400 alongside the time bump — equalization is
34
+ # more aggressive per-second so the budget pays off harder)
35
+ # Result: {23, 23} costs 100 vs {22, 24} costing 1750 — strongly
36
+ # prefers equal pairs over unequal pairs at the same sum.
37
+ max_seconds: 350
38
+ deficit_penalty_lambda: 100 # legacy knob; ignored by lex objective, kept for API compat
39
+
40
+ # State / cycle-history.
41
+ state:
42
+ staleness_max_days: 2 # plan §7.6 W003 threshold
43
+
44
+ # ---------- I/O column maps ----------
45
+ io:
46
+ sv_portal:
47
+ sheet: IN_SVportal # was "Sheet1"; renamed under IN_*-prefix convention
48
+ # 2026-05-14: SV portal export format simplified — fewer columns,
49
+ # different header names. All aliases below (FLT/Flight No., DEP/Origin,
50
+ # ARR/Dest., STD/Dep., TYPE/A/C Type, LOAD/Pax Load) map to the same
51
+ # raw-row field so the reader picks up whichever is present. STA,
52
+ # AC-subtype, and Aircraft Owner are not in the new export — flights
53
+ # default to no-NORSE-owner / blank subtype.
54
+ columns: # source-col -> raw-row field (per §4.3)
55
+ FLT: flight_id
56
+ "Flight No.": flight_id
57
+ DEP: departure
58
+ Origin: departure
59
+ ARR: arrival
60
+ "Dest.": arrival
61
+ STD: dep_time
62
+ "Dep.": dep_time
63
+ STA: arr_time
64
+ TYPE: aircraft_type
65
+ AC: aircraft_subtype
66
+ Aircraft: aircraft_subtype
67
+ # 2026-05-14 new SV portal export: "A/C Type" holds the aircraft
68
+ # MODEL number (321, 789, 320 — what the legacy export called AC).
69
+ # Map to aircraft_subtype, not aircraft_type. aircraft_type
70
+ # (the J/B/K/X ops-class letter) is absent from the new export,
71
+ # so ops_class falls through to date-based DAY/NIGHT classification.
72
+ "A/C Type": aircraft_subtype
73
+ DATE: date
74
+ Date: date
75
+ LOAD: booked_pax_raw
76
+ "Booked Pax": booked_pax_raw
77
+ "Pax Load": booked_pax_raw
78
+ "Aircraft Owner": owner
79
+ date_format: "%m/%d/%Y" # US-style m/d/Y per §1.5.1
80
+ # Step 1 drops rows whose Booked Pax does not match this regex.
81
+ pax_regex: '^\d+/\d+/\d+$'
82
+ # Phase 3 / Change 1 (2026-05-14, INTL overhaul): flights with
83
+ # DEP in this list are DROPPED at Step 1 extraction. The ARR-side
84
+ # pink-highlight machinery was removed in the same amendment;
85
+ # Gulf-3 ARR flights are plain domestic now.
86
+ routing_drop_dep_codes: [AUH, DOH, DXB]
87
+ # International airport list. Step 1 tags a flight with
88
+ # is_international=True if its **DEP** (3-letter code) is in this list
89
+ # (Change 9, 2026-05-14: DEP-side only; was DEP or ARR). Drives H12
90
+ # in Step 4 (newbies excluded) + the Change 4 D-75→STD coverage rule.
91
+ international_airport_codes:
92
+ - ATH
93
+ - ALA
94
+ - BAH
95
+ - BKK
96
+ - CAN
97
+ - CGK
98
+ - CMB
99
+ - DAC
100
+ - DMM
101
+ - DPS
102
+ - FJR
103
+ - GYD
104
+ - HAN
105
+ - HKG
106
+ - HKT
107
+ - JAF
108
+ - JED
109
+ - KBV
110
+ - KTM
111
+ - KUL
112
+ - KWI
113
+ - LGK
114
+ - MCT
115
+ - MED
116
+ - MLE
117
+ - MRU
118
+ - NBO
119
+ - PEN
120
+ - PVG
121
+ - RKT
122
+ - RUH
123
+ - SAI
124
+ - SEZ
125
+ - SGN
126
+ - SHJ
127
+ - SIN
128
+ - TAS
129
+ - TBS
130
+ - ZRH # smoke test
131
+ - GVA
132
+
133
+ staff_roster:
134
+ sheet: IN_Staff # was "Sheet1" — IN_*-prefixed naming since 2026-05-13
135
+ name_column: STAFF NAME # was "Name" — input files use STAFF NAME
136
+ # Header pattern matches "Mon,30Mar" / "Tue,1Apr".
137
+ date_header_format: "%a,%d%b"
138
+ skip_blank_columns: true # blank cols between Name and first date are dropped
139
+
140
+ am_roster:
141
+ sheet_glob: "IN_AM_Roster*" # was "AM Roster*" — IN_*-prefixed naming
142
+ name_column: STAFF NAME # was "NAME" — input files use STAFF NAME
143
+ # Header pattern matches "30-Apr".
144
+ date_header_format: "%d-%b"
145
+ # Number of leading rows after the header to skip (weekday sub-header in row 2).
146
+ subheader_rows_to_skip: 1
147
+
148
+ # ---------- Status normalization ----------
149
+ status:
150
+ # Aliases applied at read time before enum lookup.
151
+ # Note: keys are quoted so YAML 1.1 doesn't parse "OFF" / "ON" as booleans.
152
+ aliases:
153
+ "OFF": F # day off literal -> §4.1 F
154
+ # User direction 2026-05-10: future samples will write ZC entries
155
+ # as ZC/M, ZC/A, ... (current sample uses M/ZC, A/ZC). Both are
156
+ # accepted — the alias maps the new form to the canonical M/ZC.
157
+ "ZC/M": "M/ZC"
158
+ "ZC/A": "A/ZC"
159
+ "ZC/N": "N/ZC"
160
+ "ZC/M1": "M1/ZC"
161
+ "ZC/A1": "A1/ZC"
162
+
163
+ # Recognized shift codes (current_shift will be populated when status is one of these).
164
+ shifts: [M, A, N, M1, A1]
165
+
166
+ # Recognized non-shift, non-assignable statuses (already in §4.1 enum).
167
+ non_shift_recognized: [U, F, "P/L", "C/L", "C/OFF", TR, ""]
168
+
169
+ # Recognized AM/ZC shift variants.
170
+ am_shifts: ["M/IGT", "A/IGT"]
171
+ zc_shifts: ["M/ZC", "A/ZC", "N/ZC", "M1/ZC", "A1/ZC"]
172
+
173
+ # Anything not matched above maps to CrewStatus.OTHER, assignable=False.
174
+ # Common occurrences in real AM data: N/CCC, L&T TRG, *Audit suffixes.
175
+ unrecognized_treatment: OTHER
176
+
177
+ # ---------- Aircraft type -> ops_class lookup ----------
178
+ # Per §4.13: day/night split is by date; other ops_classes (p2f, ferry, norse, test)
179
+ # are derived from aircraft_type. Initial mapping below is a placeholder seeded from
180
+ # the sample's TYPE values (G, H, J, K, P, T, X) — refine as user confirms semantics.
181
+ # Any TYPE not listed falls through to date-based day/night routing.
182
+ ops_class_by_aircraft_type:
183
+ # 2026-05-11 (revised per user): TYPE-letter routing rules.
184
+ # H → P2F (passenger-to-freighter)
185
+ # J → date-based DAY/NIGHT (passenger normal)
186
+ # X → handled in code: dep==arr → TEST, dep!=arr → FERRY
187
+ # B → CHARTER (charter flight, own class)
188
+ # K / P / T / G → FERRY (legacy ferry markers)
189
+ # TEST / FERRY / CHARTER are now allocated as normal flights and
190
+ # counted under workload (no per-staff special cap). The cleaned
191
+ # output groups them on one SpecialOps group for visibility.
192
+ H: p2f
193
+ B: charter
194
+ K: ferry
195
+ P: ferry
196
+ T: ferry
197
+ G: ferry
198
+ # 2026-05-27 (user direction): TYPE=W flights route to special ops
199
+ # (the FERRY group). Previously they fell
200
+ # through to date-based DAY/NIGHT routing and ended up in DayOps.
201
+ W: ferry
202
+
203
+ # Post-pass §3 (user direction 2026-05-12) — when a handler has a P2F
204
+ # flight, the surrounding domestic flights get removed from their list
205
+ # (briefing / paperwork time) and redistributed via §4. Tolerance is
206
+ # the half-width of the matching window around each anchor (D-3h,
207
+ # D-1h, D+20min).
208
+ #
209
+ # 2026-05-28: ``logic_v2`` switches to the union-window-per-chain rule.
210
+ # For each P2F at STD=T, the {owner P2F handler, pre-planner P2F
211
+ # handler} chain loses ALL flights with STD in [T-2h, T+1h]. The P2F
212
+ # post-pass also runs BEFORE the INTL post-pass when this flag is on,
213
+ # so INTL D-75 doesn't remove a flight the P2F window would already
214
+ # free up. Default false in main builds; the "p2f-logic" variant zip
215
+ # ships with this set true.
216
+ p2f_adjustment:
217
+ tolerance_minutes: 30
218
+ logic_v2: false
219
+
220
+ ops_class_norse_owner_codes:
221
+ # NORSE flights are identified by the Aircraft Owner column (per user
222
+ # direction 2026-05-11). Case-insensitive equality match against any
223
+ # code below routes the flight to NORSE. Replaces the legacy
224
+ # (TYPE, Aircraft) compound rule — owners are explicit in the new
225
+ # export, so we no longer triangulate via type+subtype.
226
+ - N0
227
+ - AC-789
228
+
229
+ # ---------- GULF routing (2026-05-19) ----------
230
+ # A flight is GULF when its DEP airport is in the list below OR its
231
+ # Aircraft Owner / TYPE letter equals one of the listed carrier codes
232
+ # (case-insensitive). GULF flights land on the dedicated
233
+ # GULF class so the assigner can see them for reference,
234
+ # but Step 3's allocator does NOT read that sheet — they are never
235
+ # assigned to any staff.
236
+ ops_class_gulf_dep_codes:
237
+ - AUH
238
+ - DOH
239
+ - DXB
240
+ # 2026-05-21 (user direction): owner-based Gulf detection (QR) dropped.
241
+ ops_class_gulf_owner_codes: []
242
+
243
+ # ---------- User-defined extraction filters (2026-05-25) ----------
244
+ # A flight matching ANY filter below is routed to the GULF class
245
+ # (extract-only — no staff allocation). Each filter is a dict with any
246
+ # subset of fields. Empty / missing field = "don't match on that
247
+ # field". Match is AND across specified fields, case-insensitive
248
+ # exact. Example entries:
249
+ # - ac_owner: N0
250
+ # - ac_type: X
251
+ # - dep: BLR
252
+ # arr: COK
253
+ # - custom_header: REG
254
+ # custom_value: VT-IKW
255
+ # Edit via the Override drawer's "Extraction Filters" form or by hand.
256
+ extraction_filters: []