chore: release v14.4.0 - promote staging to main#260
Conversation
CLAUDE.md says the project standardised on Luxon. The backend still
imported `moment` in five files for trivial date formatting; this PR
removes those usages and drops `moment` from the root package.json.
Frontend `moment` usage (hundreds of call sites across Timesheet /
TimeLog / composables) is NOT touched here β that's a much larger
migration scoped separately. The frontend's own `package.json` keeps
its `moment` entry so the Vue build doesn't break.
New helper:
* `utils/dateHelpers.js` exposes
- `formatDate(input, fmt)` β accepts JS Date, millis, ISO string,
or `{seconds}` shape, and translates moment-style format tokens
(YYYY/MM/DD/HH/mm/ss/A/MMM/ddd) to luxon-style under the hood
so existing callers don't have to change their format strings.
- `formatNotificationDate(input)` β exact replacement for the two
`moment.calendar()` sites in `Modules/notification/sendEmail/`,
which both used the same format string for every branch (so the
relative-time wrapper was a no-op). Preserves the original
output, including the pre-existing `HH:MM` token quirk.
Migrations:
* `Modules/logTime/controllerV2.js` β single `.format("YYYY-MM-DD")`
call β `formatDate(date, 'yyyy-LL-dd')`.
* `Modules/notification/sendEmail/controller.js` and
`Modules/notification/sendEmail/controllerV2.js` β
`moment().calendar(...)` β `formatNotificationDate(...)`.
* `Modules/tasks/helpers/helper.js` and
`Modules/tasks/helpers/mongo_helper.js` β `changeDateFormat`
delegates to `formatDate` (which keeps the moment-style API its
callers pass).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two affordances dispatched click handlers on non-semantic elements (no role, no aria-label, no keyboard focus, no screen-reader announcement): * `frontend/src/components/atom/Modal/Modal.vue` β the modal close icon was a bare `<img @click="closeModal()">`. Wrap in `<button type="button" :aria-label="$t('Projects.close') || 'Close'">` with the icon inside as a presentational `<img alt="">`. * `frontend/src/components/atom/Attachments/Attachments.vue` β the "Download All" affordance was a `<span @click="downloadAllImages()">`. Replace with `<button type="button" class="download-all-btn ...">`. Both components' stylesheets gain a tiny rule to strip native button chrome (so the visual result matches the old elements) but preserve `:focus-visible` so keyboard users see the focused state. Note: a couple of nearby affordances (Attachments' "See All" `<div>` and the help-icon popover) have the same anti-pattern but are out of scope here β the audit specifically flagged the `<span>` and `<img>` cases above. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Modal atom rendered a bare `<div class="modal">` β no role,
no aria-modal, no aria-labelledby. Screen readers couldn't announce
the modal as a dialog and focus could leave the modal via Tab and
reach background controls.
Changes to `frontend/src/components/atom/Modal/Modal.vue`:
* Template:
- Root `<div class="modal">` gains `role="dialog"`,
`aria-modal="true"`, `:aria-labelledby="titleId"`, and
`tabindex="-1"` (so focus can land on the dialog itself when no
children are focusable).
- The title `<span>` now carries the `:id="titleId"` that
aria-labelledby points at.
- `@keydown.tab="handleTabKeydown"` traps Tab; `@keydown.esc.stop`
closes the modal on Escape.
* Script:
- `modalRef` template ref + `titleId` computed.
- `handleTabKeydown` cycles focus between the first and last
focusable descendants (queries the standard focusable selector
list, skipping aria-hidden and offscreen elements).
- `activateFocusTrap` captures the previously-focused element and
moves focus to the first focusable inside the modal (or the
dialog itself if there are none). Called on mount-if-open and
when `modelValue` flips to true.
- `deactivateFocusTrap` restores focus to the captured element on
close / unmount.
No new dependency: the trap is ~40 lines of inline logic; we don't
need `focus-trap` for one component.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`npm test` was a placeholder (`echo "Error: no test specified" && exit 1`)
even though jest was already installed as a devDependency. This PR
wires it up as a working bootstrap suite that future fixes can grow
into.
Changes:
* `package.json` β
- `test` now runs `jest`.
- new `test:watch` (development) and `test:naming` (the pre-existing
structural-audit test, kept off the default run because it flags
legacy naming inconsistencies tracked separately).
* `jest.config.js` β points jest at `tests/`, ignores
node_modules/frontend/installation/time-tracker-app/.claude and the
naming-conventions audit, and runs in `node` environment.
* `tests/smoke.test.js` β minimal guarantee the harness is alive
(always runs).
* `tests/utils/dateHelpers.test.js`, `tests/utils/safeServiceFile.test.js`,
`tests/utils/imageGuard.test.js` β behaviour tests for the helpers
introduced earlier in this audit (BUG-042, BUG-036, BUG-023). Each
suite is wrapped in a graceful skip when its target module isn't on
the current branch, so this PR lands cleanly today and the tests
light up automatically once the corresponding helper PRs merge to
staging.
`npm test` exits 0 β 3 passed (smoke) + 16 skipped (helpers pending
merge). After BUG-023/036/042 merge, all 19 should pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`Modules/tasks/helpers/{helper,mongo_helper}.js`'s `HandleHistory`
wrote `createdAt` / `updatedAt` manually using `DateTime.utc().ts`,
which is a numeric millisecond value β not a BSON Date. Existing
documents ended up with inconsistent shapes (number vs Date) depending
on which code path created them.
Every other schema in `utils/mongo-handler/createSchema.js` is built
with `{ timestamps: true }` so Mongoose owns the timestamps. The
history schema alone was missing that option.
Changes:
* `utils/mongo-handler/createSchema.js` β add
`{ strict: false, timestamps: true }` to `historySchema`.
* `Modules/tasks/helpers/mongo_helper.js` (HandleHistory) and
`Modules/tasks/helpers/helper.js` (HandleHistory) β drop the manual
`createdAt: utcDateTime.ts, updatedAt: utcDateTime.ts` lines.
Mongoose populates both as BSON Dates on `.save()`.
* `utils/mongo-handler/schema.js` β keep `createdAt`/`updatedAt`
declared on the history shape so callers that read them still work,
but drop `required: true`. Mongoose sets them on `.save()`; making
them required on updates would reject legacy docs that never had
them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`frontend/src/components/molecules/DropDown/CustomDropDown.vue` is the shared custom dropdown atom used across the app. Pre-fix: * The trigger `<div @click="buttonClick()">` had no role, no aria-haspopup, no aria-expanded, no tabindex β keyboard users couldn't reach or open it. * The floating panel had no role="listbox" β screen readers couldn't identify it as a menu. * The mobile-view close affordance was a bare clickable `<img>`. Changes: * Trigger gains role="button", tabindex="0", :aria-haspopup="'listbox'", :aria-expanded (bound to open state), :aria-controls (panel id). * Trigger handles keyboard: - Enter / Space β open/close (mirrors click). - Escape β close. - ArrowDown β open AND move focus to the first focusable child inside the options slot. (We can't enforce role="option" on user-provided slot content, but moving focus there gets keyboard users navigating immediately.) * Floating panel gains role="listbox" and an @keydown.esc handler. * Mobile close `<img>` wrapped in a real `<button type="button">` with aria-label="Close" and a style rule (`.dropdown-close-btn`) that strips native chrome but preserves :focus-visible. * New helpers: `closeDropdown()` and `openAndFocusFirstOption()`. Note: the sibling `DropDown.vue` and `MobileDropDown.vue` follow the same anti-pattern but were not flagged by the audit; they'd benefit from a similar sweep in a follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Done - Test Case
β¦itation-body fix(security): stop overwriting req.body, allow-list invite keys (BUG-011 / #65)
fix(security): tighten + env-configure auth rate limit (BUG-012 / #66)
β¦-revalidation fix(security): re-validate company membership per request, cached (BUG-013 / #67)
β¦on-token-empty fix(security): tighten verifyEmail against loose-equality bypass (BUG-014 / #68)
β¦er-resolve fix(stability): stop calling reject after resolve in HandleTask (BUG-016 / #70)
fix(stability): replace forEach(async) fire-and-forget pattern (BUG-017 / #71)
β¦-rejection fix(stability): surface allSettled rejection reasons (BUG-018 / #72)
β¦lobal-setchat fix(stability): declare setChat with const (BUG-019 / #73)
β¦stringify fix(stability): align cache set/remove key in MainChats (BUG-020 / #74)
perf: add MongoDB indexes for hot query paths (BUG-021 / #75)
chore(deps): upgrade sharp 0.32.6 β ^0.34.0 (BUG-022 / #76)
β¦-limits fix(security): guard sharp() inputs against pixel-bomb DoS (BUG-023 / #77)
β¦-handlers perf: switch readFileSync in request handlers to async (BUG-024 / #78)
β¦g-leaks fix(observability): route console.* through Winston (BUG-025 / #79)
β¦ality fix(stability): strict equality on isEmailVerified check (BUG-026 / #80)
β¦age-fix fix: correct copy-paste error in companyId validation message (BUG-028 / #82)
β¦on-null-guard
fix: validate timeDuration shape before .split(':') (BUG-029 / #83)
β¦a-null-guard fix(stability): guard against missing response.data in sprints (BUG-030 / #84)
β¦-send fix(stability): guard checkUserAndCompany against duplicate res.send (BUG-027 / #81)
β¦fication-silent-catch fix(observability): log silently-swallowed fetch failures (BUG-031 / #85)
β¦e-filtering fix(BUG-032): consistent soft-delete filtering on list/count endpoints
β¦249) * fix: address safe CodeRabbit findings from the promotion (#240) review Six low-risk, verified fixes: - Webhooks/dispatcher.js: guard against an undefined pending entry in the debounce callback (entry.doc could throw). - gitlabOAuth/controller.js: add a 15s timeout to the token-exchange axios call (was unbounded). - Admin/common/controller.js: drop an erroneous JSON.parse in the missing-file branch (makeDefaultBrandSettings resolves an object) - fixes a first-run 404 on getBrandSettingsData. - Stickies/helpers/stickyRules.js: strict boolean for isPinned (the string 'false' no longer coerces to true). - StickiesPanel.vue: clear the pending debounced save in remove() so it cannot fire a stale PUT after delete. - README.md: 'Priority support & SLAs' -> 'response-time targets' to match SUPPORT.md. Verified: node --check (backend), sticky-rules jest 16/16, StickiesPanel SFC compile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: scope updateMember role-promotion guard to PAT requests Companion to #248: the owner-only role-promotion check in updateMember now runs only for PAT/MCP requests (req.apiToken), so the web app's role management behaves exactly as before. Same 2026-06-15 MCP-isolation hardening as permissionGuard.js. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chore(release): back-merge main into staging after v14.3.0
feat(auth): add opt-in TOTP two-factor authentication (Phase 1)
β¦ion-aware notifications Integrations (Settings β Integrations): - Manage Slack / Discord / JSON outgoing webhooks with per-event subscriptions and delivery logs - Action-aware notifications: announce the specific change (status, assignee, priority, due, rename, estimate, β¦) with fromβto diffs and assignee/lead ids resolved to names - Deliver exactly one task.created on create (suppress create-flow churn and the project-counter phantom emit); re-read the full task before delivery so payloads are complete Importers (project filter toolbar): - CSV and Trello importers mirroring the Jira importer; map columns / lists onto the project's statuses - Resolve statuses from the project's taskStatusData with a guaranteed type/key so imports never fail statusType validation; route the Jira importer through the same shared helper - Downloadable sample templates in all three import modals Tasks: - Stop the automatic AI time estimate on task creation (manual-only now) Tests: 198 passing (webhook-rules, csv-rules, trello-rules). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
β¦ention notifications S3-01 Trello rich import: extract checklists, comments, labels, attachments and member emails from board JSON; enrich imported tasks (assignee resolution by email, checklist items, attachment link-refs, labels appended to description) and create comments post-import. S3-02 Story points: points field + updatePoints action and permission; per-project estimation scale (fibonacci/linear/tshirt/hours) with a config modal and POST /api/v1/projectSetting/estimationScale; a scale-aware points picker on the task detail panel; and a points rollup in list group headers. S3-04 @mention notifications: parse [Name](userId) mentions from comment text, persist a mentions record, and dispatch the "@mentioned" notification on comment create and update. S3-03 backlog view was skipped (AlianHub has no un-sprinted tasks) and its scaffolding removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
β¦ation feat(integrations): sprint 2 β webhooks UI, CSV/Trello importers, action-aware notifications
β¦ivity
node-global-key-listener ships a native hook binary (WinKeyServer.exe) that antivirus (Bitdefender) flags as a keylogger and quarantines, silently zeroing all keyboard/mouse activity counts. Replace it with Electron's built-in powerMonitor.getSystemIdleTime(): while tracking, sample once per second and emit an activity tick per active second. No native binary, no accessibility permission, AV-proof, cross-platform.
Desktop: background.js runs a 1 Hz idle sampler emitting 'activity:tick'; timelog.js setActivityTick buckets ticks per minute as { active: N }; trackerRunning.jsx and tracker.js consume it and strokes carry { active }; package.json drops the dependency. Frontend helper.js surfaces the active value in the existing Keyboard column (Mouse stays 0); old timesheets keep their original keyboard/mouse counts. Backend and DB schema unchanged - strokes are stored opaquely in trackShots.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
β¦ation feat(sprint3): story points, estimation scale, Trello rich import, @mention notifications
Refines the powerMonitor approach so the timesheet shows real Keyboard AND Mouse activity instead of one combined value. Each second while tracking, the sampler reads two hook-free signals - powerMonitor.getSystemIdleTime() and screen.getCursorScreenPoint(): cursor moved => mouse activity; input with no cursor movement (typing) => keyboard activity. Still no global input hook, so antivirus does not flag it. A click that does not move the cursor counts as keyboard (acceptable trade-off vs the AV-flagged hook).
Strokes return to the { keyboard, mouse } shape, so the web frontend needs no change: helper.js reverts to its original (reads keyboard/mouse) and the production website displays the data correctly with no deploy.
background.js: cursor-movement + idle sampler emits activity:tick with a type; timelog.js setActivityTick buckets per minute as { keyboard, mouse }; trackerRunning.jsx and tracker.js pass the type and build keyboard/mouse strokes; helper.js reverted to original.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
β¦owermonitor fix(tracker): AV-safe keyboard/mouse activity detection
S4-01 burndown: Sprints/burndown.js now also computes story points (totalPoints + per-day remainingPoints/idealPoints) alongside the existing count/hours, exposed at GET /api/v1/agile/burndown (query-aware, reuses the existing handler). S4-02 velocity + S4-03 CFD: new Modules/AgileReports/ β GET /api/v1/agile/velocity (committed vs completed points per sprint + rolling-3 avg) and /cfd (status-band counts per day). Computed on-the-fly from tasks + Task_Status history instead of a snapshot cron: the cron is production-only (untestable locally) and history already holds the transitions, so no new collection/cron and it works immediately. S4-04 PDF: new Modules/Export/ β POST /api/v1/export/pdf renders a branded PDF from a client payload (table rows + chart dataURI images) via pdfmake (lazy-required so a missing install never breaks startup; standard Helvetica font). Export PDF buttons on each Reports chart. Frontend: views/Projects/Reports/ (ReportsView + Burndown/Velocity/CFD ApexCharts). Reports added as a first-class add/removable view via the +View catalog (projectTabs response injection + ViewsDropdown preview + projectComponentsIcons + i18n). No DB schema change; backend suite unaffected (208 pass). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
β¦ation feat(sprint4): agile reports β burndown, velocity, CFD, PDF export
S5-01 (AHE-3710): chose dhtmlx-gantt (GPL) as the Gantt foundation β ADR in docs/adr/gantt-library.md. Lazy-loaded so it never enters the main bundle.
S5-02 (AHE-3714): GanttView.vue β a first-class add/removable project view (keyName 'GanttView'). Bars from startDate/DueDate fed live from the Vuex projectData store; drag/resize -> new 'updateDates' task action (PATCH /api/v2/tasks, mirrors updatePoints); dependency arrows from task relations ('blocks' => finish-to-start) with draw/delete -> /api/v2/tasks/relations; day/week/month zoom; unscheduled tray; milestone diamonds from /api/v1/milestone/project/:pid; read-only when the user lacks task-edit permission. Uses the dhtmlx singleton (no destructor) so reopening the tab re-inits cleanly.
S5-03 (AHE-3715): recurring tasks. New RECURRING_TASKS collection wired through the mongo-core path (schemaType/collections/schema/createSchema/mongoQueries). Modules/RecurringTasks/ β CRUD + a node-schedule cron (every 15 min, prod-only) that instantiates real tasks via taskMongo.create across all companies, plus /run-now and /run-due endpoints so it is testable in dev. Frontend RecurringTasksManager.vue as an add/removable 'RecurringTasks' view.
Notes: the Gantt/RecurringTasks/Reports views are surfaced in the + View catalog via PROJECT_TAB_COMPONENTS DB records (added per-company), not controller injection. dhtmlx-gantt added to frontend deps. Additive only β 208 backend tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
β¦ation feat(sprint5): gantt/timeline view + recurring tasks
When Gantt was the active view on a fresh reload, the Vuex task store was empty (only List/Table/Board mount triggers the load via taskListHelper), so the chart rendered nothing. GanttView now triggers the same loader on mount when the store is empty for the project+sprint, and retries if sprints arrive late; the existing reactive watch renders once data lands. Retains the earlier reopen-crash fix (dhtmlx singleton, no destructor, guarded init). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per-feature files in .claude/test-cases/ following the existing template: Integrations (Slack+Discord), CSV importer, Trello importer, story points, @mentions, velocity, CFD, reports PDF export, Gantt, recurring tasks. Includes third-party setup steps (Slack app webhook, Discord webhook, Trello JSON export) and Gantt reload/reopen regression cases. Skips BurndownChart (already present); excludes n8n (not built). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(gantt): render on direct reload + Sprint 2-5 QA test cases
β¦evoke, rate-limit S6-02 hardening, all backward-compatible (legacy 16-byte/no-expiry/no-password shares keep working). Backend: 32-byte tokens (legacy 32-hex still resolve); optional expiresAt (resolveShare 404s expired); optional bcrypt password gate on the public page (POST /share/:token unlock); hard revoke via DELETE /api/v2/public-shares/:id (removes the share + the global token-index row); express-rate-limit on /share/:token (60/min) and intake (10/min); passwordHash never returned (responses expose hasPassword). Frontend: PublicShareModal exposes expiry + password on create, a lock/expiry status row, and a Delete-link button; + i18n keys. Verified: 209 backend tests pass (added a legacy-token backward-compat test); SFC + en.js compile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
β¦dening feat(public-share): harden read-only links β expiry, password, hard-revoke, rate-limit
β¦rogress Relations: a task blocked_by an open task now shows a "Blocked by N open task(s)" alert in its Linked Tasks panel β making AlianHub's blocked-task alert real. Adds getOpenBlockers + the pure selectOpenBlockers helper and an `openBlockers` action on POST /api/v2/tasks/relations. Additive: the central status-write path is untouched; the panel reuses data it already loads. Epics: schema gains startDate, dueDate, ownerUserId, priority; status adds in_progress; create/update accept + validate the new fields (parseEpicDates, EPIC_PRIORITIES). EpicsPanel shows a priority badge, owner, dates, a % label and an inline status select on top of the existing progress bar. Tests: +16 (selectOpenBlockers/isClosedStatusType, parseEpicDates, priority & status) β full suite 225 pass. Test-cases added under .claude/test-cases/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
β¦nhancements feat(tasks,epics): blocked-task warning + epic dates/owner/priority/progress
|
Caution Review failedThe pull request is closed. βΉοΈ Recent review infoβοΈ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: β Files ignored due to path filters (3)
π Files selected for processing (108)
π WalkthroughWalkthroughThis PR adds 2FA, agile reports with PDF export, CSV and Trello importers, recurring tasks, story points and estimation scale support, Gantt and epic updates, mentions, protected public shares, richer webhook integrations, related tests/docs, and a separate desktop time-tracker activity sampling change. ChangesApplication features
Time tracker activity sampling
Sequence Diagram(s)sequenceDiagram
participant User
participant LoginUI
participant AuthAPI
participant TwoFactorAPI
participant Session
User->>LoginUI: submit email and password
LoginUI->>AuthAPI: login request
AuthAPI-->>LoginUI: twoFactorRequired + tempToken
User->>LoginUI: enter TOTP or recovery code
LoginUI->>TwoFactorAPI: validate tempToken + code
TwoFactorAPI->>Session: finalize session
Session-->>LoginUI: uid + auth tokens
sequenceDiagram
participant User
participant ReportsUI
participant AgileReportsAPI
participant PdfExportAPI
User->>ReportsUI: open report tab
ReportsUI->>AgileReportsAPI: fetch burndown/velocity/cfd
AgileReportsAPI-->>ReportsUI: report data
User->>ReportsUI: export PDF
ReportsUI->>PdfExportAPI: send title, chart image, table
PdfExportAPI-->>ReportsUI: PDF file
Estimated code review effortπ― 5 (Critical) | β±οΈ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
β¨ Finishing Touchesπ Generate docstrings
π§ͺ Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. π§ ast-grep (0.43.0)utils/mongo-handler/schema.jsThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Release v14.4.0 β staging β main promotion
Promotes the full staging line to production for the v14.4.0 release. Cut as
release/v14.4.0because themainbranch-name check requiresrelease/vX.Y.Z.Headline contents
Merge instructions
mainβstagingto keep the two branches aligned.π€ Generated with Claude Code
Summary by CodeRabbit
New Features
@Mentionsin comments with notificationsImprovements