All notable changes to Milestone Planner are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Administrators can test an organization's Microsoft Entra SSO configuration from the admin portal, which reports each setting Microsoft accepts or rejects without anyone having to attempt a sign-in, along with how the server reaches the internet.
- A failed sign-in with Microsoft now says what actually went wrong — a redirect address registered under the wrong platform, an invalid or expired client secret, missing consent — instead of "Failed to exchange authorization code". The Microsoft error code is shown so an administrator can act on it without reading the server log.
- Single sign-on no longer falls back to a workspace's own settings when the organization's configuration cannot be read. It previously carried on with the wrong settings, and Microsoft then rejected the sign-in with an error pointing at the wrong thing entirely.
- Workspace SSO settings saved without a client secret are no longer offered on the sign-in page: signing in with them could never succeed, and it failed as though the secret were wrong rather than missing.
- Whitespace accidentally saved with an SSO setting — a trailing newline from a copy-paste — is now ignored instead of making Microsoft reject the credentials.
- A sign-in that cannot reach Microsoft at all now returns to the sign-in page saying so, instead of an unexplained server error.
- A failed sign-in now distinguishes a rejection by Microsoft from a reply that did not come from Microsoft — the latter usually meaning a proxy or gateway answered instead — so the two are no longer investigated as the same problem.
- Provisioning a tenant from the admin portal now reports the new administrator's actual password. It previously showed a different value, so the new administrator could not sign in and the real password was unrecoverable, leaving the tenant to be repaired with a password reset.
- Provisioning a tenant no longer returns the PostgreSQL administrator password in the admin portal's response.
- A failed sign-in with Microsoft now returns to the workspace's own sign-in page and shows why it failed. Previously, when single sign-on was managed by an organization, every failure silently landed on the admin portal login with no explanation, making a working configuration look broken.
- The workspace SSO settings screen now shows the organization's settings — tenant ID, client ID, redirect URI, auto-create and default role — read-only, with the secret masked, when SSO is managed by an organization. It previously showed an empty, greyed-out form.
- Sign-in no longer rejects everyone when group-based access is required but the group lookup itself fails (usually a missing
GroupMember.Read.Allconsent on the app registration). That case is now reported as a configuration problem instead of "you do not have access". - An organization SSO configuration missing its redirect URI is no longer advertised as ready on the sign-in page, where starting a login would fail.
- A sign-in no longer returns an opaque server error when the organization's stored client secret cannot be decrypted.
- A tenant can now be assigned to an organization at creation time, so it inherits the organization's single sign-on configuration immediately instead of only after a separate edit.
- Bank-holiday import now uses version 4 of the Nager public-holiday API at its new address,
https://nagerholidays.com/api/v4. If your firewall allows onlydate.nager.at, addnagerholidays.comor holiday import will stop working after this upgrade. - An installation that pins
NAGER_API_URLto a/api/v3URL keeps working — the version is detected automatically and a warning is logged — but version 3 of the holiday API reaches end of life on 31 January 2027. - Imported holiday names are now in English (for example "Swiss National Day" rather than "Nationalfeiertag"); version 4 of the holiday API no longer publishes local-language names. Existing holidays keep their current names until a site's holidays are refreshed.
- Only public and bank holidays are imported. The holiday API's school, observance, optional and authority entries are ignored, so they no longer count against working days.
- Region filtering now matches subdivision codes exactly, and accepts either form — a site configured with
ZHor withCH-ZHgets the same holidays. - Changing a site's country now replaces its imported holidays for the current and next year instead of adding to them, so holidays from the previous country no longer linger.
- Refreshing holidays no longer clears a site's existing holidays when the holiday API is unreachable — they are kept until a successful fetch replaces them.
- A single duplicate or malformed holiday returned by the API no longer discards the whole import; it is skipped and the remaining holidays are stored.
- Updated a build-time dependency (
nanoid) that a newly published advisory flagged as vulnerable. Build tooling only — the shipped application was never affected.
- The MPP import diagnostics endpoints (
/import/test,/import/test-upload) now require a superuser session — previously they were reachable without authentication, including an endpoint that accepted file uploads. - The browser console no longer logs every API write request in production builds (the diagnostic logging now runs only in development).
- The frontend now declares Node.js 24 as its required toolchain (
enginesfield), matching the Docker build and CI. - React hooks-order violations now fail the frontend lint check instead of passing as warnings; the WebSocket hook that carried the only violations was refactored accordingly.
- Deprecated Python
datetime.utcnow()usage was fully migrated ahead of future Python upgrades. No behaviour change.
- Session expiry timestamps are now recorded as true Unix epochs. They were previously skewed 1–2 hours by the container timezone (self-cancelling in normal operation, but sessions spanning a daylight-saving switch could last up to an hour longer or shorter, and the stored expiry disagreed with the admin-portal sessions). Sessions created before this update will expire up to 2 hours early, once — logging in again is all that's needed.
- The session cookie metadata now records the actual expiry time instead of the session's creation time.
- Removed dead code left over from earlier iterations: unused backend exception classes and an unused site-access dependency, and several never-rendered frontend components, hooks and contexts (fill-down editing, touch drag, change-indicator badges, an orphaned theme selector and loading spinner, and duplicated row-position logic).
- Removed the unused
blackandisortdevelopment dependencies — Ruff handles formatting and import sorting.
.env.examplenow matches what the application actually reads: the master database is configured viaMASTER_DB_*variables (the previously documentedMASTER_DATABASE_URLwas ignored), and the proxy and auto-initialization variables are now documented. The ignoredSECRET_KEYvariable was removed from the compose files.
- Updated
cryptographyto 50.0.0, clearing a high-severity advisory about PKCS#7 enveloped-data decryption leaking key information through distinguishable errors and timing. Milestone Planner never used that code path — the library is only used for AES-GCM encryption of stored credentials — so no data was at risk. - Removed the unused
python-jose,authlibanditsdangerousbackend dependencies, which no longer ship in the application image. - Updated the web framework (FastAPI 0.141.1, Starlette 1.4.1, Pydantic 2.13.4), clearing seven Starlette advisories that the previous pins were stuck behind. Two were denial-of-service flaws an unauthenticated visitor could have triggered through form and file uploads; the rest covered request-URL handling. API responses are unchanged.
- Updated the bundled
brace-expansionandjs-yamlbuild-time dependencies to releases without denial-of-service advisories. They are used only when building the app, not at runtime.
- Backend CI now audits Python dependencies for known vulnerabilities on every pull request and once a week, matching the existing frontend audit.
- Moved
email-validatoroff a yanked release to a properly published version. No behaviour change.
- Removed the unused
react-router-domfrontend dependency, clearing two React Router advisories (an open-redirect and an SSR hydration flaw) that no longer apply to this app. - Updated the bundled
js-yaml,postcss,brace-expansionandminimatchbuild-time dependencies to versions without known denial-of-service and path-traversal advisories.
- The documentation screenshot pipeline now captures every screenshot in the docs automatically, including the admin portal, all management modals, tags, equipment maintenance blocks, and the read-only tenant SSO form shown when organization SSO applies.
- Fourteen more documentation illustrations: dependency arrows, critical path, context menu, column manager and filters, vacation and equipment booking modals, bank holidays, archived view, dark theme, site editor, staff filter, and the admin portal's create-tenant and organization SSO dialogs.
- Changing the status of a tenant that belongs to an organization no longer fails with an internal server error in the admin portal.
- The screenshot demo-data seed script is now safe to re-run: it no longer duplicates vacations or custom columns.
- The frontend build and development environments now use Node.js 24 LTS (previously Node 20, which has reached end-of-life). No application behaviour changes.
- The full site export now includes a Staff notes sheet.
- Newly provisioned tenants no longer receive unused legacy phase/subphase columns on equipment assignments; equipment bookings are project-level, matching the application.
- Fresh single-tenant installs via
setup_databases.sqlwere missing the user work-capacity and system-flag columns, which broke the app on first login; they are now included. - The master fresh-install script now creates the organizations and organization SSO tables and the tenant organization/group-access columns instead of relying on runtime auto-migration.
- Deleting a site or a project manager on provisioned tenants now detaches their projects and equipment instead of being blocked by the database.
- Other connected users now see a project immediately after an MS Project/CSV import instead of having to reload.
- Importing a project is now blocked while What-If mode is active — it previously bypassed the sandbox and wrote to the server for real.
- The manual tenant schema template was rebuilt to match the actual application schema (it had drifted on a dozen tables: wrong column names, missing columns and constraints).
- Unused frontend staff create/update/delete functions that targeted endpoints that never existed.
- The manual tenant schema template now includes the equipment blocks, tags and project tags tables it was missing.
- The staff notes table is now created consistently as
staff_noteson every install path; databases from older installs are migrated automatically (legacynotesrows are carried over and the old table removed). - Assignment endpoints are now correctly listed in the broadcast middleware's skip list, replacing a stale entry for a route that never existed.
- Dead HTTP presence endpoints and unused presence polling code; presence has always worked over the WebSocket connection.
- Admin portal dialogs no longer close when clicking outside them, which could discard in-progress input (e.g. when a text selection ended on the backdrop); use the close/cancel buttons or press Escape instead. Escape now closes admin portal dialogs.
- The fresh-install schema (
setup_databases.sql) now includes the previously missing company events, staff notes, custom columns, skills and project presence tables, matching the schema created for provisioned tenants.
- Corrected outdated setup and migration documentation: removed references to a nonexistent Alembic setup and
migrate_all_tenants.pyscript, documented the master-database migration runner, and completed the list of available migrations.
- Organization-level Microsoft Entra SSO sign-in no longer returns a 500 error on the Microsoft callback in multi-tenant mode: the workspace is now carried through the OAuth flow so the shared organization callback URL completes against the correct tenant database and returns the user to their workspace. A single organization SSO configuration and redirect URI now works for every tenant in the organization.
- Guardrails against redundant SSO setup: when a workspace's organization manages SSO, the tenant-level SSO settings form now explains that organization SSO takes precedence and is shown read-only (and the server rejects enabling tenant-level SSO), and the admin panel warns when adding a tenant whose own SSO would be overridden by organization SSO.
- SSO login button no longer stays hidden (and the SSO login/callback flow no longer fails) for multi-tenant instances: SSO configuration is now resolved correctly from the tenant context set by the tenant middleware, so both organization-level and tenant-level Microsoft Entra SSO work when signing in at a tenant URL.
- Tenant provisioning now works on managed PostgreSQL (Azure Database, RDS, Cloud SQL) where the admin role is not a superuser: the provisioning admin is granted each new tenant role before creating its database (previously failed with
must be able to SET ROLE "…"), and the tenant user is explicitly grantedCREATE/USAGEon itspublicschema (previously failed withpermission denied for schema publicwhen building tables).
- Updated
cryptographyto 48.0.1, fixing a vulnerable OpenSSL version bundled in the wheels. - Updated
python-multipartto 0.0.31, addressing denial-of-service and parameter-smuggling issues in querystring and multipart form parsing. - Updated
viteto 7.3.5, fixing aserver.fs.denybypass and an NTLM hash-disclosure issue in the bundled dev-server tooling. - Updated bundled frontend dependencies
form-data(CRLF injection),js-yaml(denial-of-service),@babel/core(arbitrary file read),esbuild(dev-server file read),ws(memory disclosure / denial-of-service), andbrace-expansion(denial-of-service) to patched versions.
- Bumped
react-router-domfrom 6.30.3 to 6.30.4 (Dependabot npm group update).
- Bumped
vitestfrom 4.0.18 to 4.1.0 (Dependabot npm group update).
- Bumped
authlibfrom 1.6.11 to 1.6.12 (Dependabot pip group update) — fixes redirecting to an unvalidatedredirect_urionInvalidScopeErrorinOpenIDImplicitGrantandOpenIDHybridGrant.
- Bumped
python-multipartfrom 0.0.26 to 0.0.27 (Dependabot pip group update).
- Bumped
authlibfrom 1.6.9 to 1.6.11 andpython-dotenvfrom 1.0.1 to 1.2.2 (Dependabot pip group update).
- Reproducible screenshot capture pipeline for the docs (
scripts/screenshots/): two headless Playwright scripts plus a SQL seed for vacations, bank holidays, and populated custom columns. Run against thedemotenant fromapp.scripts.seed_demoto refresh every screenshot referenced by the MkDocs site.
- Refreshed and expanded screenshot coverage in the user guide:
gantt-main.pngnow shows projects expanded into phases with custom columns populated and the today indicator on a real timeline.- New combined-view screenshots (
gantt-with-staff-panel.png,gantt-with-equipment-panel.png) demonstrating the Panels dock. - New
vacations-view.pngfor the previously screenshot-less Vacations & Time Off page. - New
what-if-active.pngshowing the active What-If state with Discard/Exit, replacingwhat-if.png. - New collaboration screenshots (
collab-online-users.png,collab-presence-viewing.png,collab-activity-feed.png) for the previously text-only Real-Time Collaboration page. custom-columns.pngnow shows populated values rather than the empty Manage Columns modal.
- Docker image now builds again — added
g++to the Python builder stage sopsutilandjpype1source builds succeed when no precompiled wheel is available for the target platform. /healthand/api/healthnow report the correct version inside Docker. TheVERSIONfile is now copied into the runtime image; previously it was missing and__version__fell back to0.0.0.
1.0.0 is a stability declaration — it captures the current shipping state of
the application and starts the formal SemVer + CHANGELOG discipline. From here
on, every code-impacting change bumps VERSION and lands a CHANGELOG entry in
the same PR.
- Adopted Semantic Versioning — single source of truth at
/VERSION. - Adopted Keep a Changelog format for
CHANGELOG.md. - New
.github/workflows/version-check.ymlCI gate that fails PRs whoseVERSIONbump is missing a matching## [<version>]heading inCHANGELOG.md. - Backend now reads its version from
/VERSIONat startup (app/__init__.py) — exposed via/healthand/api/health.
- Backend
__version__reconciled from the inconsistent hardcoded2.0.0down to the canonical1.0.0baseline. Frontendpackage.jsonversion aligned to match (and is now static — only/VERSIONis bumped going forward).
- Stale repo-root documentation files that pre-date or duplicate the MkDocs site at
docs/:AUDIT.md— one-off Feb-2026 audit report whose remediation items have either landed or migrated to GitHub issues.USER_MANUAL.md— duplicate of the MkDocsdocs/user-guide/content; the canonical end-user manual is published at docs-milestone.verdet.me.DEVELOPMENT.md— superseded bydocs/developer-guide/.