nessar d74dfb9938
CI / policy (push) Successful in 48s
CI / test (push) Canceled after 0s
CI / benchmark (push) Canceled after 0s
CI / build (push) Canceled after 0s
CI / security (push) Canceled after 0s
CI / images (push) Canceled after 0s
CI / validate (push) Canceled after 1m8s
CI / policy (pull_request) Successful in 9s
CI / validate (pull_request) Successful in 1m6s
CI / test (pull_request) Successful in 3m30s
CI / benchmark (pull_request) Failing after 5m39s
CI / build (pull_request) Successful in 1m5s
CI / security (pull_request) Successful in 1m27s
CI / required (push) Failing after 1s
CI / images (pull_request) Failing after 18s
CI / required (pull_request) Failing after 1s
ci: migrate to Gitea Actions
2026-10-13 19:43:08 +02:00
2026-10-13 19:43:08 +02:00
2025-10-23 00:21:20 +02:00
2026-10-13 19:43:08 +02:00
2025-10-31 01:49:42 +01:00
2026-10-13 19:43:08 +02:00
2026-10-13 19:43:08 +02:00
2026-10-13 19:43:08 +02:00
2026-10-13 19:43:08 +02:00
2026-10-01 05:02:09 +00:00
2026-10-02 15:05:30 +02:00
2025-12-06 22:19:44 +01:00
2025-12-06 22:19:44 +01:00

VacOptimizer

VacOptimizer is a personal leave planner for turning dated leave balances into a deterministic plan around weekends and French public holidays. It handles mandatory rules, ordered preferences, earned leave, expiring balances, and interrupted calculations without relying on a backend solver.

Source code, issues, and pull requests are hosted on Gitea. The issue-tracker guide documents tea-cli access and tracker conventions.

The planner answers these questions:

  • Is the request valid?
  • Can all mandatory rules be satisfied together?
  • What is the best plan found by the ordered heuristic?
  • If calculation stops early, what valid incumbent is available?
  • Why did each displayed placement survive the objective order?
  • Which alternatives were considered, and what would another date change?

Architecture

flowchart LR
    Host["Express production host<br/>static and client assets"] -. serves .-> UI

    subgraph Browser
        UI["Angular standalone components"]
        State["Signals + localStorage<br/>settings, balances, rules"]
        Duration["Indicative duration<br/>session-local calibration"]
        Facade["Planning calculation context<br/>immutable snapshot + async artifacts"]
        Worker["Leave-planner Web Worker"]
        Core["Resumable planner core"]
        Holidays["date-holidays<br/>French public calendar"]

        UI <--> State
        State -- "pending inputs" --> Duration
        Duration -- "estimated wait" --> UI
        UI -- "rendered-calendar timing" --> Duration
        UI -- "Calculate / Cancel" --> Facade
        Facade -- "request / cancellation" --> Worker
        Worker --> Core
        Core --> Holidays
        Worker -- "heartbeats, progress, checkpoints, outcome" --> Facade
        Worker -- "after outcome: explanation + trace batches" --> Facade
        Facade -- "trace acknowledgements" --> Worker
        Facade --> UI
    end

Leave calculation stays in the browser. The production Express process serves the generated Angular application; it does not participate in planning. Browser calculations run cooperatively in a dedicated worker. The resumable core is emitted as a separate worker/lazy chunk and retains a synchronous adapter for direct core callers. Trace delivery uses bounded batches with receiver acknowledgements; large calendars and traces can still cause UI pauses and substantial memory use, as recorded in the benchmark report.

Main execution path

  1. PlanningDurationService reads the pending horizon, balances, and ordered rules to estimate the wait without running the planner.
  2. On startup or an explicit Calculate action, CalendarYear submits those inputs through CalendarService, which opens one immutable planning calculation context.
  3. leave-planner.service.ts copies the request into a canonical transport shape and starts the worker.
  4. leave-planner.worker.ts runs the framework-free core, sends progress and valid checkpoints, publishes the outcome first, then delivers its stable explanation and original trace.
  5. CalendarYear ignores stale calculations, renders the selected dates, reports remaining balances or errors, and records successful request-to-rendered-calendar timing for duration refinement.

