The validator's last-ditch fallback (used when both project-specific
and global columns are missing) returned a hardcoded list that omitted
"on_hold". The function's own docstring on the same code path even
calls this out explicitly:
drops to globally-defined columns like "on_hold" come back as 400
"Invalid status".
The broader fix from PR #605 made the validator fall back to global
columns first, which fixes the common case. But the very last fallback
list — used during fresh migrations before the kanban_columns table is
seeded — still rejects "on_hold" tasks the user has already created.
Real installs that ship with on_hold columns enabled hit this on the
first request after a clean migration.
Add on_hold to the hardcoded list so it stays consistent with what
on_hold-enabled installs expect to validate.
Auto-lint reformatted the surrounding column declarations; the only
behavioral change is the addition of on_hold to the fallback list.
ClientApprovalStatus is defined with uppercase Python names and lowercase
string values (PENDING = "pending", etc.), but the Postgres enum type
clientapprovalstatus is defined with the lowercase values. SQLAlchemy
defaults to binding the enum *name*, so every query against the column
sent "PENDING" and Postgres rejected it with InvalidTextRepresentation.
This made get_pending_approvals_for_client raise on every client portal
request — the navbar context processor catches the exception and returns
0, but the stack trace was logged on every page load.
Pass values_callable to SQLEnum so SQLAlchemy uses the enum *value* (the
lowercase string PG actually stores).
The auto-lint hook reformatted the rest of the file; the only behavioral
change is the values_callable parameter on the status column.
The conditional invoice-unbilled-time IIFE was wrapped in its own
<script>...</script> inside an outer <script> block. Browsers do not
allow nesting; the inner </script> closed the outer script, leaving
confirmDeleteNote and the trailing </script> as raw HTML. The function
rendered as visible text at the bottom of the client detail page and
was unreachable, breaking the per-note Delete button.
Removing the nested tags lets the IIFE run inline within the outer
script and restores confirmDeleteNote.
projects/list.html had an extra </div> at line 216 (after listView's
closing tag) that pushed the rest of the page outside the projectsContainer
wrapper, causing the grid to render unstyled and the footer to bleed up
to the top-right.
weekly_goals/index.html had an extra </div> at line 223 just before
{% endblock %} with no matching open, producing the same broken-wrapper
effect.
Both pages now render centered with the standard footer position.
Companion to #603. That earlier PR added is_license_activated guards
to three donate UI gates (header support button, user-menu support
link, support modal donate/buy-license buttons). Six other donate
gates in templates were missed:
- app/templates/base.html:1187 (sidebar nav donate entry)
- app/templates/base.html:1356 (large dismissible support banner)
- app/templates/main/help.html:834, 841 (help-page donate prompts)
- app/templates/main/about.html:43 (about-page donate header)
- app/templates/main/dashboard.html:629 (dashboard donate widget)
- app/templates/reports/index.html:18 (reports-page donate prompt)
Each was gated only on `current_user.ui_show_donate` (per-user flag),
not on the instance-wide is_license_activated. So a licensed instance
where some users still had the default ui_show_donate=true would keep
showing donate prompts to those users — most prominently the big
amber-gradient banner in base.html that draws the eye on every page.
Repro: with settings.donate_ui_hidden=true (license active), log in
as a user whose ui_show_donate is still the default true, and observe
the banner at the top of every page plus the help/dashboard/reports/
about prompts — none of which respect the active license.
Fix: append `and not is_license_activated` to the six leaking
visibility guards. Mirror of the pattern in #603.
The two about.html gates at lines 189 and 196 already check
donate_ui_hidden (functionally equivalent to is_license_activated)
and are left untouched.
5 files, +7 / -7 (or +9 / -9 when combined with #603's base.html
edits in this same branch). No backend, schema, or behavioural
change beyond the template visibility guard.
Note on overlap with #603: this branch's base.html includes both
PR #603's changes and this PR's changes. If #603 lands first, the
base.html diff in this PR shrinks to the +2/-2 unique to it.
If this PR lands first, #603 is subsumed and can be closed.
Wires the OIDC groups claim into the RBAC Role table introduced by
migration 030 (super_admin, admin, manager, user, viewer).
Until now, OIDC could only set the legacy users.role="admin" column
via OIDC_ADMIN_GROUP. Nothing in the codebase ever assigned Role rows
from OIDC, which meant IdP groups could not grant super_admin,
manager, or any custom role through SSO — only the binary is_admin
flag through the legacy column.
Three new env vars, all opt-in:
OIDC_ROLE_GROUP_MAP — JSON map of OIDC group name -> Role name.
Example:
OIDC_ROLE_GROUP_MAP='{"app-admin":"admin","app-manager":"manager"}'
Empty/invalid JSON disables the feature; OIDC_ADMIN_GROUP keeps
working unchanged.
OIDC_ROLE_SYNC_MODE — "additive" (default) or "sync".
additive: only ADD Role rows matching the user's groups; never
revoke. Misconfigured map degrades to a no-op.
sync: also REMOVE mapped Role rows when the matching group
is gone from the user's claims.
OIDC_NEVER_REVOKE_USER_IDS — comma-separated user IDs that must
never have roles revoked by OIDC sync, regardless of mode.
Useful for protecting bootstrap admins against a misconfigured
map in sync mode.
Implementation in app/routes/auth.py runs after the existing
OIDC_ADMIN_GROUP block. Steps on each OIDC login:
1. Parse the user's groups claim against OIDC_ROLE_GROUP_MAP -> a
set of target Role names.
2. Look up matching Role rows in DB (silently skips names that do
not exist as Role rows).
3. ADD: any target Role the user does not already have.
4. REMOVE: only in sync mode, only Role rows whose name is in the
map's values (so manually-assigned roles outside the OIDC scope
are preserved), and only if the user id is not in
OIDC_NEVER_REVOKE_USER_IDS.
5. Commit through safe_commit; failures log a warning and continue.
Defensive JSON parsing in config.py handles empty/missing input,
invalid JSON, non-dict roots (array, null, number), and falsy
keys/values — all degrade to {} (no-op). A warning is logged on the
first OIDC callback after a parse failure so a misconfigured env var
surfaces in the app log without crashing the app.
OIDC_ROLE_SYNC_MODE defaults to "additive" for any value other than
exactly "additive" or "sync" so typos default to safe.
OIDC_NEVER_REVOKE_USER_IDS ignores non-integer entries.
Why additive default: a misconfigured OIDC_ROLE_GROUP_MAP in sync
mode would silently revoke every mapped role on the next login,
including the bootstrap super_admin if the IdP claims do not include
the configured group. Additive mode means a misconfigured map
degrades to a no-op, not a lockout.
Backward compatible: every existing OIDC deployment without these
env vars set keeps identical behaviour. OIDC_ADMIN_GROUP is
untouched.
2 files, +103 / -0. No schema change, no data migration.
Companion to #606. The route validator at tasks.py:223 already calls
KanbanColumn.get_valid_status_keys(), but two downstream spots still
silently re-introduced the old hardcoded 5-key behaviour.
1. app/services/task_service.py:46
`VALID_STATUSES = ("todo", "in_progress", "review", "done", "cancelled")`
was a class-level hardcoded tuple. create_task() at line 85 silently
coerced any status not in the tuple to TaskStatus.TODO.value. So a
user creating a task with initial status "on_hold" would have it
quietly clamped to "todo" at the service layer even though the route
accepted it.
2. app/templates/tasks/create.html and tasks/edit.html
The status preview badge — both the server-rendered Jinja chain
in create.html and the client-side updateBadge() JS map in both
templates — hardcoded the same 5 keys. Selecting "on_hold" in the
dropdown caused the JS lookup to miss and fall back to the first
map entry ("To Do"), so the preview lied even before the form was
submitted.
Fix 1 (service): create_task() now calls
KanbanColumn.get_valid_status_keys(project_id=project_id) to build
the allowed set per call. The VALID_STATUSES tuple is kept as a
last-ditch fallback for the table-not-yet-seeded path and extended
to include "on_hold" so even the fallback matches the default seed.
Fix 2 (templates): the Jinja preview chain in create.html now loops
over kanban_columns to find the matching label. The JS updateBadge()
map in both create.html and edit.html now generates entries from
{% for col in kanban_columns %}, so any configured column key works
without further code changes.
3 files, +25 / -13. No schema change, no data migration.
Two distinct fix sets in one commit, both extending the kanban validator
fix in PR #605 and the project_attachments path-resolution fix already
discussed in this repo's history.
PHASE A — five upload routes joined current_app.root_path + ".." +
"uploads/<X>", which on a deployed instance with the standard
docker-compose layout resolves to /app/uploads/<X>. That path is
outside the mounted app_uploads volume, so every upload returns 500
with PermissionError. Same defect as project_attachments.
- app/routes/team_chat.py:470 (chat attachments)
- app/routes/clients.py:1257 (client attachments)
- app/routes/comments.py:279 (comment attachments)
- app/routes/quotes.py:1120 (quote attachments)
- app/routes/client_portal.py:1330,1347 (legacy "uploads/" download
fallback branches — same join, same bug)
Fix prepends "app/static/" so the resolved path lands inside the
mounted volume at /app/app/static/uploads/<X>. Mirrors the
invoice_images and quote_images patterns elsewhere in the same files.
PHASE B — validator/UI drift bugs, same class as the kanban fix in #605.
- app/models/kanban_column.py
* new helper get_columns_with_global_fallback() — returns
project columns or falls back to globals; mirrors
get_valid_status_keys behaviour for templates
* last-ditch hardcoded fallback in get_valid_status_keys now
includes "on_hold" so the table-not-yet-seeded path matches
the keys initialize_default_columns seeds
- app/routes/tasks.py
* task_counts now initialises from kanban_columns instead of
the hardcoded 4 keys; tasks in cancelled/on_hold/custom
columns are counted in the summary cards
* create-task validator now calls get_valid_status_keys(project_id)
instead of a hardcoded 5-key tuple; users creating a task in
on_hold no longer silently get clamped to todo
* every render_template("tasks/create.html", ...) and
("tasks/edit.html", ...) now passes kanban_columns
- app/templates/tasks/create.html and tasks/edit.html
* status <option> list now loops over kanban_columns instead of
hardcoding 5 keys
- app/routes/invoices.py:832
* bulk-update validator now accepts "issued", mirroring the
single-update validator at line 623; the model supports it
- app/routes/quotes.py:920, 1026, 1320
* admin-notification queries now use User.is_admin (which
considers both the legacy role column AND Role rows) instead
of User.query.filter_by(role="admin", ...). RBAC-only admins
granted via the Role table are now notified on quote.sent,
quote.accepted, and quote.approval.requested.
10 files, +60 / -41. No schema change. No data migration.
PUT /api/tasks/<id>/status returns 400 "Invalid status" whenever the
task belongs to a project that has no project-specific kanban_columns
rows AND the user drops it into a configured global column other than
the four hardcoded fallback keys.
Reproduction:
1. Project has no project-specific kanban_columns rows.
2. The instance has 5 globals (project_id IS NULL): todo, in_progress,
review, done, on_hold.
3. The kanban UI renders the 5 globals as drop targets for that
project's tasks.
4. User drops a task into "On Hold". Frontend sends status="on_hold".
5. app/routes/tasks.py:1519 calls
KanbanColumn.get_valid_status_keys(project_id=task.project_id)
with the project's id.
6. get_active_columns(project_id=<id>) filters strictly on project_id
and returns [].
7. get_valid_status_keys then falls back to the hardcoded list
["todo", "in_progress", "review", "done", "cancelled"]
which is missing "on_hold" (and includes "cancelled", which isn't
even a configured column).
8. "on_hold" is not in that list -> 400.
Drops to the four hardcoded keys all returned 200; only "On Hold"
failed, exactly matching the live 200/400 alternation observed in
production logs.
Fix: when there are no project-specific columns, fall back to the
configured global columns from the database (which is the set the UI
is already rendering). The hardcoded list is only used as a last-ditch
fallback when even the globals table is empty - this preserves the
table-not-yet-seeded safety net during fresh migrations.
Pure validator change; no schema change, no behavioural change beyond
accepting the statuses the UI is already offering.
These were caught by the project's own flake8 step but the failing
checks have been red on a number of recent runs, suggesting it's worth
fixing the underlying defects rather than ignoring the rule.
1. app/routes/auth.py — F821: undefined name 'datetime'
`current_user.two_factor_confirmed_at = datetime.utcnow()` (line ~620)
used `datetime` without importing it. Confirming 2FA raises
`NameError: name 'datetime' is not defined` at runtime.
Adds `from datetime import datetime` to the imports.
2. app/routes/timer.py — F823: local variable '_' referenced before assignment
`from flask_babel import gettext as _` is imported at module scope.
Four functions then unpack `can_start, _ = TimeTrackingService().can_start_timer(...)`
which makes `_` a function-local for the entire enclosing scope and
shadows the i18n alias. Three earlier `flash(_("..."))` calls in the
same functions (lines 171, 449, 2019) reference the local before it
exists and raise `UnboundLocalError` at runtime.
Fix: rename the throwaway slot from `_` to `_unused` in all four
`can_start_timer` unpackings. The translation alias resolves cleanly
in every flash() call again.
Total: +6 / -4 across two files.
The header support button, the user-menu support link, and the donate /
buy-license buttons inside the support modal were rendered for every
authenticated user, including instances with `donate_ui_hidden = true`
(an activated supporter license). Other donate prompts (sidebar, dashboard,
about, reports, help) already gated on `is_license_activated`; these three
spots slipped through.
Wrap each in `{% if not is_license_activated %}` so a licensed instance
gets a clean UI. The "Love TimeTracker? Share it" button stays visible —
sharing is still useful regardless of license state. Modal title copy
already adapts via the existing `is_license_activated` branch.
Extract snapshot reload from saved-design loading and reuse it for
history restore so undo matches save semantics.
Keep a capped stack of stage.toJSON() snapshots with debounced pushes
after drags, transforms, property panel edits, alignment/layer moves,
adds, deletes, and related actions.
Wire Ctrl/Cmd+Z and Ctrl/Cmd+Y (plus Ctrl/Cmd+Shift+Z for redo)
outside focused inputs; add non-passive wheel handling on the canvas
container to zoom within existing scale limits.
Document shortcuts and wheel zoom in the editor info box (i18n-ready).
When the layout editor posts template_json with the preview request,
use it instead of loading only the saved database template. Preview
then matches unsaved canvas edits and avoids stale layouts.
Normalize page width/height from the selected page size when parsing
form JSON; fall back to the stored template if the body is missing
or invalid.
Add a regression test ensuring form JSON overrides DB content.
Ensure quote list/detail access uses shared quote scope resolution so users with quote-management permissions can view records they can edit, including post-edit redirects in web and API flows. Add regression coverage for non-admin edit_quotes behavior and document the scope-alignment requirement in advanced permissions docs.
Register Português in LANGUAGES and normalize pt-BR/pt-PT (and similar)
to pt in _normalize_locale so Accept-Language and stored preferences resolve
to translations/pt/.
Add translations/pt/LC_MESSAGES/messages.po seeded from English msgids;
translators can fill msgstr incrementally.
Extend i18n tests for pt presence and catalog file. Update translation
docs (TRANSLATION_SYSTEM, CONTRIBUTING_TRANSLATIONS, implementation note).
Replace the separate plus and bolt floating controls with a single Actions menu inside #fabDock, driven by app/static/floating-actions.js. The dock stacks Actions, optional team chat, and AI Helper using shared CSS variables for spacing; the AI control is a circular FAB matching the other buttons.
Move the chat widget panel to a fixed viewport overlay so dock z-index no longer paints controls over the open panel, and lift the panel bottom when the admin version banner or mobile bottom nav applies. Fade non-actions dock children while the actions menu is open (fab-dock--menu-open).
Update README.md, docs/UI_GUIDELINES.md, and the advanced-features implementation summaries so contributors describe the floating hub instead of global-fab.js. Keep app/static/quick-actions.js aligned with the retired mount pattern for any remaining references.
- Add ollama and ollama-init services with ollama_data volume; app waits for the model pull and receives AI_* defaults (AI_BASE_URL=http://ollama:11434).
- Document bundled stack, env vars, and Ollama vs host base URL in README and DOCKER_COMPOSE_SETUP.
- Align env.example AI defaults with the compose stack.
- fix(ai): include api_key_set on AIProviderConfig so from_settings(**get_ai_config()) matches the settings dict.
Introduce a guided LDAP configuration wizard mirroring the OIDC flow:
five-step UI with server/TLS, bind, directory layout, groups and
AUTH_METHOD, then optional connection test and .env / Docker Compose
generation for copy-paste deployment.
- Refactor LDAPService.test_connection to accept an optional config
mapping so the wizard can test draft values without merging live env
secrets; keep POST /admin/ldap/test on current_app.config.
- Add GET /admin/ldap/setup-wizard plus POST endpoints for test,
validate, and generate-config (manage_settings, rate limited).
- Surface an LDAP card with status badge and wizard link on the
integrations list for admins and manage_settings users.
- Add tests for validate, generate, and wizard test delegation.
Extra or unmatched </div> tags inside {% block content %} closed layout
ancestors early, which broke the centered main column and stacked modals
and scripts incorrectly.
- import_export/index.html: drop duplicate grid closer
- saved_filters/list.html: remove orphan closer after page body
- time_entry_templates/list.html: same orphan pattern as saved filters
Timer starts always blocked a second running entry and never read the\nadmin-controlled Settings flag.\n\n- Add TimeTrackingService.can_start_timer() using Settings.get_settings()\n and wire it into start_timer, web timer routes, kiosk start, and\n legacy POST /api/timer/resume.\n- POST /api/v1/timer/start returns 409 with error_code\n timer_already_running when single-active mode is on and a timer\n is already running.\n- Deduplicate start_timer template handling in the service.\n\nTests: tests/test_single_active_timer_setting.py.\nDocs: REST_API (responses), GETTING_STARTED, REQUIREMENTS, Docker env\nnotes, TESTING_STRATEGY, env.example comment; CHANGELOG entry.
Introduce AUTH_METHOD values ldap and all, with LDAP_* environment settings, ldap3-based LDAPService (search, optional groupOfNames checks, user bind, DB sync), and users.auth_provider (local|oidc|ldap) via migration 153_add_user_auth_provider.
Login supports LDAP-only and combined all (local then LDAP where appropriate); OIDC callback sets auth_provider. Forgot/reset/change password flows skip LDAP-managed accounts. Admin System Settings gains a read-only LDAP summary and POST /admin/ldap/test. Production env validation requires core LDAP variables when LDAP is enabled; OIDC registration and docs recognize all.
Documentation: new docs/admin/configuration/LDAP_SETUP.md; updates to OIDC_SETUP, GETTING_STARTED, Docker guides, Render deploy notes, docs README, and CHANGELOG. Tests: tests/test_ldap_auth.py; test_oidc_logout allows auth_method all.
Add app/static/manifest.json (TimeTracker / Tracker, indigo theme) and PNG install icons via scripts/generate_pwa_icons.py.
Replace inline Flask service worker with app/static/js/sw.js served at /service-worker.js for full-site scope. Cache name timetracker-v1: cache-first for /static, network-first for HTML and non-v1 /api, no interception of /api/v1/* (preserves Authorization).
Add public GET /offline and offline.html for SW navigation fallback; redirect /manifest.webmanifest to the static manifest.
Wire base.html (manifest link, theme-color #4F46E5, SW registration) and pwa-enhancements.js (ready/update/push without duplicate registration). Remove legacy app/static/service-worker.js and manifest.webmanifest.
Tests: service worker and offline routes, manifest redirect, TestPWA expectations; drop duplicate test_enhanced_ui app/client fixtures in favor of conftest.
Docs: ASSETS.md, BUILD_CONFIGURATION.md, implementation notes, and incomplete-features analysis updated for new paths.
Add POST /api/v1/clients/{client_id}/invoice-unbilled to build one draft
invoice from completed billable time not yet on any invoice line, grouped
by project with RateOverride-based rates. Supports API tokens
(write:invoices) or a logged-in user with create_invoices; enforces client
and module access.
InvoiceService gains shared preview/state helpers and
create_client_unbilled_invoice with safe_commit and invoice webhook.
Client detail page adds a confirmation flow and redirect to the new
invoice. Document read:invoices / write:invoices and the new route in
docs/api/API_TOKEN_SCOPES.md; expose the path on the v1 discovery payload.
Tests: two-project client creates two line items; second call returns
no_unbilled_entries.
Global quick actions (FAB):
- Add a fixed primary FAB with an animated popover (Start Timer, Log Time,
New Task) for authenticated users in base.html.
- Start Timer uses the dashboard control when present, otherwise opens the
dashboard with #start-timer so the existing start modal appears.
- Hide the FAB from the md breakpoint up while the header floating timer is
active (floating-timer-bar.js toggles body.fab-hide-desktop-timer-active).
Time entries overview:
- Make Notes and Duration editable in the table where permissions allow;
save via same-origin PUT/PATCH /api/entry/<id> (time-entries-inline-edit.js).
- Register PATCH on the session update_entry route alongside PUT.
Dashboard:
- Add a this-week vs last-week hours chart fed by GET /api/reports/week-comparison
and dashboard-enhancements.js.
Documentation:
- Extend UI_GUIDELINES (FAB, inline edit, file reference) and ARCHITECTURE
(session JSON endpoints) to match the new behavior.
Add an authenticated-only bottom bar below the md breakpoint with
Heroicon-style tabs for Dashboard, Timer, Time entries, Projects, and
More. More opens a slide-up sheet (backdrop, close, Escape) for
Invoices, Clients, Reports, and user Settings, gated by module flags
where applicable.
Align shell layout to Tailwind md (768px): sidebar hidden md:flex,
main md:ml-64 / md:ml-16 when collapsed, mobile hamburger md:hidden,
RTL mainContent margin reset at 767px. Main column uses pb-16 on
small screens so content clears the bar; bar and sheet use pb-safe
(env safe-area) with a Tailwind safelist and @layer utilities rule.
Remove the legacy six-slot FAB bottom nav from base.html.
Docs: README UI overview, CHANGELOG [Unreleased], UI_GUIDELINES layout
and file reference.
Adds a Tailwind-styled global command palette (Ctrl/Cmd+K) with fuzzy search and a Start Timer flow that picks a project then POSTs /api/timer/start with CSRF.
Updates docs to reflect the new shortcut.
Adopt an indigo brand palette with slate neutrals, add semantic colors, and introduce base/component layers (buttons, cards, badges). Move Inter loading to the Tailwind input CSS and update brand guidelines accordingly.
Introduce a web-first AI helper with admin-configurable providers (Ollama or hosted OpenAI-compatible), server-side context building, and confirmed write actions. Expose the feature via session /api/ai/* endpoints and scoped /api/v1/ai/* endpoints.
Harden security by requiring a strong SECRET_KEY for Docker Compose, adding optional settings encryption-at-rest (Fernet), and introducing TOTP-based 2FA plus password reset flows. Update admin UI, API docs, and install documentation.
Allow the desktop renderer to authenticate through the app login endpoint and call API routes from its packaged origin without weakening non-API responses.
Client portal: add min-w-0, break-words, and flex gap/shrink utilities on
the projects grid cards so long project names no longer force horizontal
overflow and clip against the viewport edge.
Desktop: add app and tray icons, adjust Electron main process (window,
tray, lifecycle), renderer connection and API client updates, build script
and package metadata, and regenerate the bundled renderer script.
Add render_sandboxed_string() using SandboxedEnvironment so stored invoice and
quote HTML, ReportLab text templates, admin PDF previews, and invoice email
HTML are not evaluated with Flask's full template globals (mitigating SSTI).
Add regression tests for sandbox behavior and demo user permissions.
Create the auto-provisioned demo user with legacy role "user" and the RBAC
user role so public demos cannot reach settings or the PDF layout editor.
On startup, downgrade an existing demo user that still has admin or
super_admin roles left over from older releases.
Document behavior in docs/deploy/RENDER.md and README.md.
Client-portal-enabled users (main app login, typically viewer) were not
included in get_allowed_client_ids(), so ProjectService and other callers
saw scope_client_ids=None and listed every project.
- Return [client_id] for is_client_portal_user in User.get_allowed_client_ids
- Derive get_allowed_project_ids from allowed client IDs for all non-admins
- Apply client/project scope and access checks from allowed IDs, not only
subcontractor is_scope_restricted (fixes user_can_access_* for portal)
FixesDRYTRIX/TimeTracker#592.
Tests: extend test_scope_filter with client_portal_scoped_user and API
isolation for GET /api/v1/projects.
Desktop (Electron):
- Add two-step first-run wizard: test TimeTracker via GET /api/v1/info, then log in with API token
- Replace bogus token check with validateSession (users/me, fallback to timer/status for narrow scopes)
- Normalize base URLs; classify TLS/DNS/timeout errors; periodic 401 forces re-login
- Settings save/test use public + authenticated checks; prebuild/prestart and npm test
Server:
- Exempt /api/v1/info, /api/v1/health, and POST /api/v1/auth/login from HTML setup redirect
- Include setup_required on GET /api/v1/info for unfinished installs
Mobile (Flutter):
- Validate saved token against new server URL before persisting settings change
- Remove unused lib/core/config.dart; point BUILD_CONFIGURATION at app_config.dart
Docs: DESKTOP_SETTINGS, desktop README, mobile-desktop-apps README, REST_API /info
Delete app/utils/posthog_features.py. It was unused by routes, and
is_posthog_enabled() always returned False, so flags never activated and
the API was misleading.
Document the real model: deployment behavior uses environment variables
and app/config.py; per-user UI preferences stay on the user record.
Refresh PostHog monitoring guides and README accordingly, update the
incomplete-implementations note, and soften posthog_segmentation wording
so it does not imply an in-app PostHog flag layer.
Register optional blueprints and the optional audit_logs module with full tracebacks (logger.exception and stable extra fields). Re-raise registration errors when FLASK_ENV is development or DEBUG is enabled so local misconfiguration fails fast; production and testing keep skipping optional modules after logging.
Update REST API, API versioning, architecture, project structure, contributor guide, and CONTRIBUTING for global search responses (partial and per-domain errors), shared run_global_search in app/services/global_search_service.py, and blueprint registry observability.
Document the dual HTTP surface everywhere integrators look: OpenAPI intro and servers, ARCHITECTURE, REST_API, API_VERSIONING (deprecated vs internal routes, shared modules), and CONTRIBUTING (v1-first rule).
Session JSON routes in app/routes/api.py that overlap REST v1 now return X-API-Deprecated and a Link header with rel successor-version, implemented via app/utils/api_deprecation.py.
Extract shared global search into app/services/global_search_service.py for both GET /api/search and GET /api/v1/search while preserving legacy short-query 200 empty responses and v1 400 validation.
Refactor legacy POST /api/timer/start, /api/timer/stop, and the start path of /api/timer/resume to use TimeTrackingService; keep existing socketio emits for the web UI.
Add tests/test_api_deprecation_headers.py and adjust search partial-failure tests to patch Project.query on the service module.
Global search referenced Client.company, which is not a column on Client, so client matches failed at runtime. Legacy and v1 search, plus search_clients(), now filter on name, email, description, and contact_person; result descriptions use the same fields. Legacy /api/search returns count: 0 for queries shorter than two characters so responses stay consistent.
OpenAPI info.version is taken from get_version_from_setup(), with a config fallback when the resolved version is unknown. get_version_from_setup() also honors TIMETRACKER_VERSION and APP_VERSION for CI and container builds.
Client.__init__ accepts custom_fields. ClientService no longer passes status= into Client(), which the initializer does not support.
Tests add HTTP route contract checks and OpenAPI version alignment, fix subcontractor search fixtures (Client/Task construction and v1 client fixture naming), and update related API integration tests.
Smart notifications (opt-in under user settings): NotificationService builds candidates from the user's local day and active timers; GET /api/notifications and POST /api/notifications/dismiss; migration 150 adds user columns and user_smart_notification_dismissals. /api/summary/today uses the same local-day totals. Client polls from smart-notifications.js; toastManager.show gains onDismiss for server dismiss sync. Config and env.example document SMART_NOTIFY_* variables.
Value dashboard: StatsService with Redis-backed caching, GET /api/stats/value-dashboard, dashboard template and dashboard-enhancements polling alongside existing widgets.
API v1 token search now uses apply_project_scope and apply_client_scope on queries; scope_filter adds apply_project_scope; tests extended for the new helper.