Skip to content

Commit 176acbb

Browse files
parth0025shyamvadaliya12claudejoshishiv4
authored
chore(release): promote staging to main for v14.4.0 (#261)
* fix(BUG-042): drop `moment` from backend; standardise on luxon 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> * fix(BUG-043): wrap clickable <span>/<img> affordances in real <button> 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> * fix(BUG-044): give Modal role=dialog, aria-modal, and focus trap 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> * fix(BUG-045): wire up the jest test suite `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> * fix(BUG-046): let Mongoose manage history timestamps `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> * fix(BUG-047): ARIA semantics + keyboard nav for CustomDropDown `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 Done - Test Case * Add Gitigonre Add Gitigonre * Rename the folder Auth Rename the folder Auth * Rename Folder Rename Folder * Chnage the name Chnage the name * Folder Rename Folder Rename * remove dist remove dist * Create yml Create yml * Update main.yml * Update main.yml * Update main.yml * Basic Setup Basic Setup * fix: repair broken advisory URL in SECURITY.md (#44) The advisory link was split across two lines and wrapped in a code span, rendering as malformed text rather than a clickable link. Joined the URL and used proper markdown link syntax. Pairs with enabling Private Vulnerability Reporting in repo settings so external reporters can actually submit advisories. Closes #44 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: refresh correct lastRequest field on connection reuse (#42) updateConnectionRecord was writing to lastRequset (typo) while the idle-cleanup loop in startInterval reads lastRequest. Active company databases therefore kept the timestamp frozen at createdAt and were eligible for termination after the 30-minute window despite continuous use. Fixed the property name so the update path and the cleanup path reference the same field. Closes #42 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: remove duplicate CheckInstallStep mount and .env hot-reload (#41) Two related sources of duplicate-handler accumulation in the bootstrap: 1. CheckInstallStep was mounted twice: once inside initializeControllers() and once unconditionally at the top level. The top-level mount has to stay because the install wizard must be reachable before MONGODB_URL is configured. Removed the inner mount so each startup registers it exactly once. 2. fs.watchFile on .env called initializeControllers() on every save, which re-ran .init(app) for ~60 route modules onto the same Express app instance (Express has no clean route un-registration) and spun another setInterval inside startInterval(). Removed the watcher; nodemon already restarts on file changes in dev, and production env changes require a process restart anyway. After this change initializeControllers() is only invoked once, from the MONGODB_URL startup gate. Closes #41 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: preserve dataType and options across JWT refresh retry (#38) apiRequest and apiRequestWithoutCompnay reconstruct the call after a token refresh using only (type, endPoint, data), dropping dataType and options. Any retry of a multipart upload then runs through the JSON axios instance instead of the form-data instance, and any caller-supplied abort signal or per-request option is lost. Forwarded dataType and options into both retry calls so the replay matches the original. Closes #38 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: clean up close_click IPC listener after each notification (#39) sendNotification() registered a new ipcMain.on('close_click', ...) handler on every screenshot capture and never removed it, so the process accumulated one global IPC listener per screenshot and a single close click eventually fired N stale handlers, each trying to close an already-destroyed BrowserWindow reference. Switched the registration to ipcMain.once so the happy path (user clicks close) auto-removes the listener. For the auto-timeout path (window closes after 10s without a click) the handler stays registered and would later steal a future notification's close event, so also remove it explicitly when the window emits 'closed'. The timeout id is now tracked and cleared on the same hook so the timer can't fire against a destroyed window. The window reference is captured in a local so each listener targets its own window even if a subsequent sendNotification() reassigns the module-level screenshotNotificationWindow before this listener fires. Closes #39 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: allow editing the "Created by" user on a task The task sidebar Details panel previously showed Created by (the Task_Leader field) as a read-only user — set once at creation and never changeable. Add an inline picker mirroring the existing Assignee field so the creator can be reassigned. Changes: - Backend: new updateTaskLeader action on task_class_Mongo. Validates the new Task_Leader, writes via $set, emits the same socketEmitter.emit('update', { module: 'task', updatedFields }) used by updateAssignee, and logs a TaskLeader_Changed history entry via HandleHistory. Includes the isUpdateTask:false side-effects-only branch for parity with updateStatus / updatePriority. - Frontend store (TaskOperations): new updateTaskLeader action that optimistically commits the new Task_Leader into the projectData Vuex store, then PATCHes /api/v2/tasks with action 'updateTaskLeader'. - TaskDetailRightSide.vue: replaced the read-only Created-by block with an Assignee picker (single-select) for users holding the task.task_assignee + task.task_list permissions, preserving the original read-only display as the fallback for users without those permissions. Wired @selected to a new updateTaskLeader() handler with the same toast / error pattern as updateAssignee. - i18n: added Toast.Created_by_updated_successfully and Toast.Created_by_not_updated to the English locale. The other 10 locales fall back to English via vue-i18n until translators backfill — flagged as a follow-up. Permission policy: reuses task.task_assignee. Anyone allowed to change the Assignee can also change Created by. No new permission key, no role-permission migration. Backend has no per-field gate, consistent with the existing Assignee / Status / Priority update paths which all trust the frontend permission gate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: split header notifications into Unread and Archive views Previously the notification bell fetched every notification the user had ever received (read or unread) in a single combined list, making it hard to focus on actionable items. This change splits the dropdown into two views — Unread (default on every open) and Archive — gated by a single toggle button in the dropdown head. Changes: - Backend: GET /api/v1/app-notification/notification accepts a new optional `filter` query param ('unread' | 'archived'). 'unread' adds `notSeen: { $in: [userId] }` to the aggregation match; 'archived' adds `notSeen: { $nin: [userId] }`. Default is 'unread' to keep the bell focused on actionable items. Refactored the match clauses into a `baseMatch` array so the filter is appended cleanly; query shape and pagination are otherwise unchanged. - Header.vue: added a `notificationFilter` ref ('unread' by default), a "View Archive" / "View Unread" toggle in the dropdown head, a `switchNotificationFilter()` handler that resets paging state and refetches, and an `openNotificationsDropdown()` helper that the bell click handlers now use so each fresh dropdown open lands in Unread. `markRead()` and `markAllRead()` clear the read items from the local list while the Unread filter is active so the dropdown reflects the filter without an extra refetch. The "Mark all as read" button visibility now keys off `notifications.length` (the list is already filtered to unread on this view) rather than the server-side `totalNotification` counter, which can lag and falsely hide the button. - i18n: added Header.View_Archive / Header.View_Unread to the English locale; other locales fall back to English via vue-i18n until translators backfill (flagged as a follow-up). Out of scope (deferred per user instruction): goals 3 and 4 from the original spec — creator-prefs fire policy verification and email preference parity audit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: honour project-level Ignore for task creator and company owner Closes goals 3 & 4 of the notification refactor. Two parallel bypasses in handleNotification.HandleBothNotification let task creators and company owners through the project-level watcher filter (the Ignore / All Activity / Participating setting in the List of Watcher panel): - Task branch (taskId path): Task_Leader was Set-union'd into the recipient list AFTER `projectData.watchers` had already filtered out any user set to "ignore". So a creator who had set the project to Ignore still ended up in `assigneeUsers`, and the downstream per-event preference check (NOTIFICATIONS_SETTINGS — a separate preference layer that defaults email=true) emitted both an in-app and an email notification. Now Task_Leader is included only if their project-watcher setting is not "ignore". - Project branch (type === 'project'): same pattern — companyOwnerId was union'd in unconditionally, bypassing the watcher filter. Honours "ignore" too. This explains the goal-4 symptom of "I'm getting emails for events I disabled": the disablement was set at the project-watcher layer, but the bypass routed the recipient straight to the per-event layer where email was still on. Other recipient paths are untouched. Default behaviour for users who have not chosen "ignore" is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: per-company screenshot retention policy + nightly cleanup cron Adds an opt-in retention policy on the time-tracker screenshot pipeline. Visible on /settings/setting to the company owner (roleType === 1) only. When enabled, a nightly cron permanently deletes trackshots older than the configured window (3 / 6 / 12 / 24 months) from both the per-tenant TimeSheet.trackShots subdoc array and from Wasabi (main object + every thumbnail variant). Backend ======= - utils/mongo-handler/schema.js: new `screenshotRetention` Map field on the global companies schema. Stores `enabled`, `maxAgeMonths`, `enabledAt`, `enabledBy`, `lastRunAt`, `lastRunStats`, `firstRunCompletedAt`, `runningSince`. Backward-compatible — legacy companies without the field read as `enabled: false`. - Modules/ScreenshotRetention/: new module. helper.js — policy read/write, preview counter, per-company cleanup workflow, and the cron entry point. Production-ready guarantees: * Wasabi delete BEFORE db $pull so transient Wasabi failures leave the db record intact and the next nightly run retries (no permanent orphans). * Per-trackshot main + 4 thumbnail keys deleted (sizes hard-coded from thumbnail.json). * Filters by trackshot.screenShotTime (epoch ms), not parent TimeSheet timestamp. * Advisory `runningSince` lock prevents double-runs; stale locks (>4h) are reclaimed. * First-run safety cap (50k deletions) for the initial cleanup on legacy data; lifted once `firstRunCompletedAt` is stamped. * Bounded company concurrency (5 in parallel) via Promise.allSettled. controller.js — three endpoints with owner role check: GET /api/v1/screenshot-retention GET /api/v1/screenshot-retention/preview PUT /api/v1/screenshot-retention Owner check looks up the per-tenant `company_users` doc for the caller and confirms `roleType === 1`. Returns 403 on mismatch. routes.js — endpoint registration. init.js — module bootstrap (matches existing convention). - index.js: register the new module beside the rest of `initializeControllers()`. - cron.js: removed the broken `cleanUpTrackshot()` call (was referencing an unimported binding and never executed). Added a new schedule at 00:30 UTC that invokes `screenshotRetention.runRetentionForAllCompanies()`. Off-peak vs the other midnight jobs so a heavy cleanup doesn't compound with the bucket-size + AI-reset jobs on the same minute. Frontend ======== - frontend/src/components/molecules/Setting/SettingScreenshotRetention.vue: new card mounted on /settings/setting. Renders only for `companyUser.roleType === 1`. Toggle + retention-window dropdown + last-run telemetry. Enabling fires a SweetAlert confirmation that shows the preview-count from the new GET preview endpoint so the owner knows exactly what will be deleted on the next nightly run. - frontend/src/views/Settings/Setting/Setting.vue: mount the new component in the existing right-hand column. Self-hides for non-owners. - frontend/src/locales/en.js: new ScreenshotRetention.* keys (heading, toggle/window labels, confirmation copy, last-run telemetry). Other locales fall back to English via vue-i18n. Out of scope (deferred) ======================= - Translation backfill for non-English locales. - A `RetentionAuditLog` collection for per-run history beyond `lastRunStats`. The cron emits structured logs in the meantime. - A "dry run" mode that lets the owner see what would be deleted without enabling the policy. The preview endpoint already gives the count; a per-record preview would need its own UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: comment out unused companyId injection in SettingScreenshotRetention component * feat(setup): one-command developer setup via `npm run setup` Adds an additive setup orchestrator that installs deps, builds the wizard, bootstraps .env, starts backend + frontend, completes the installation wizard headlessly, creates a default admin account, and opens the login page — all from a single command. Non-technical contributors can go from `git clone` to a working login screen with no manual steps. New - scripts/dev.js — orchestrator with HTTP-based wizard auto-completion, MongoDB probe with retry, defensive .env patching, credentials banner. - nodemon.json — explicit watch list (server-side dirs / .js only) so wizard writes to installationSteps.json no longer restart the backend mid-request (root cause of the "wizard reloads on MongoDB step" bug). - package.json — `setup`, `dev`, `setup:reset` scripts + nodemon devDep. Wizard improvements (back-compat preserving) - Modules/CheckInstallStep/controller.js: `isDoItLater` support added for Firebase (step 4) and SMTP (step 6), mirroring the existing AI (step 5) skip pattern. Both steps remain mandatory unless the caller opts in. - Defensive APIURL fallback at module-load (was crashing the backend on fresh clones if .env hadn't loaded yet — TypeError on .substring of undefined). .env.example - SERVICE_FILE corrected from "../firebase-adminsdk.json" to "./firebase-adminsdk.json" (the prior default tripped the BUG-036 path-traversal guard and blocked the Firebase wizard step). - Quick-start comment block at the top. Nothing existing changes - `npm start`, `npm run nodemon`, `npm run basic-install`, the interactive wizard, and the documented manual setup paths are all untouched. - New scripts never run automatically; users must invoke them explicitly. - Fallback chain at every failure point (MongoDB unreachable / auto-setup error / --manual flag) opens the interactive wizard UI so the user is never left in an unrecoverable state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(screenshot-retention): security + correctness pass on PR #157 Independent review of the original commit surfaced three blockers and several correctness issues. This is a follow-up commit on the same branch that addresses all of them. BLOCKERS ======== 1. **Routes had no JWT middleware.** Config/setMiddleware.js gates auth via path allowlists. The new `/api/v1/screenshot-retention` and `/api/v1/screenshot-retention/preview` paths were NOT listed in either `verifyJWTTokenWithCRoute` or `verifyJWTToken`, so the endpoints shipped completely unauthenticated — anyone on the internet could toggle retention for any company. Added both paths to `verifyJWTTokenWithCRoute` so the JWT middleware fires and populates `req.uid` / `req.aud`. 2. **Thumbnail keys were generated in the wrong dimension order.** The upload-time call at Modules/storage/wasabi/controller.js:298 passes `(thu.height, thu.width)` into a function whose params are `(width, height)`, so the stored filename is effectively `<base>-<height>x<width>.<ext>`. The cleanup helper was building `<base>-<width>x<height>.<ext>` and missing every thumbnail variant on every retention run — leaving four orphan Wasabi objects per trackshot, permanently. derivThumbnailKeys now mirrors the upload-time swap with a comment explaining the pre-existing bug in the upload path (out of scope to fix here). 3. **Cursor-based pagination replaces skip-based scan.** The previous loop advanced `pagedSkip += docs.length` after each batch, but the `$pull` inside the loop removes docs from the match set — meaning the next page's skip lands past where it should. Most of the DB was being missed on legacy-data tenants. Replaced with `_id > lastSeenId` cursor that's immune to in-flight match-set shrinkage. SECURITY ======== 4. **Controller now reads userId from `req.uid`** (set by Config/jwt.js#checkToken from the verified token's `uid` claim), not from `req.body.userId`. The old code let a non-owner pass the owner's userId in the body and pass the role check. Status codes updated: missing userId → 401 (authentication required) rather than 400. HIGH ==== 5. **Legacy string-typed `screenShotTime` is now handled.** Multipart form uploads coerce numbers to strings at write time, so the `screenShotTime` field on legacy trackshots is stored as a string. The previous `$lt: cutoffMs` (number) didn't match strings. - `countOldTrackshots` now uses `$convert` with `to: 'long', onError: null` inside the $filter so the comparison works regardless of the field's actual storage type. - `runRetentionForCompany`'s loose DB query + strict in-memory filter coerces via `Number(t.screenShotTime)` and skips non-finite values. 6. **`firstRunCompletedAt` is now stamped only when a real cleanup completed.** Previous condition `deletedCount < cap` stamped the marker on a zero-deletion run (e.g. a new company with no eligible data), which lifted the first-run safety cap before any real bulk data accumulated. New condition: scan exhausted (no cap hit, no error) AND deletedCount > 0. 7. **Empty-image trackshots no longer inflate `deletedCount`.** The scan loop now pre-filters them out (`oldShots` requires `t.image`), and `deleteTrackshotObjects` returns `skipped: true` rather than `mainDeleted: true` if it's ever called with a missing key. Stats now track `skippedCount` separately. MEDIUM ====== 8. **Removed the no-op `updateCompanyPolicy(companyId, {}, {})` call** that hit the empty-patch early-return at line 176 without writing anything. Lock acquisition is a single `stampMasterField` call, which is at least document-atomic. Cluster-aware leader election is still out of scope (documented). 9. **`lastRunStats` now records partial-run errors and cap-hit state.** New stats fields: `skippedCount`, `hitCap`, `error`. Ops can tell from the master doc whether the run completed cleanly, hit the cap, or threw mid-loop. 10. **Frontend now uses `watch(isOwner, …, {immediate: true})`** instead of one-shot `onMounted`. The Vuex `companyUserDetail` store sometimes hydrates after the component mounts on soft route changes; the one-shot mount would never call `loadPolicy()` in that case and the card stayed on hard-coded defaults forever. LOW === 11. **`extractKey` regex tightened** from `[a-f0-9]{20,}` to `[a-f0-9]{24}` (exact ObjectId length) and now decodes URL path segments so percent-encoded keys map back to their canonical storage form. 12. **`getS3Client` doc + body cleaned up.** `s3Client` is not exported from `wasabi/controller.js`, so the old "fall back if not available" branch was always taken. Removed the misleading primary path; we just build the client from `awsRef` and memoise it. Test plan addendum ================== - Hit `GET /api/v1/screenshot-retention` without a JWT → 401. - Hit `PUT /api/v1/screenshot-retention` with a forged body `userId` of the owner from a non-owner JWT → 403 (controller uses `req.uid`, not body). - On a tenant with legacy string-typed `screenShotTime`: - GET /preview returns a non-zero count for the configured window. - A nightly run deletes those records + their (height×width-named) thumbnail variants. - Confirm thumbnail keys deleted in Wasabi by listing the bucket after a run — there should be no orphans for the deleted main keys. - Confirm pagination handles a tenant whose every TimeSheet has at least one expired trackshot (all docs drop from match set on first $pull): the cursor variant should still terminate cleanly and process every doc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(security): P0 + partial P1 hardening, CI pipeline, multer v2 Closes the critical security and dependency gaps surfaced in the codebase audit. All 20 unit tests pass; no behavior change for normal request paths. Backend security - Sanitize regex search inputs across 7 controllers (AI, AdvancedGlobalFilter, Comments, MediaFiles, Project filter, notification-count, trackerDownload) via new utils/escapeRegex.js. Blocks NoSQL regex injection / ReDoS / cross-tenant name enumeration. - Add helmet + global express-rate-limit in index.js. CSP disabled to preserve the Vue inline-script setup; secure / sameSite already env-gated correctly. trust-proxy set to 'loopback' to silence the ERR_ERL_UNEXPECTED_X_FORWARDED_FOR warning behind a same-host reverse proxy. - Harden all 4 multer call sites via new utils/uploadConfig.js (DEFAULT_LIMITS, safeFileFilter blocking executable extensions, safeRelativePath for traversal protection). bucket.helper.js storage destination now validates path before writing. - New requireCompanyAud middleware (Config/jwt.js) enforces the JWT `aud` claim against any companyId in body / params / query / header on the ~50 verifyJWTTokenV2-only routes. Non-ObjectId values (e.g. USER_PROFILES global bucket) pass through. Dependencies - Remove aws-sdk v2 from package.json (EOL Sept 2025); only @aws-sdk v3 was actually imported. - Bump multer 1.4.5-lts.1 -> 2.1.1 to clear known 1.x CVEs. Schema strictness (P1-SEC-11) - utils/mongo-handler/createSchema.js: flipped 7 core schemas to `strict: true` (tasks, comments, timesheet, history, adminDetail, subscriptionPlan, globalCustomFields). Kept `strict: false` on 7 intentionally-dynamic schemas (notification counters, Chargebee webhook mirrors, custom-field definitions, plan-feature maps) with inline comments explaining why. CI / DevOps - .github/workflows/main.yml: added a `validate` job (npm test, npm audit advisory, frontend install + build) that the deploy job now `needs:`. Previously deploys ran with zero validation. Diagnostics - modules/storage/wasabi/controller.js: replaced 6 generic `Error while upload file: ${error}` rejects with a formatS3UploadError helper that logs the AWS error Code, HTTP status, bucket, key, and requestId. Documented for follow-up - Auth/controller.js: TODO comments at both res.cookie sites explaining why httpOnly stays false until the frontend stops reading the cookie via js-cookie (P1-SEC-09 deferred). Pre-existing fixes pulled in - tests/utils/imageGuard.test.js: corrected env var names so the suite goes from 19/20 to 20/20 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(safeServiceFile): reject Windows absolute paths on any host The CI pipeline added in the same PR is the first thing to run tests on Ubuntu. `path.isAbsolute('C:\Windows\...')` returns false on Linux, so the safeServiceFile absolute-path guard fell through and the request hit the `.json` extension check instead. The test in `tests/utils/safeServiceFile.test.js:46` expects the "relative" error in both shapes and was passing on Windows hosts by accident. Use `path.win32.isAbsolute(...)` alongside the platform-bound `path.isAbsolute(...)` so both POSIX (`/etc/hosts`) and Windows (`C:\Windows\...`) absolute paths are rejected regardless of which OS the process runs on. This is also a defensive improvement to BUG-036: a Linux deployment can no longer be coaxed into re-resolving a Windows-shaped path inside the project root. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Update main.yml * Update main.yml * feat(ai-project-generator): one-shot AI project bootstrap with PDF brief + SSE progress Adds a parallel "Create with AI" path to the existing manual project wizard. The user describes their project in natural language (optionally attaches a PDF/DOCX/MD/TXT brief and chooses public/private workspace + a target task count), the LLM returns a plan (project metadata + folders → sprints → tasks with rich descriptions), the user reviews/edits names, and on "Create everything" the orchestrator commits the whole hierarchy via the same write paths the manual flow uses — with SSE progress events and rollback on partial failure. == Backend == - New module Modules/AIProjectGenerator with: * llmProvider/ — env-selected adapter (openai | anthropic). Reads AI_API_KEY/AI_MODEL (existing) for OpenAI and ANTHROPIC_API_KEY/ ANTHROPIC_MODEL (new) for Anthropic, picked via LLM_PROVIDER. * briefExtractor.js — multer v2 disk-buffer + pdf-parse + mammoth, 10MB cap, mime allow-list, control-char strip, 100k char truncation. * schemaValidator.js — zod PlanSchema with strict task/status/folder rules; sanitizes member ids against the active company roster. * promptTemplates.js — system + user + repair prompts. Forces 4-8 tasks per sprint, domain-specific status names, full lifecycle coverage. * orchestrator.js — sequential project → folders → sprints → bulk tasks. Reserves a taskKey range atomically, emits SSE per step, soft-rollbacks in reverse on any failure (tasks → sprints → folders → project + projectCount decrement). * sseEmitter.js — heartbeat-equipped SSE channel keyed by random jobId. * Endpoints: /api/v1/ai/project/{upload-brief,plan,clarify,execute} + unauthenticated /api/v1/ai-progress/:jobId (jobId is a bearer capability — same pattern as /api/v1/generatePrompt/events). - Config/setMiddleware.js: protected new auth paths via verifyJWTTokenWithCRoute; SSE endpoint deliberately omitted. - index.js: register the new module after Modules/AI. - .env.example: LLM_PROVIDER, ANTHROPIC_API_KEY, ANTHROPIC_MODEL, LLM_MAX_TOKENS_PLAN, LLM_MAX_TOKENS_CLARIFY. - package.json: +@anthropic-ai/sdk, +pdf-parse, +mammoth, +zod. == Frontend == - New AiProjectCreator.vue (organism) — 3-step sidebar: 1. Describe: textarea (20-char minimum), public/private workspace toggle (mirrors manual ProjectWorkspace step), target-task-count slider, PDF/DOCX/TXT/MD upload, inline clarification Q&A. 2. Review plan: project icon/code/description preview + inline-editable names at every level (project / folder / sprint / task), expandable task descriptions. 3. Execute: live SSE progress UI (project → folders → sprints → tasks) with rollback-aware error state and "Open project" CTA on complete. Close affordance is a close icon (was a Cancel button) — disabled while uploading/loading; the sidebar can't be dismissed during the execute phase to prevent orphaned in-flight jobs. - New aiProjectGenerator.js composable wraps the four endpoints + EventSource subscription. - Projects.vue / ProjectListComponent.vue / ProjectListing.vue add the "✨ Create with AI" button beside "+ New Project", gated on currentCompany.planFeature.aiPermission. - env.js: 5 new endpoint constants. == Highlights from QA pass == - projectIcon is persisted in the canonical {type:'color', data:'#hex'} shape Item.vue expects, so the AI-bootstrapped project's color/initial pill renders in the sidebar (was an empty box). - Multer rejection (LIMIT_FILE_SIZE, unsupported mime, etc.) is caught before Express's default handler so users see "File is too large. Maximum allowed size is 10 MB." instead of "Request failed with status code 500". - The user's explicit public/private choice in step 1 is forced onto the plan server-side at /plan, /clarify and /execute — it always wins over whatever the LLM emitted. - Plan-feature quota (projectCount.*) is incremented like the manual flow but the AI path intentionally skips checkProjectPlan's hard cap so this feature remains usable on plans that limit project counts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(main.yml): map VUE_APP_* secrets into the frontend build step The CI frontend build was running with no .env present (gitignored, not materialised on the runner) so every `process.env.VUE_APP_*` reference was inlined as `undefined` in the bundle. Map the Firebase keys, storage config, support-routing ids, and OAuth feature flags as repository secrets in the Build frontend step so the validation build matches what production actually ships. Also bumps Node to 22, switches to npm ci, adds concurrency cancellation for staging deploys, and tightens the workflow trigger list. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(env): update AI model to gpt-4o-mini in .env.example * refactor(projects): split Projects.vue and Task.vue mega-components (#167) Projects.vue: 2,737 -> 1,365 lines (50% reduction). Task.vue: 1,015 -> 540 lines (47% reduction). - 9 composables extracted (useProjectCalendar, useProjectAvatar, useEmbedViews, useProjectLifecycle, useProjectAssignee, useProjectTour, useProjectSearch, useProjectRules, useProjectNameEdit) - 5 sub-components extracted (ProjectActionsBar, ProjectFiltersToolbar, ProjectSidebars, ProjectBottomModals, ProjectEmptyState) - 7 view components (ListView, Comments, ActivityLog, WorkloadView, BoardView, ProjectDetail, TableView, EmbedViewItem) converted to defineAsyncComponent so each tab pulls its own chunk on demand - 2 task composables (useTaskMutations, useTaskActions) and 1 sub-component (TaskQuickMenu) extracted; duplicate mobile/desktop quick-menu markup deduped Bundle (npm run build): main project chunk 2,621,285 -> 2,047,548 bytes (-22%, -560 KB). New lazy chunks: project-list-view 381 KB, project-detail 117 KB, project-kanban 42 KB, project-table-view 28 KB, project-workload 17 KB, embed-view 3 KB, project-activity-log 0.5 KB. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(backend): split task/logtime/milestone/auth mega-files (#164) Splits four oversized source files into focused sub-modules so no file in the affected modules exceeds ~640 lines (issue target: ~800). Public API is preserved: each original module path keeps re-exporting every symbol it used to, so routes.js and downstream consumers are untouched. - Modules/Tasks/helpers/task_class_Mongo.js (3,129 -> 27 lines) Mixin pattern under taskMongo/: create, updateBasic, updateAssignment, updateMeta, structural, mergeDuplicate, internals. Methods merge into Task.prototype via Object.assign so internal `this.X(...)` calls and the `taskMongo` singleton work unchanged. - Modules/LogTime/controllerV2.js (1,739 -> 12 lines) Re-export index over controllerV2/: helpers (3 utilities used by Tasks/EstimatedTime), manualLogtime, tracker, capture, timelog. Internal `exports.updateRemainingTime(...)` etc. rewritten to direct calls now that the utilities live in a sibling helpers.js. - Modules/Milestone/controller.js (1,193 -> 10 lines) Re-export index over controller/: helpers (6 notification/history utilities), crud, status, query. - Modules/Auth/controller.js (1,061 -> 11 lines) Re-export index over the existing controller/ folder, adding authHelpers (5 internal helpers, incl. the externally-imported addAndRemoveUserInMongodbNotificationCount), register, loginSession, password. Verification: jest 20/20 passing; smoke-checked all 80 original exports remain reachable on their original module paths; loaded routes.js and every downstream consumer (Company/controller, CheckInstallStep/createCompany, EstimatedTime/controller, Auth/controller/createUser) without errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(auth): relocate stranded verify*Auth helpers + repair inline require paths (#164) Follow-up to 5c24855. The original splitter only tracked top-level `exports.NAME = ...` declarations, so five non-exported helpers between `registerAuth` and `verifyAuth` in the source file (verifyGithubAccessToken, verifyGithubAuth, verifyGoogleIdToken, verifyGoogleAuth, verifyLocalAuth) got swallowed into `registerAuth`'s line range and ended up in `controller/register.js`, while their only caller — `verifyAuth` — landed in `controller/authHelpers.js`. Every login attempt threw `ReferenceError: verifyLocalAuth is not defined` at runtime. The previous smoke test missed it because it only checked `typeof exports.verifyAuth === 'function'`, which is true even when calling the function blows up. Changes: - Move the 5 verify*Auth helpers into authHelpers.js, placed before verifyAuth so their definitions are in scope at call time. - Patch inline `require()` paths that the preamble-rewriter didn't touch (they live inside function bodies, not at file top): * authHelpers.js: `../Template/passwordExpiredMail` -> `../../Template/passwordExpiredMail` * authHelpers.js: `../Template/forgotPassword` -> `../../Template/forgotPassword` * Drop the inline `const logger = require("../../Config/loggerConfig")` inside verifyGoogleAuth — it was already redundant in the original (logger is imported at the top) and now also pointed to a non-existent depth. - register.js trimmed to 31 lines (preamble + registerAuth only). Verification: - New smoke test actually invokes `verifyAuth` with each authProvider ('local', 'github', 'google') and asserts the early-return validation message — proving every verify* helper symbol resolves at runtime. - Verified the two inline template require specifiers resolve to real files via `require.resolve`. - Grep-confirmed no stale `../Template/` or `../../Config/` paths survive in any Auth sub-file. - `npm test` 20/20 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ai-project): default ProjectType to "Fix" to match manual flow The two valid project types in AlianHub are "Fix" and "Hourly" (see frontend/src/components/atom/ProjectType/ProjectType.vue). The manual project creation flow hardcodes "Fix" on submit (CreateProjectSidebar.vue, TemplateAllDetail.vue), and downstream features like milestones (ProjectDetail.vue) only branch on those two values. The AI project generator was instead: - Defaulting to "General" in the orchestrator and Zod schema - Telling the LLM to invent ProjectType values like "Software", "Marketing", "Operations" — none recognised by the rest of the app Changes: - orchestrator.js: always assign ProjectType: "Fix" (drop LLM-supplied value). - schemaValidator.js: default changed from "General" to "Fix" (defence in depth for any other consumers of the schema). - promptTemplates.js: removed the ProjectType field from the LLM schema description so the model doesn't waste tokens generating an ignored value. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf(mongo): bound the per-tenant Mongoose connection pool (#162) Previously the per-tenant connection registry grew unbounded — every active company kept a Mongoose connection forever (helper.js had a 30min idle sweep, but no cap and the threshold was hardcoded). Under heavy multi-tenant load this leaked sockets and RAM. - Add LRU eviction in handleConnection: before opening a new connection, evict the least-recently-used entry that's outside a 5s grace window (the raw Connection is returned to callers, so we must not close one that may still be servicing a query). - Make the idle sweep env-configurable: TENANT_CONNECTION_IDLE_MS and TENANT_CONNECTION_SWEEP_MS, defaults preserve the prior 30min/5min behavior. Iterate in reverse during sweep to avoid splice-skip. - Centralize cleanup in closeAndRemove helper (also drops two debug console.logs that printed the full connection list every sweep). - Document MAX_TENANT_CONNECTIONS (default 100), TENANT_CONNECTION_IDLE_MS, and TENANT_CONNECTION_SWEEP_MS in .env.example. Fresh setups pick these up automatically via scripts/dev.js's full-file copy; existing .envs rely on code defaults (no patching of patchMissingEnvKeys — these are optional tuning knobs, not boot-required). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(logging): replace console.log with Winston logger across modules (#165) (#177) Replace all 56 console.log occurrences across 15 files in Modules/ with the existing Winston logger (Config/loggerConfig.js), applying correct log levels: error for catch blocks, warn for rejected batch items, and info for progress/completion messages. Add logger import to files that lacked it: Tasks/routes.js, Project/controller/getSprintFolder.js, CheckInstallStep/controller.js, and CheckInstallStep/initalizations.js. The console.log inside the Firebase service-worker template string in CheckInstallStep/controller.js is intentionally preserved — it is browser-side code written to a .js file, not server-side Node.js. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(tasks): multi-select bulk actions across List/Kanban/Table views Adds an end-to-end ClickUp-style bulk-action experience to the project task views with full parity to single-task semantics. Frontend - New Vuex module `TaskSelection` + `useTaskSelection` composable; selection auto-clears on view/project switch. - Hover/selected checkboxes added to ItemList rows, sprint headers, Kanban cards/columns, and Table rows/group headers — all gated by the same `checkPermission` keys the single-task path uses. - Kanban drag is disabled when 2+ tasks are selected so multi-move goes through the bar (no ambiguous single-vs-multi drag). - New `BulkActionBar` (with `BulkMenu` sub-component) mounted in Projects.vue. Single-open menu state with a capture-phase document listener that ignores teleported popups (.dp__menu / sidebar / drop-down-menu) so the calendar and assignee pickers work cleanly. - Internal search in Status / Priority / Assignees / Tags menus. - Assignee + tag rows: click to add, X icon to remove, with tri-state (`none` / `some` / `all`) computed against the current selection. - Due date uses the existing `DueDateCompo` calendar, center-aligned in a custom wrapper. - Optimistic store updates for delete/archive (deletedStatusKey + sprint count adjustment) and for status/priority/assignee/tags/ due-date so the UI reflects changes immediately, matching the single-task pattern. - Skipped/error toasts surface backend permission/scope failures with human-readable reasons. Backend - New `Modules/Tasks/helpers/taskMongo/bulk.js` mixin with bulkUpdateStatus, bulkUpdatePriority, bulkUpdateAssignee, bulkUpdateDueDate, bulkUpdateStartDate, bulkUpdateTags, bulkArchive, bulkRestore, bulkDelete, bulkMove, bulkDuplicate. - New POST /api/v2/tasks/bulk route with dynamic action dispatch, mirroring the existing PATCH /api/v2/tasks pattern. companyId is taken from the verified header (never the body) to prevent spoof. - Each bulk method does the DB write directly via updateMany for reliability, then calls HandleHistory + HandleBothNotification per task so activity logs and notifications match what N single calls would produce. - Per-task `socketEmitter.emit('update', ...)` is fired with a merged post-update task doc so taskSocket.js fans the change out to all clients viewing that project+sprint room in real time with the correct new values (not stale ones). - bulkArchive/bulkDelete/bulkRestore go through the existing `updateArchiveDelete` helper to preserve subtask cascades, parent count updates, sprint reconciliation, and comment count cleanup. Permissions (gated at three layers) - Selection visibility: hover checkbox only renders if the user has `task.task_status` on that project. - Action button gating: each bar action ties to the existing per-action permission key (task.task_delete, task.task_priority, task.task_tag, etc.); buttons disable with a tooltip. - Backend re-check: companyId scoping via loadScopedTasks drops cross-tenant ids into `skipped[]` before any write. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(aipg): collapse plan flow to one-shot, drop clarification + task-count slider Removes the multi-turn clarification round-trip that was causing "Conversation not found or expired" errors on staging-app — the proxy intermittently 504s on /api/v1/ai/project/clarify and by the time the user retries, the cached conversation has expired so the request 404s. What changed: - /api/v1/ai/project/clarify is replaced with a 410 Gone stub (kept for one deploy cycle so any cached frontend that still POSTs there gets a clear "retry /plan" response instead of a 404). - /api/v1/ai/project/plan is now a single round-trip. The conversation cache (myCache "convo:" keys), conversationId param, and clarifyRound bookkeeping are gone. - Frontend "Quick questions to sharpen the plan" card removed. - Frontend "Target task count" slider card removed. The model now picks the task count entirely from the description + ruleset (4-8 tasks per sprint, hard ceiling of 100 plan-wide). - The error UI on step 1 now shows a retry hint and the primary button re-labels itself to "Try again" — clicking it just re-runs the same /plan call. No state to lose between attempts. System prompt change: - "Clarification rule" now says: never ask questions, always set needsClarification=false, fill in reasonable defaults for any unclear bits and note assumptions inside task descriptions. - "Target task count floor" language stripped; let the model pick. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(table): adjust table layout and spacing for improved readability * Fix comment duplicates and preserve drafts * feat(ai-project-generator): implement async project plan generation with SSE support * perf(socket): phase 1 quick-wins for real-time scalability Five low-risk, backend-only optimizations to the socket event pipeline and MongoDB connection layer. Wire protocol (event names, payloads) is unchanged — no frontend changes required. Fix #2 — Namespaced socket events Wrap the internal EventEmitter so payloads tagged with `module` also publish a `<module>:<event>` event. Migrate the 5 socket controllers and the notification middleware to subscribe to the namespaced form. Stops every task/comment/companies/notification handler from waking up on every mutation across the system. Fix #3 — Remove JSON.parse(JSON.stringify) from socket hot paths Drop five deep clones across taskSocket / commentSocket / companiesSocket that existed only to coerce ObjectId to string. Field access works directly on the doc; template literals already call toString(). Fix #4 — Auto-cleanup socketRef.rooms on disconnect Listen to Socket.io's native `disconnect` event to purge stale room entries. The existing `disconnectNameSpace` flow only fires when the client explicitly emits it — browser close / network drop / mobile background kill previously left dead entries piling up forever. Fix #6 — MongoDB pool sizing Bump maxPoolSize 3 -> 10 (env: MONGO_POOL_SIZE), add minPoolSize 2 (env: MONGO_MIN_POOL_SIZE). The 3-connection cap queued any 4th concurrent query per tenant — task-heavy flows easily saturated it. Fix #13 — Fail fast on queue saturation Reduce waitQueueTimeoutMS 30000 -> 5000 (env: MONGO_WAIT_QUEUE_TIMEOUT_MS). A queued query that can't get a socket inside 5s is almost always doomed; failing fast surfaces the real problem instead of pinning a request worker for 30s. Verification - npm test: 20/20 pass - node --check clean on all 9 touched files - Namespaced emitter routing smoke-tested - Confirmed no `socketEmitter.on('update'|'insert'|...)` legacy subscriptions remain anywhere in the codebase - Frontend socket inventory audited — every emit/on event name is preserved on the wire; no client changes required See .claude/SOCKET-PERFORMANCE-PLAN.md for the full multi-phase plan and the remaining fixes scheduled for phases 2-4. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * perf(socket): phase 2 core refactor — Map-based room index Phase 2 of SOCKET-PERFORMANCE-PLAN. Backend-only; wire protocol unchanged. Fix #1 — Map-based room index (socket/helper.js) Replace the linear `exports.rooms = []` array (scanned by .filter() on every event) with two Maps: byPrefix : prefix -> Map<roomName, entry> (O(1) handler lookup) bySocket : socket -> Set<roomName> (O(rooms/socket) cleanup) Room names already follow `<prefix>**<socketId>`; we index by exactly that prefix. New API: upsertRoom / removeRoom / removeBySocket / findRoomsByPrefix / findRoomsByPrefixes. Fix #5 — data.socket.rooms.has(data.roomName) liveness guard Replaces `Array.from(data.namespace.adapter.rooms.keys()).filter(...)` inside every handler. socket.rooms is a small Set (3-5 entries per socket); .has() is O(1) vs the previous full-namespace scan. Fix #7 — Shared upsertRoom helper Idempotent on roomName via Map.set semantics — tab refresh / reconnect no longer creates duplicate index entries that fired the same event to the same room multiple times. Replaces hand-rolled findIndex / push-or-replace dedup that was inlined inconsistently across the 5 controllers. Fix #9 — Cache getTotalSprintCount (modules/Tasks/helpers/mongo_helper.js) Wrap the aggregate-pipeline + plan-feature check in node-cache with a 30s TTL keyed by `sprintPlanCheck:<companyId>:<sprintId>`. Trade-off (documented inline): up to 30s of over-allocation after limit hit, or 30s of denial after a plan upgrade. Acceptable for soft plan caps. socket/socketinit.js Drop `exports.rooms = []`. Disconnect handler now calls helper.removeBySocket(socket). disconnectNameSpace simplified from the recursive countFunction to a plain forEach over adapter.rooms. Verification - npm test: 28/28 pass (20 existing + 8 new tests/socket-room-index.test.js) - .claude/tests/smoke-phase2.js: emitter -> namespaced handler -> Map index -> namespace.to().emit() flow verified end-to-end - All touched files `node --check` clean - Frontend audit (frontend/src/**): all wire events (joinX/leaveX, taskX, chatTaskX, commentX, companiesX, userIdNoticationUpdate) and room-name `<prefix>**<socketId>` convention unchanged Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(tasks): resolve list-view assignee TypeError after Task.vue refactor The Task.vue mega-component split (#167) extracted changeAssignee into the useTaskMutations composable and passed assigneeInProgress through the template as a third argument. Vue 3 auto-unwraps top-level refs in templates, so the composable received the inner {} object instead of the ref — making assigneeInProgress.value undefined and throwing "Cannot read properties of undefined (reading '<userId>')" on every assignee click in list view. Move the ref into the composable (matching useProjectAssignee.js) so closure ownership replaces the broken template hand-off. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * In progress changes commit * fix(ai-project-generator): unblock execute hang, fix list rendering, tighten prompt splitting Bug fixes: - orchestrator: wrap critical steps (checkProjectPlan, loadCompanyContext, generateUniqueProjectCode, saveProject) with 45s timeouts so a stuck MongoDB call surfaces a clear error instead of freezing the UI forever - orchestrator: roll back the project count whenever checkProjectPlan incremented it, not only when a project doc was saved. The old logic leaked +1 per failed attempt and eventually tripped the plan limit - orchestrator: normalize list items to @editorjs/nested-list's { content, items: [] } shape — the LLM emits plain strings, which the editor renders as literal "undefined" - orchestrator: drop bogus .catch() on removeProjectCount() (it returns undefined, not a Promise — was throwing TypeError on the error path) - orchestrator: per-step [AIPG][jobId] logging to pinpoint future hangs - openai provider: bump axios timeout to 10 min for reasoning/gpt-5 models (was 4 min, which the enriched prompt + 40+ task generation routinely exceeded); env override via OPENAI_TIMEOUT_MS Prompt improvements (task-guidance.md, examples.md, sprint-guidance.md): - one task per screen for every team, with explicit design→build symmetry - one task per HTTP method (no PUT/DELETE skips) - always include a project scaffolding task for new codebases - anti-patterns section calling out the bundling shapes seen in past outputs ("X and Y", "X with Y", redundant planning docs, QA bundling) - task count floor by project type, with pre-emit checklist - drop "Required skills" from descriptions — team assignment alone communicates the skill set - bumped recommended OpenAI model in .env.example to gpt-4o / gpt-4.1 for stronger instruction following UI: - show the original brief filename instead of "Brief loaded · N tokens" - sprint-name input takes the full row width in the plan preview Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * revert(staging): temporarily disable hardening middleware and tenant pool cap - index.js: comment out trust-proxy, helmet, and global rate-limit middleware - middlewares/mongoConnector: revert issue #162 LRU-bounded tenant connection pool back to the previous unbounded behaviour with the fixed idle/sweep timers - .env.example: drop the MAX_TENANT_CONNECTIONS / TENANT_CONNECTION_IDLE_MS / TENANT_CONNECTION_SWEEP_MS doc block that the pool cap referenced This is a deliberate rollback on the staging branch to unblock staging-only behaviour; the hardening should be reinstated once the regression is diagnosed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add space * fix(promptBuilder): correct comment formatting for consistency * fix: consolidate lowercase modules/ into Modules/ to resolve case-folding split On Windows (case-insensitive filesystem) commits accidentally tracked 12 AIProjectGenerator files under modules/ (lowercase) while the rest of the codebase lives under Modules/ (uppercase). The local tree merged both, hiding the issue — but on Linux (case-sensitive) GitHub and production saw two separate folders, breaking ./promptBuilder and path.join(__dirname, 'prompts') require resolutions at runtime. Move all 12 files into Modules/AIProjectGenerator/ so the folder is unified across all platforms. * feat(tasks): AI-estimate completion time on task create Estimate task completion time with an LLM after every task is created and persist the result (in minutes) to `tasks.totalEstimatedTime`. The estimate represents the wall-clock time an AI coding agent (Claude Code) would need to do the task end-to-end, with no manual implementation. Why - Gives newly-created tasks an immediate, description-driven time estimate so planning and remaining-hours math have a value to work with from the start. - Reuses the existing AIProjectGenerator llmProvider factory (Anthropic-preferred, OpenAI fallback), so installs that already have AI configured pick this up with no new env vars. How - New helper `Modules/EstimatedTime/aiTaskEstimator.js`. Reads the system prompt once at module load from a Markdown partial, builds a structured user message from the task (title, type, priority, parent/subtask flag, description), parses JSON, clamps to [5 min, 7 days], $sets totalEstimatedTime, and emits a `task` socket update. Never throws; silently no-ops when no LLM provider is configured. - New prompt partial `Modules/AIProjectGenerator/prompts/project-plan/ task-time-estimate.md` walks the model through deliverable shape, surface area, ambiguity, verification, and iteration overhead — explicitly discouraging lazy round-number defaults so the estimate reflects the actual description. - Hook in `Modules/Tasks/helpers/taskMongo/create.js` after the central task `create()` succeeds (skips mainChat). Fire-and-forget, so API responses are not delayed. - Hook in `Modules/AIProjectGenerator/orchestrator.js` after the bulk `insertMany` so AI-orchestrated project tasks also get estimates. Background runner with a concurrency cap of 3 to stay under provider rate limits when a single plan generates many tasks. Docs - `.claude/AI-TIME-ESTIMATION.md` explains the model the prompt uses and how estimates flow through the app in plain language. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(estimated-time): manual AI trigger + accuracy rewrite Three related improvements to the task time estimation feature built in #188: 1. Manual AI trigger in the task detail sidebar - New icon-only button next to the "Estimated" field. Tooltip "Generate estimate using AI" via native title attribute (matches existing project tooltip convention). - Inline spinner + disabled state while the request is in flight; debounce-safe (`isAiEstimateLoading` guards double-clicks). - Backed by a new endpoint `POST /api/v1/estimatedTime/ai/:tid` in `Modules/EstimatedTime/controller.js` that fetches the canonical task doc server-side (so the LLM payload comes from the DB, not stale client data), runs the estimator with the new `force: true` flag so it overwrites any existing value, persists `totalEstimatedTime`, and emits a Socket.io `task` update. - Permission gate: button is inner-gated by `checkPermission('task.task_estimated_hours', ...) === true`, mirroring the read-only / read-write pattern Priority / Start Date / Due Date use in the same component. Read-only users still see the value, just not the AI trigger. 2. Accuracy rewrite of the estimator prompt - The system prompt now walks the model through a four-phase pipeline before producing a number: Phase 1 — Normalize: remove redundancy, merge overlapping requirements, drop filler, keep implicit work. Phase 2 — Extract: enumerate the unique work items. Phase 3 — Estimate: apply deliverable shape, surface area, clarity, verification, and iteration overhead to EACH item, not to the whole description. Phase 4 — Sum: add up the per-item estimates without double-counting shared setup. - Output JSON now includes a `work_items[]` array — forcing chain-of-thought enumeration before the number, which is what structurally prevents both inflation (no item -> no minutes) and underestimation (each implicit item must be named). - Four worked examples baked into the prompt cover redundancy, overlapping requirements, filler stripping, and implicit work. - Parser is backward compatible: still consumes only `minutes`, so tasks estimated by the old prompt shape keep working. 3. Estimator tuning - `maxTokens: 256 -> 1024` so the work_items array doesn't get truncated mid-JSON (truncated response = parser returns null = estimate skipped, so this directly affects success rate). - `temperature: 0.2 -> 0.15` pulls the model toward deterministic, calibrated outputs — estimation should not be creative. - New `normalizeDescriptionForPrompt` helper strips trailing whitespace and collapses 3+ blank or 3+ accidental-duplicate lines so the model spends its reasoning budg…
1 parent 02d04a0 commit 176acbb

111 files changed

Lines changed: 6999 additions & 429 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "Sample Trello Board (rich)",
3+
"lists": [
4+
{ "id": "list-todo", "name": "To Do", "closed": false },
5+
{ "id": "list-hold", "name": "On Hold", "closed": false },
6+
{ "id": "list-done", "name": "Complete", "closed": false }
7+
],
8+
"members": [
9+
{ "id": "m1", "fullName": "Parth Detroja", "username": "parth", "email": "harmit.mendapara@aliansoftware.net" }
10+
],
11+
"cards": [
12+
{
13+
"id": "c1", "name": "Set up project repo", "desc": "Init repo and CI pipeline",
14+
"due": "2026-06-24T10:00:00.000Z", "idList": "list-todo", "closed": false,
15+
"idMembers": ["m1"],
16+
"labels": [{ "name": "Backend", "color": "blue" }, { "name": "P0", "color": "red" }],
17+
"attachments": [{ "name": "spec.pdf", "url": "https://trello.com/attach/spec.pdf", "bytes": 20480 }]
18+
},
19+
{
20+
"id": "c2", "name": "Build landing page", "desc": "Hero, features and pricing sections",
21+
"due": null, "idList": "list-hold", "closed": false,
22+
"idMembers": [],
23+
"labels": [{ "name": "Frontend", "color": "green" }],
24+
"attachments": []
25+
},
26+
{
27+
"id": "c3", "name": "Archived card should be skipped", "desc": "this card is closed",
28+
"due": null, "idList": "list-done", "closed": true
29+
}
30+
],
31+
"checklists": [
32+
{
33+
"id": "cl1", "idCard": "c1", "name": "Acceptance criteria",
34+
"checkItems": [
35+
{ "name": "Repo created", "state": "complete", "pos": 100 },
36+
{ "name": "CI pipeline green", "state": "incomplete", "pos": 200 }
37+
]
38+
}
39+
],
40+
"actions": [
41+
{ "type": "commentCard", "date": "2026-06-18T09:00:00.000Z", "data": { "text": "Use the org template for CI.", "card": { "id": "c1" } }, "memberCreator": { "fullName": "Parth Detroja" } },
42+
{ "type": "commentCard", "date": "2026-06-18T11:00:00.000Z", "data": { "text": "Landing copy is in the brief.", "card": { "id": "c2" } }, "memberCreator": { "fullName": "Jane Doe" } }
43+
]
44+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Blocked-Task Warning — Test Cases
2+
3+
**Feature:** A task that is `blocked_by` one or more still-open tasks shows a clear "⚠ Blocked by N open task(s)" banner in its Linked Tasks panel. Makes AlianHub's blocked-task alert real.
4+
5+
**Location:**
6+
- Frontend: Task detail → **Linked Tasks** panel — `frontend/src/components/organisms/LinkedTasks/LinkedTasks.vue` (computed `openBlockers`, banner).
7+
- Backend: `POST /api/v2/tasks/relations` action `openBlockers``getOpenBlockers` (`Modules/Tasks/helpers/taskMongo/relations.js`); pure helper `selectOpenBlockers` (`relationRules.js`). A blocker counts only while `statusType !== 'close'` and not soft-deleted.
8+
9+
**Legend:** ✅ Pass · ❌ Fail · ⏳ Pending (not yet run in the app)
10+
11+
| ID | Title | Precondition | Steps | Expected | Actual | Status |
12+
|----|-------|--------------|-------|----------|--------|--------|
13+
| BW_001 | Warning shows for an open blocker | Task B is `blocked_by` Task A; A is open | Open Task B → Linked Tasks panel | Amber banner: "⚠ Blocked by 1 open task(s): {A.key}" | | ⏳ Pending |
14+
| BW_002 | Warning clears when the blocker closes | As BW_001 | Move Task A to a Done/closed status; reopen Task B | Banner disappears (0 open blockers) | | ⏳ Pending |
15+
| BW_003 | Only `blocked_by` counts | Task B `blocks` Task A (B blocks, isn't blocked) | Open Task B | No banner on B | | ⏳ Pending |
16+
| BW_004 | Multiple open blockers listed | B `blocked_by` A and C, both open | Open Task B | Banner shows count 2 and both keys | | ⏳ Pending |
17+
| BW_005 | Soft-deleted blocker ignored | B `blocked_by` A; A soft-deleted | Open Task B | A not counted in the banner | | ⏳ Pending |
18+
| BW_006 | `openBlockers` API action | B `blocked_by` A (open) | POST /api/v2/tasks/relations {action:'openBlockers', taskId:B} | `{ status:true, data:[A summary] }` | | ⏳ Pending |
19+
| BW_007 | duplicates / relates_to never block | B `relates_to`/`duplicates` A | Open Task B | No banner | | ⏳ Pending |
20+
21+
**Unit coverage:** `selectOpenBlockers` + `isClosedStatusType` fully covered in `tests/task-relations-rules.test.js` (open / closed / deleted / wrong-type / null / mixed).
22+
23+
**Total:** 7 manual cases · pure logic unit-tested (green).

.claude/test-cases/CsvImporter.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# CSV Importer — Test Cases
2+
3+
**Location:** Project board toolbar → `...` (More) → Import CSV · **Last updated:** 2026-06-18
4+
5+
> **Status legend:** ⏳ Pending · ✅ Pass · ❌ Fail · ⚠️ Blocked · 🔄 Flaky
6+
7+
---
8+
9+
## Import Flow
10+
11+
| ID | Title | Precondition | Steps | Expected Result | Actual Result | Status |
12+
|---------|------------------------------------|-------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|---------------|--------|
13+
| CSV_001 | Import a valid CSV | A CSV with a header row (Task Name, Status, Priority, Due Date, Description); a sprint to import into | 1. Open `...` → Import CSV 2. Upload the file 3. Confirm the auto-detected column mapping 4. Pick the target sprint 5. Import | Success message with created count; tasks appear in the sprint with mapped name/status/priority/due | ||
14+
| CSV_002 | Manual column-mapping override | A CSV whose headers differ (e.g. "Summary" not "Task Name") | 1. Upload 2. Adjust the column-mapping dropdowns manually 3. Import | Import respects the manual mapping | ||
15+
| CSV_003 | Row missing a task name is skipped | A CSV where one row has no task name | 1. Import | That row is skipped; created count excludes it; import does not abort | ||
16+
| CSV_004 | Unknown status falls back | A CSV with a status not configured in the project | 1. Import | Task falls back to the project's first status | ||
17+
| CSV_005 | Priority mapping | A CSV with Highest / High / Medium / Low | 1. Import 2. Open the tasks | Priorities mapped (e.g. Highest→Urgent, High→High, Medium→Normal) | ||
18+
| CSV_006 | XLSX upload | An `.xlsx` file with the same columns | 1. Upload the .xlsx and import | Parses and imports the same as CSV | ||
19+
| CSV_007 | Import history | A completed import | 1. Re-open the import modal / import history | Job listed as source "csv" with total / processed / created counts | ||
20+
| CSV_008 | Invalid file handled | Import modal open | 1. Try an empty file 2. Try a non-CSV renamed to `.csv` | Readable error in both cases; nothing created; modal stays usable | ||
21+
22+
---
23+
24+
**Total:** 8 test cases · **All status:** ⏳ Pending
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Cumulative Flow Diagram — Test Cases
2+
3+
**Location:** Project → `+ View` → Reports → Cumulative Flow · **Last updated:** 2026-06-18
4+
5+
> **Status legend:** ⏳ Pending · ✅ Pass · ❌ Fail · ⚠️ Blocked · 🔄 Flaky
6+
7+
---
8+
9+
## Chart Rendering
10+
11+
| ID | Title | Precondition | Steps | Expected Result | Actual Result | Status |
12+
|---------|-------------------------------|---------------------------------------------|---------------------------------------------|--------------------------------------------------------------------------------------------------|---------------|--------|
13+
| CFD_001 | CFD renders | Reports view added; project with tasks across statuses | 1. Open Reports → Cumulative Flow | Stacked-area chart of task counts by status band (To do / In progress / On hold / Done) over ~30 days | ||
14+
| CFD_002 | Done band reflects completions | Some tasks completed over recent days | 1. Observe the "Done" band over time | The Done band grows on the dates tasks were completed | ||
15+
| CFD_003 | Empty state handled | A project with no tasks | 1. Open Reports → Cumulative Flow | Renders an empty/zero state — no crash | ||
16+
17+
---
18+
19+
**Total:** 3 test cases · **All status:** ⏳ Pending
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Epic Enhancements (dates / owner / priority / status / progress) — Test Cases
2+
3+
**Feature:** Epics gain **start/due dates**, an **owner**, a **priority** (low/medium/high), an **in_progress** status (alongside open/done), and a visible **progress bar with %** — turning the thin v1 into a usable progress layer.
4+
5+
**Location:**
6+
- Frontend: **Epics** panel — `frontend/src/components/molecules/Epics/EpicsPanel.vue` (create form fields, priority badge, status select, owner + dates, % label).
7+
- Backend: `Modules/Epics/controller.js` (create/update accept the new fields), `helpers/epicRules.js` (`EPIC_PRIORITIES`, `parseEpicDates`, extended `EPIC_STATUSES`), schema `utils/mongo-handler/schema.js` (epics: priority, ownerUserId, startDate, dueDate).
8+
9+
**Legend:** ✅ Pass · ❌ Fail · ⏳ Pending (not yet run in the app)
10+
11+
| ID | Title | Precondition | Steps | Expected | Actual | Status |
12+
|----|-------|--------------|-------|----------|--------|--------|
13+
| EE_001 | Create epic with priority + dates | Project open; Epics panel open | Enter name, pick priority **High**, set start + due, click Add | Epic created; owner defaults to creator | | ⏳ Pending |
14+
| EE_002 | Row renders new metadata | EE_001 done | View the epic row | Priority badge (High), owner name, due date all shown | | ⏳ Pending |
15+
| EE_003 | Progress bar + % label | Epic has 4 tasks, 1 closed | View epic row | Bar ≈25%, label "25% · 1/4" | | ⏳ Pending |
16+
| EE_004 | Set status In progress | Epic exists | Pick "In progress" in the row status select | Persists (PUT /epics/:id); chip tinted amber | | ⏳ Pending |
17+
| EE_005 | Set status Done | Epic exists | Pick "Done" | Persists; chip tinted green | | ⏳ Pending |
18+
| EE_006 | Invalid priority rejected (API) || PUT /api/v2/epics/:id {priority:'urgent'} | `{ status:false, "Invalid priority." }` | | ⏳ Pending |
19+
| EE_007 | start-after-due rejected (API) || POST /api/v2/epics {startDate:'2026-07-01', dueDate:'2026-06-01', …} | `{ status:false, "startDate must be on or before dueDate." }` | | ⏳ Pending |
20+
| EE_008 | Owner shows Unassigned when empty | Epic with no ownerUserId | View the row | Owner: "Unassigned" | | ⏳ Pending |
21+
22+
**Unit coverage:** `validateEpicInput` (priority), `parseEpicDates` (valid / clear / partial / unparseable / start>due), and `EPIC_STATUSES`/`EPIC_PRIORITIES` covered in `tests/epic-rules.test.js`.
23+
24+
**Total:** 8 manual cases · pure logic unit-tested (green).

.claude/test-cases/GanttView.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Gantt / Timeline View — Test Cases
2+
3+
**Location:** Project → `+ View` → Gantt · **Last updated:** 2026-06-18
4+
5+
> **Status legend:** ⏳ Pending · ✅ Pass · ❌ Fail · ⚠️ Blocked · 🔄 Flaky
6+
>
7+
> **Setup:** run `cd frontend && npm install` once (adds `dhtmlx-gantt`); add the **Gantt** view to the project via `+ View`.
8+
9+
---
10+
11+
## Rendering & Scheduling
12+
13+
| ID | Title | Precondition | Steps | Expected Result | Actual Result | Status |
14+
|---------|-----------------------------|-------------------------------------------|----------------------------------------------------------------|----------------------------------------------------------------------------|---------------|--------|
15+
| GNT_001 | Add the Gantt view | dhtmlx-gantt installed; project open | 1. `+ View` → Gantt → Add | Gantt tab appears and opens | ||
16+
| GNT_002 | Bars render | Tasks with both a start and a due date | 1. Open the Gantt tab | Those tasks render as bars on the timeline | ||
17+
| GNT_003 | Drag to reschedule | A scheduled task | 1. Drag a bar to new dates 2. Reload + open List/Table | Start/due updated and persisted everywhere | ||
18+
| GNT_004 | Resize duration | A scheduled task | 1. Drag a bar's edge | Due date changes and persists | ||
19+
| GNT_005 | Zoom presets | Gantt open | 1. Switch Day / Week / Month | Timeline scale changes accordingly | ||
20+
| GNT_006 | Unscheduled tray → schedule | A task with no start/due dates | 1. In the Unscheduled tray click **Schedule** on the task | Task gets today→+1 day and appears as a bar | ||
21+
| GNT_007 | Milestones shown | Project with milestones | 1. Open the Gantt | Milestones render as read-only diamond markers | ||
22+
23+
---
24+
25+
## Dependencies, Sync & Permissions
26+
27+
| ID | Title | Precondition | Steps | Expected Result | Actual Result | Status |
28+
|---------|--------------------------------|---------------------------------------------|-------------------------------------------------------------|--------------------------------------------------------------------------|---------------|--------|
29+
| GNT_008 | Draw + delete a dependency | Two scheduled tasks | 1. Drag from one bar's edge to another 2. Then delete the link | A "blocks" relation is created (visible in the task's Linked Tasks — see TaskRelations) then removed | ||
30+
| GNT_009 | Live sync across sessions | Same project Gantt open in two browsers | 1. Drag a bar in window 1 | Window 2 updates within a couple of seconds without reload | ||
31+
| GNT_010 | Read-only without edit rights | User lacking task-edit permission | 1. Open the Gantt | Read-only (no drag / no link drawing); "View only" badge shown | ||
32+
33+
---
34+
35+
## Regression (fixes)
36+
37+
| ID | Title | Precondition | Steps | Expected Result | Actual Result | Status |
38+
|---------|-----------------------------------------|------------------------------------|-----------------------------------------------------------------------|------------------------------------------------------------------|---------------|--------|
39+
| GNT_011 | Reload with Gantt as the active view | Gantt tab is currently active | 1. Reload the page (do NOT visit another view first) | Bars render — Gantt loads the task store itself (no empty chart) | ||
40+
| GNT_012 | Repeated view switching doesn't crash || 1. Switch List → Table → Gantt and back several times | No error; Gantt re-renders cleanly each time (no "tasksStore" crash) | ||
41+
42+
---
43+
44+
**Total:** 12 test cases · **All status:** ⏳ Pending

0 commit comments

Comments
 (0)