Calculations run until all objectives complete or the user cancels; there is no automatic elapsed-time cutoff. During preparation and optimization, Cancel interrupts the worker cooperatively and preserves its best jointly valid candidate as a cancelled, incomplete calculation. A cancellation before a valid candidate reports no plan. The final calendar is delivered before the explanation and complete original trace, whose later arrival or failure cannot change the retained plan. Worker heartbeats distinguish responsive long phases from a worker that stops responding. See the cancellation mechanism and checks.

Technical callers may explicitly supply deadlineMs, a finite non-negative planning-work budget of any size. Omission means no deadline; the calendar and benchmark never supply one. This opt-in budget starts after calendar preparation and excludes trace capture, observer callbacks, and post-outcome artifact work. Expiry returns PlanningIncomplete with the latest valid checkpoint, if any, rather than a conflict. It is independent of worker liveness monitoring.

Planning model

A request contains:

  • a user-selected half-open planning horizon, including past dates and multiple years;
  • CP, RTT, or Other leave balances with quantities, availability dates, and expiry dates;
  • ordered TIME_WINDOW and IF_THEN rules;
  • an optional explicit technical deadline and progress callback.

Rules are either:

  • obligations, which must all be feasible in the same plan; or
  • preferences, optimized lexicographically in user order.

TIME_WINDOW places a quantity inside an inclusive date window. IF_THEN creates before, after, around, or automatic patterns for each matching French public holiday. A completed IF_THEN occurrence may earn a new dated leave entitlement after its final time-off period ends.

The outcome is one of:

Outcome Meaning
InvalidInput The request is malformed.
PlanningConflict Valid obligations cannot all be satisfied together.
Plan All stages completed with placements, periods, metrics, provenance, and remaining balances.
PlanningIncomplete Cancellation, the deadline, or worker failure stopped calculation; a valid incumbent may be present.

All planning dates are normalized as YYYY-MM-DD calendar values in the Europe/Paris domain. The planner treats weekdays excluding French national public holidays as working days.

Settings save a fixed start date and an inclusive last planning date. The planner's exclusive end is the following calendar date, so matching settings dates cover one day. Without saved dates, the default is today in Europe/Paris through the same date twelve months later, exclusive (February 29 uses February 28 in the following year). Invalid edits prevent calculation and preserve the last valid saved dates. Changes to dates, balances, and rules apply to the displayed calendar on the next explicit Calculate action.

An indicative range beside Calculate follows the current dates, balances, and rules. Completed plans refine it locally for the current browser session using request-to-rendered-calendar time; later trace delivery is excluded. The range never stops a calculation. See the duration model and measurement contract.

Optimization pipeline

The core is a deterministic, constraint-first heuristic rather than a mathematical solver. It applies these objectives in order, never improving a lower objective by degrading a higher one; complete public-holiday weeks are selected before broader calendar-gain and work-sequence optimization:

Planner state machine

stateDiagram-v2
    direction LR

    state "Request snapshot" as Snapshot
    state "Validate and normalize" as Validation
    state "Prepare calendar across the horizon" as Preparation
    state "Solve obligations jointly" as Obligations
    state "Place at-risk balances" as AtRisk
    state "Resolve ordered preferences and earned chains" as Preferences
    state "Run lexicographic quality passes<br/>(complete holiday weeks, then non-working-day gain)" as Quality

    [*] --> Snapshot
    Snapshot --> Validation: canonical request

    Validation --> InvalidInput: malformed input
    Validation --> Preparation: valid input
    Preparation --> Obligations: calendar ready

    Obligations --> PlanningConflict: no joint solution
    Obligations --> AtRisk: feasible solution

    AtRisk --> Preferences: at-risk leave placed
    Preferences --> Quality: ordered preferences resolved
    Quality --> Plan: all quality passes complete

    Snapshot --> PlanningIncomplete: worker unavailable
    Validation --> PlanningIncomplete: interrupted
    Preparation --> PlanningIncomplete: Cancel or worker failure
    Obligations --> PlanningIncomplete: Cancel or interruption
    AtRisk --> PlanningIncomplete: Cancel or interruption
    Preferences --> PlanningIncomplete: Cancel or interruption
    Quality --> PlanningIncomplete: Cancel or interruption

    InvalidInput --> [*]
    PlanningConflict --> [*]
    PlanningIncomplete --> [*]
    Plan --> [*]

PlanningIncomplete is reachable from every active calculation state. Cancellation retains the best available jointly valid candidate, even before a stage completes; completed stages also publish checkpoints for other interruption paths. An explicit technical deadline applies only after calendar preparation, while worker failure can interrupt any active phase. PlanningConflict is produced only when the obligations are valid but jointly infeasible. Outcome delivery does not wait for explanation or trace completion.

  1. validate and normalize the request;
  2. find joint obligation assignments;
  3. minimize unplaced at-risk leave;
  4. satisfy each preference in user order, including shared placements and earned-entitlement chains;
  5. maximize completed public-holiday week value (Wednesday holidays outrank Tuesday/Thursday, which outrank Monday/Friday);
  6. maximize adjacent weekend and public-holiday gain;
  7. minimize the longest work sequence;
  8. minimize deficits caused by internal work sequences shorter than five days;
  9. minimize supporting placements;
  10. prefer earlier-expiring balances;
  11. prefer earlier placement dates.

Every placement records its source balance and all rules it satisfies. Remaining balances are classified as unavoidable expiry, at risk, or deferred.

The calendar keeps plan details and the advanced trace collapsed initially. Plan details navigate final placements chronologically, showing provenance, contributions, the first decisive objective, and the full ordered comparison including obligation validity. Compare dates opens an on-demand hypothetical comparison across the submitted horizon, preserving the selected balance, type, and provenance without changing the plan.

The trace explorer navigates original phases and atomic decisions, expands candidate evaluations, and shows before/after calendar states. Historical date statuses remain separate from hypothetical results. Incomplete outcomes explain the retained plan and unfinished phases; conflicts identify the reported obligations and bottleneck. Controls support keyboard navigation, labelled dates, live announcements, and text or icons alongside color. Only the latest trace is retained locally in memory.

Technical stack

  • Angular 22 standalone components with zoneless change detection
  • Angular Material and CDK
  • Signals for UI state and local storage for settings, edited leave balances, and rules
  • TypeScript 6
  • Web Workers for planner isolation
  • date-holidays for the French public calendar
  • Bun for dependency management and project scripts
  • Vitest through the Angular test builder
  • ESLint and Prettier
  • Angular server build hosted by Express
  • Gitea repository hosting and a multi-stage Docker image

The /benchmark route and planner core are lazy-loaded, so the normal application does not execute the benchmark or bundle the planner core into its main entry point.

The header opens one Settings dialog for planning dates, System / Light / Dark theme selection, English / French / German / Spanish language selection, and ISO week numbers. Changes apply and save on selection; closing the dialog retains them. System follows device appearance changes.

Saved balances, including an empty list, are restored before the initial calculation. When no readable saved list exists, CP, RTT, and Other start with random quantities from 10 through 20. These defaults remain unsaved until the first balance edit; subsequent edits save the entire list with identities, order, quantities, types, and calendar dates. Invalid saved balance data is cleared without removing unrelated settings or rules.

Repository layout

src/app/
├── benchmark/                    # Lazy seven-scenario performance harness
├── components/                   # Calendar, inputs, rules, and settings UI
├── services/
│   ├── leave-planner.service.ts  # Canonical async planning boundary
│   ├── leave-planner.worker.ts   # Browser worker adapter
│   ├── leave-planner.core.ts     # Framework-free planning algorithm
│   ├── calendar-service.ts       # Calendar projection and UI integration
│   ├── calendar-settings-service.ts # Persisted horizon and week-number settings
│   ├── planning-duration.service.ts # Indicative wait and session-local refinement
│   ├── user-input-service.ts     # Saved balances and explicit calculation requests
│   └── planning-date.ts          # Shared Europe/Paris date helpers
└── shared/                       # Small shared UI utilities

src/i18n/                         # English, French, German, and Spanish text
docs/adr/                         # Architecture decisions
docs/benchmark/                   # Reference-phone benchmark protocol
CONTEXT.md                        # Canonical domain language and rules

Local development

Prerequisite: Bun. Dependency versions are locked in bun.lock.

bun install --frozen-lockfile
bun start

Open http://localhost:4200. The development server reloads when source files change.

Useful commands

Command Purpose
bun start Generate the i18n registry and start the development server.
bun run test --watch=false Run the unit and integration suite once.
bun run lint Run ESLint.
bun run format:check Check Prettier formatting.
bunx tsc --noEmit -p tsconfig.app.json Type-check application code.
bunx tsc --noEmit -p tsconfig.spec.json Type-check tests.
bun run build Create the optimized browser and server bundles in dist/.
bun run serve:ssr:vac-optimizer Serve a completed production build on port 4000.

Performance benchmark

Build or start the application, then open /benchmark. The isolated harness covers a representative year, typed obligations, chained earned entitlements, a deliberate conflict, a loaded year, and two- and three-year horizons with dated annual balances. It reports preparation time, planning latency, separate trace-delivery latency, total wall time, UI timer delay, available main-thread heap measurements, outcome, last completed stage, placement count, determinism, and trace size/status.

The release protocol, recorded desktop measurements, and hardware limitations are documented in docs/benchmark/README.md. No reference-phone run or multi-year duration ceiling is established; timing measurements are release evidence, not CI assertions.

Docker

docker build -t vac-optimizer .
docker run --rm -p 4000:4000 vac-optimizer

The image builds the Angular application with Bun and Node, then runs the generated Express server with Bun. Set PORT to override the default port 4000.

Continuous integration

.gitea/workflows/ci.yml defines Gitea Actions validation, functional coverage, production builds, security scans, and Docker image publication. Issue #268 tracks activation. Actions is enabled with owner approval; publisher credentials and live workflow validation are still pending. The activation guide records runner, credential, permission, and merge-gate requirements.

The workflow runs on branch/tag pushes, PR opening/reopening/synchronization/edits, and manual dispatch. Manual runs validate without publishing images. Required jobs cover policy regression tests, GitFlow and Conventional Commits, ESLint, Prettier, application/test type checks, Vitest coverage, the production build, Semgrep SAST, and Trivy dependency/secret scans. Coverage, build output, and SARIF reports are retained for seven days.

Functional tests use one worker and a 2 GiB Node heap. The separate benchmark job runs without coverage and keeps its five-minute protocol timeout. At the user's request, it has continue-on-error: true while #78 is open. #78 must restore the benchmark gate after the protocol passes. Passing functional checks during this exception does not establish benchmark acceptance.

The required job aggregates successful validation and expected image publication, accepting the image job's intentional skip for fork PRs and manual runs. Image jobs require successful validation and are limited to pushes and same-repository PRs. Registry credentials are confined to the publishing step.

Both protected branches still require gitlab-to-gitea/ci-conversion-required until the owner approves the first merge message and a successful Gitea PR run, then replaces the migration gate with the actual pull-request context for required. Local validation does not satisfy this status. Historical CI reports and artifacts remain in administrator rollback backups; the original configuration is retained in Git history.

GitFlow and releases

develop is the default integration branch. Create feature/* branches from it, rebase them onto the current origin/develop before opening or merging their PR, and merge into develop with a merge commit (--no-ff). Existing working-branch names such as feat/* follow the same rules. Working branches cannot merge develop into themselves instead of rebasing.

Create release/X.Y.Z from develop when the release scope is ready. Set package.json to X.Y.Z on this branch, stabilize it, then merge it into main and back into develop. Keep the release branch until both PRs have merged. A hotfix/X.Y.Z starts from main, changes the package version, and follows the same two-target merge process. Do not rebase main, develop, or a frozen release onto ongoing development.

Use Conventional Commits for new commits and PR titles, for example feat(calendar): add planning periods, fix(planner): handle expired balances, or chore(release): prepare 1.6.0. Scopes are optional; ! marks a breaking change. The merge commit subject must preserve the validated PR title. Gitea supports a Git-based merge-message template at .gitea/default_merge_message/MERGE_TEMPLATE.md, using ${PullRequestTitle} and ${PullRequestReference}. The committed template and a checked example expansion are documented in the activation guide; legacy history is not rewritten.

The image publisher implements this tag contract in gitea.nessar.fr/nessar/vac-optimizer:

Trigger Published image tags
Successful push to any branch branch-<normalized branch slug> and sha-<full commit SHA>
Explicit release tag vX.Y.Z vX.Y.Z and latest, using the existing SHA image

For example, feature/calendar uses branch-feature-calendar; integration uses branch-develop. Preserve the previous slug normalization: lowercase the branch name, replace characters other than a-z and 0-9 with -, truncate to 63 bytes, and remove leading/trailing -. Choose branch names with distinct slugs. Only valid release tags can update latest. Automatic image publication and tag promotion begin after the owner provisions publisher credentials and validates publication permissions.

After owner activation, the PR into main has merged, and its full checks and SHA image publication have succeeded, tag the published main commit:

git fetch origin
git tag -a v1.6.0 origin/main -m "Release 1.6.0"
git push origin v1.6.0

The tag must match the tagged commit's package.json version, use stable vMAJOR.MINOR.PATCH syntax, and point to a commit reachable from main. Wait for the main checks and image publication before tagging: promotion requires its sha-<commit> image. Tags do not increment the version automatically.

The Gitea repository policy requires develop as the default branch and Merge commit as the merge method, with squash and rebase merge methods disabled. Configure branch protection for main and develop to disable direct and forced pushes, restrict the merge allowlist to designated maintainers, require successful status checks, and block outdated PRs. Administrators must follow the protection rules. After merge-message approval and a successful reviewed PR run, replace the migration gate with the reported pull-request context for the required job, as described in the activation guide. These are required settings; the migration does not establish that every setting has already been applied.

Only release/* and hotfix/* PRs may target main; working PRs target develop. CI validates these rules and checks working-branch rebases against the fetched integration head. If develop advances after validation, rebase and rerun the PR checks before merging.

Run node scripts/ci-policy.test.mjs and node scripts/ci-image.test.mjs locally to verify GitFlow event handling and image publication without writing to a registry. The adapter reads Gitea webhook data through the runner's GITHUB_* compatibility variables. Local checks do not satisfy the migration gate.

Domain and design references

  • CONTEXT.md defines the project vocabulary and planner semantics.
  • ADR 0001 records the ordered heuristic decision.
  • ADR 0002 records the client-side scope.
  • ADR 0003 records the constraint-first planner architecture.
  • ADR 0004 separates the outcome, stable explanation, original trace, and hypothetical analysis.
  • ADR 0005 records saved-input restoration and random fallback behavior.
  • ADR 0006 records fixed saved horizons, cancellation, duration estimation, and the removal of the automatic deadline.
S
Description
No description provided
Readme
4.8 MiB
0 Stars 1 Watchers 0 Forks
Languages
TypeScript 71%
Python 19.5%
HTML 5.3%
SCSS 2.5%
Shell 0.8%
Other 0.8%