Skip to content

Commit 26c04d8

Browse files
github-actions[bot]Marfuenclaude
authored
feat: add compliance timeline to overview (feature flagged)
* feat(db): add timeline models and enums for compliance timeline feature Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(api): add default timeline template definitions as code constants Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(api): add timeline date recalculation helper with tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(api): add timelines service with CRUD, activation, pause/resume, phase completion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(api): split timelines service into focused modules under 300 lines each Extract lifecycle, phase editing, template management, and template resolution into separate files to comply with the max 300 lines per file rule. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(api): add timeline DTOs, controllers, module registration, and Swagger cleanup - DTOs: activate-timeline, update-phase, create-template, create-phase-template, update-template, update-phase-template with class-validator decorators - Customer controller: GET /timelines, GET /timelines/:id, POST /timelines/:id/phases/:phaseId/ready with Slack webhook notification - Admin template controller: full CRUD for timeline templates and their phases - Admin org timelines controller: activate, pause, resume, phase CRUD, complete - TimelinesModule registered in AppModule with all services and controllers - Added @ApiExcludeController() to all 8 existing admin controllers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(api): auto-create timeline on framework add and auto-complete phases on 100% tasks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(app): add SWR hooks for timelines and admin timeline management Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(app): add reusable TimelinePhaseBar component Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add generated prisma to gitignore and add completedBy include to findOne * feat(app): add TimelineOverview component to Overview page Add a compliance timeline section above the existing dashboard grid, showing stacked timeline cards with phase bars, status badges, and date summaries. Also updates the Timeline hook types to match the actual API response (DRAFT/ACTIVE/PAUSED/COMPLETED statuses). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(app): add expanded timeline view to framework detail page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(app): add admin timeline template management page Add a new admin page for managing timeline templates with CRUD operations. Includes a template list with phase bar previews and a sheet editor for creating/editing templates and their phases. Added sidebar navigation link. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(app): add Timeline tab to admin org detail page Add a Timeline tab to the admin organization detail view showing all timelines for an org. Includes status badges, phase tables, and action buttons for activating (with date picker), pausing, resuming, and editing individual phases via a sheet editor. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(api): auto-create timelines for existing framework instances on first load * fix(api): fix type error in date helper tests * feat(api): smart timeline backfill using trust status, scores, and task completion data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(app): use design system tokens, remove outer card, add next cycle date Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(app): improve phase bar with progress indicator and add timeline summary * fix(api): use 'Attestation' instead of 'Certification' for HIPAA and GDPR * feat: separate SOC 2 Type 1 and Type 2 timelines, add date markers to phase bar * fix: replace em dashes with hyphens in template names * fix(api): wrap admin timeline endpoints in { data, count } response format * fix(app): use correct timeline status values (DRAFT/ACTIVE/PAUSED/COMPLETED) in admin components * fix(app): fix missing closing brace in TimelineCard * fix(app): replace table layout with stacked phase cards in admin timeline view * feat(app): add tabs to Overview page with Timeline tab and compact teaser Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(app): simplify timeline teaser to clean single-line banner without phase bar * fix(app): remove timeline teaser from Overview tab * fix(app): remove redundant header and summary from Timeline tab * fix(app): use defaultDurationWeeks for template phase duration (fixes NaN) * fix: recalculate end date and downstream phases when duration or start date changes * feat(api): auto-complete timeline phases immediately when tasks are marked done * feat: add phase grouping with groupLabel for SOC 2 Type 2 sub-phases * feat(admin): add delete, reset, and recreate timeline actions with confirmation dialogs * fix(app): close confirmation dialogs immediately on confirm * fix(api): recreate also deletes DB templates so code defaults are re-seeded * fix(api): force-refresh templates from code defaults during recreate * fix(app): add bracket lines connecting group label to phase edges * fix(app): render grouped phases as cohesive blocks with no internal gaps * fix(app): show one start/end date per group instead of per sub-phase * feat: add AUTO_POLICIES and AUTO_PEOPLE completion types for independent sub-phase tracking * fix(api): use getOverviewScores for AUTO_POLICIES and AUTO_PEOPLE checks * fix(api): use correct property names from scores (total/published) * fix(app): wrap phase groups to new rows on small screens * fix(app): vertical phase stack on mobile, horizontal bar on lg+ screens * fix(app): keep grouped phases in one row on mobile, only ungrouped phases stack * fix(app): treat ungrouped phases as individual groups so each gets its own row on mobile * fix(app): show group labels and dates per row on mobile phase bar * fix(app): replace mobile phase bars with clean vertical step list * fix(app): compact horizontal bars on mobile without per-phase dates * fix(app): remove checkmarks from completed phase bars * fix(app): add diagonal stripe pattern to unfilled portion of active phase * fix(app): use primary color for stripe pattern on active phase * fix(app): use oklch color-mix for stripe pattern instead of hsl wrapper * fix(app): reduce stripe opacity to 10% for subtler effect * fix(app): reduce gap between phases from 3px to 1px * fix(api): rename SOC 2 Type 2 - Year 1 to SOC 2 Type 2 * fix(app): remove redundant Started/Est. completion text from timeline cards * feat: live completion percentages for AUTO_* phases, revert if metric drops below 100% * fix(api): rename SOC 2 sub-phases to Policies, Evidence, People * feat(app): show live completion % and fill for all AUTO_* sub-phases independently * feat(app): show avg completion % on group label, single cohesive fill for grouped phases * fix(app): show individual sub-phase percentages inside grouped bar * fix(app): add pulsing progress marker to grouped phase bar * feat(app): add dedicated full-page timeline template editor Replace the drawer-based template editor with a dedicated page at [orgId]/admin/timeline-templates/[templateId]. The new editor includes metadata form, live phase bar preview, individual phase cards with save/delete/reorder, and group label support with color indicators. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(app): framework dropdown in template creation and editing * feat(api): centralized Slack notifications for phase completion, timeline completion, and ready-for-review * fix(api): assign transaction result to txResult variable * fix(api): use template name in Slack notifications to distinguish SOC 2 Type 1 vs Type 2 * fix(api): use Slack Block Kit for cleaner notification formatting * feat(api): add clickable org link to Slack notifications with admin timeline URL * fix(api): show org ID in Slack notification messages * fix(app): only show stripes on IN_PROGRESS phases, plain muted for PENDING * fix(api): only advance next phase to IN_PROGRESS when all prior phases are completed * fix(app): show framework name instead of ID in template editor dropdown * feat(app): group sub-phases under parent card in template editor Phases that share a groupLabel are now nested inside a parent group card with an editable label, total duration display, and "Add Sub-phase" button. Sub-phases render as compact inline rows instead of full cards. Ungrouped phases retain the existing full PhaseCard rendering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(api): add Auditor Review + Draft Report phases to SOC 2 templates * fix(app): phase numbering counts groups as 1, label below Phase N, add wk suffix to duration * fix(app): use DS Text component for weeks label instead of custom span * fix(app): add column labels (Name, Duration, Completion) above sub-phase rows * feat(app): add confirmation dialogs to all delete actions in template editor Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(api): add groupLabel to phase template DTOs and service * fix(api): remove AdminAuditLogInterceptor from template controller (no org context) * fix(api): pass groupLabel from DTO to service in addPhase and updatePhase * feat(api): add timeline auto-completion hooks to policies, members, tasks, and evidence services Wire up checkAutoCompletePhases fire-and-forget calls in all services that can affect compliance progress metrics, enabling automatic phase completion/reversion when tasks, policies, people, or evidence change. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(api): add Preparing for Audit group (Policies, Evidence, People) to SOC 2 Type 1 * fix(api): auto-complete phases at 100% on page load, not just on events * fix(app): group phase cards visually in admin timeline, show template name as title * fix(api): use actual completion date as anchor for downstream phase recalculation * chore(db): add migration for AUTO_POLICIES and AUTO_PEOPLE completion types * feat: add Start Next Cycle action for completed timelines (admin only) * fix(app): use render prop on AlertDialogTrigger to avoid nested buttons * fix(app): destructure onStartNextCycle from TimelineActions props * feat(app): group timeline cycles by framework with stacked card effect for past cycles * fix(app): fix stacked card effect to show behind the main card * fix(app): stacked card effect as thin strips peeking below main card * fix(app): simple lip button below card for showing previous cycles * fix(app): remove bottom border radius from card when previous cycles lip is attached * fix(app): subtle muted background on previous cycles lip * fix(app): reduce lip padding and font size for sleeker look * refactor(app): migrate TimelineOverview to DS components (Card, Badge, Text, Stack) * fix(app): improve timeline card padding, title size, and spacing * fix(app): remove bg from previous cycles lip, only show on hover * fix(app): remove lip, show cycle badge (Year 2) when multiple cycles exist * fix(app): show Year badge on all timelines, computed from cycle count not internal ID * fix(api): always use now() for completedAt and endDate when auto-completing phases * fix(api): pin phase dates on early completion so recalculation doesn't overwrite them * fix(api): use min(endDate, now) for completedAt in backfill to avoid future dates * chore: update OpenAPI spec with timeline endpoints * feat(timelines): add track-based SOC2 timeline model * feat(findings): integrate timelines module and add AUTO_FINDINGS phase completion logic * chore: ignore .worktrees directory * feat(flags): add per-org PostHog feature flag admin toggle Gates features behind PostHog flags evaluated per organization via group targeting. Platform admins can list/toggle flags for a specific org from the admin panel; end users pick up changes on route navigation. - register org as PostHog group via OrganizationIdentifier in org layout - useFeatureFlag hook (posthog-js/react based, auto-reactive) - reload flags on route change so toggles propagate without full refresh - admin endpoints list + patch flags for a given org (groupIdentify) - Feature Flags tab in admin org panel with search + toggle - gate Timeline tab in Overview behind is-timeline-enabled Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: timeline cleanup — Card DS fix, dedup import, timeline migration - TimelineOverview uses DS Card title/headerAction props (no className) - remove duplicate ApiExcludeController import - add timeline prisma migration - regenerate openapi.json Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(timeline,flags): address AI review feedback API: - reset timeline status to ACTIVE when reopening regressed phases so a COMPLETED timeline never holds an IN_PROGRESS phase (fix data corruption) - fire-and-forget createTimelinesForFrameworks so timeline creation errors don't mask successful framework creation - log errors in checkAutoCompletePhases .catch() in tasks/people/policies services instead of swallowing silently - apply AdminAuditLogInterceptor to feature flag admin controller so PATCH toggles get audit-logged - drop unused AdminAuditLogInterceptor import from admin-timeline-templates controller (no orgId in path, interceptor would no-op) - cache PostHog "not configured" state so warning logs once, not per request - read Slack webhook URL + APP_URL at call time instead of module load time so env var loading order doesn't silently disable notifications App: - surface SWR error state in FeatureFlagsTab so failed fetches don't render as an empty "no flags found" panel - check .error on api.delete / api.patch / api.post in template-actions so partial saves throw instead of silently succeeding - non-mutating sort of template.phases in TemplateEditorPage to avoid corrupting SWR cache Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(timeline): address remaining AI review P2 feedback API: - extract COMPLETION_TYPES to shared timeline-constants, narrow DTO completionType from string to CompletionType union - trigger checkAutoCompletePhases on both done and not_relevant task statuses (AUTO_TASKS counts both as finished) - drop unnecessary checkAutoCompletePhases after task creation — a todo task can't advance any AUTO phase App: - extract COMPLETION_OPTIONS constant to timeline-templates/constants and share across PhaseRow, TemplateEditor, template-actions - remove orderIndex from form state (controlled/uncontrolled hybrid); compute from array position at submit time in template-actions - convert TemplateMetadataForm's framework fetch from raw useEffect to SWR with proper error handling - delete duplicate admin layout under timeline-templates (parent admin layout already guards the route) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(timelines): make findAllForOrganization pure-read Previously GET /timelines mutated state (completed phases, reverted regressed phases, updated timeline status). That violates HTTP semantics and races under concurrent reads. - extract the sync loop into TimelinesService.reconcileAutoPhasesForOrganization (handles both advancement and regression, replaces the inline logic) - findAllForOrganization is now a pure read: ensureTimelinesExist (idempotent backfill) + fetch + in-memory enrichment with live completion percentages - call reconcileAutoPhasesForOrganization from checkAutoCompletePhases so every existing event hook (task/policy/people/findings mutations) drives both advancement and regression — no stale state left for reads to repair - recreateAllForOrganization explicitly reconciles before returning so freshly-recreated timelines are synced against current metrics Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(timelines): always reconcile after checkAutoCompletePhases The reconcile call sat after an early return for phases.length === 0, which skipped it in the exact case it mattered: when all AUTO phases are already COMPLETED and a metric drops (regression). Wrap the advancement in a try/finally so reconciliation fires unconditionally on every event hook — extract advancement into runPhaseAdvancement for readability. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(db): reorder timeline migrations to apply after main's latest Three of the branch's timeline migrations were dated 2026-04-07/08, which is before main's 20260410120000_add_finding_scope. On merge, Prisma would apply them out-of-order relative to what staging/prod already have. Rename them to 20260410130000-20260410130002 so they sit right after main's latest migration while preserving internal dependency order (timeline models → phase group label → auto policies/people completion types → rest of timeline migrations). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address cubic P1/P2 review feedback batch API: - remove unused UseInterceptors import from admin-timeline-templates - add @Transform trim to UnlockTimelineDto so whitespace-only reason fails @isnotempty validation - log errors (this.logger.warn) on checkAutoCompletePhases .catch() in people-invite and evidence-forms services (matches pattern elsewhere) - replace unreachable ECONNREFUSED check in dynamic-manifest-loader with the correct Prisma error code P1001 - forbidNonWhitelisted on FF admin controller's ValidationPipe so bogus body keys return 400 instead of silently stripping - @ISINT (not @IsNumber) on cycleNumber so 1.5 etc. is rejected - wrap posthog groupIdentify + flush in try/catch in FF service — network failures now surface as meaningful BadRequestException + log instead of raw 500 - check response.ok in Slack helper so non-200s are logged - return sendSlack promise from notify* helpers instead of floating it - drop getScores' checkAutoCompletePhases call — mutation event hooks already drive both advancement and regression; the dashboard read shouldn't trigger heavy writes on every load App: - TemplateMetadataForm: reset(values) after save so isDirty flips back, wrap setSaving in try/finally - TemplateEditorPage: render distinct error state when the SWR fetch fails instead of falling through to "Template not found" - TimelineActivateForm: validate date before ISO conversion and wrap body in try/finally so a bad date can't leave the button stuck in loading Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address new cubic P1/P2 review batch P1: - template-actions: upsert phases first, then delete removed ones. Reverse order meant a mid-save failure could drop phases before their replacements were successfully written. P2: - tasks.service: trigger checkAutoCompletePhases on ANY status change (bulk and single update). Limiting it to done/not_relevant missed regressions when a finished task is reopened. - findings.service: same — run reconciliation on every finding status transition, not only when moving to closed. - admin-feature-flags.service: paginate the PostHog feature_flags REST response (follow `next` cursor) so orgs with >200 flags don't silently drop the tail. - admin-feature-flags.service: treat any non-empty string variant returned by getAllFlags as enabled (multivariate flags report variant name, not boolean true), not just strict `=== true`. - DTOs: use @ISINT instead of @IsNumber for cycleNumber, orderIndex, durationWeeks, defaultDurationWeeks — rejects fractional values at the boundary instead of failing later at the DB Int column. - timelines-phases.service: wire data.endDate and data.datesPinned through the update. Previously the DTO advertised them but the service silently ignored both. - admin-org-timelines.controller: pass datesPinned from DTO to service. - TimelineActivateForm: parse date input as local midnight (match YYYY-MM-DD, construct Date with local components) so a negative-UTC-offset user's chosen day isn't shifted by ISO serialization. - FeatureFlagsTab: disable all flag switches while any PATCH is in flight so a late rollback from a failed request can't overwrite a newer successful toggle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address new cubic P1/P2 batch after main merge P1: - timelines-phases.service: if caller sets an explicit start/end date, force datesPinned=true regardless of what the DTO says — storing a specific date with datesPinned=false would let the downstream date recalculator silently overwrite it - template-actions.createNewTemplate: wrap the phase-creation loop in try/catch and delete the orphan template on failure, so a failed phase POST can't leave a template with zero phases in the framework's list - timelines.service.ensureTimelinesExist: always call backfillTimeline (which is idempotent per-track) instead of short-circuiting when any timeline already exists. Repairs partial state from a half-succeeded createTimelinesForFrameworks call (e.g. SOC 2 with only Type 1 created) P2: - markReadyForReview: return idempotently (alreadyReady=true) when the phase is already marked, and only fire the Slack ping on the first transition. Double-clicks / retries no longer spam the CX channel. - timelines-slack.helper: use the same base-URL fallback chain the other notifiers use (NEXT_PUBLIC_APP_URL → BETTER_AUTH_URL → APP_URL), and escape user-supplied text before interpolating into Slack mrkdwn so org names with `<`, `>`, `&`, `|` can't break link syntax. - posthog.service: prefer POSTHOG_API_KEY over NEXT_PUBLIC_POSTHOG_KEY so backend config can't be shadowed by frontend env values. - tasks.service.createTask: re-run checkAutoCompletePhases after create — a new todo task reduces AUTO_TASKS completion % and can regress a previously COMPLETED phase. - policies.service.spec: register a TimelinesService stub so the test module compiles after the service gained that dependency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address new cubic P1/P2 batch P1: - admin-feature-flags.service: validate the PostHog pagination \`next\` URL against the configured host before following it. A compromised or malicious PostHog response could otherwise redirect pagination to an attacker-controlled host and leak the Authorization header (personal API key) via SSRF. - findings.service: wire checkAutoCompletePhases into create() and delete() too. Previously only update() triggered reconciliation, so creating a new open finding or deleting the last open one could leave AUTO_FINDINGS phases out of sync with the actual ratio. P2: - frameworks-timeline.helper.createTimelinesForFrameworks: try/catch per track instead of wrapping the whole loop. A soc2_type1 failure no longer silently skips soc2_type2 — each track is attempted independently and logged on its own. - timelines-date.helper: defensively copy Date objects on assignment so the caller's timelineStartDate (and the pinned start/end dates of input phases) can't be mutated by a caller writing back into the returned rows. - admin-org-timelines.controller.addPhase: cast to PhaseCompletionType (already imported) instead of a hardcoded string union — stays in sync with the Prisma enum automatically when a new type is added. - timeline.prisma: document the NULL-distinct behavior of the (frameworkId, templateKey) unique constraint so it's not misread as a bug. Rows without explicit keys are already covered by the (frameworkId, trackKey, cycleNumber) constraint above. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(flags): add FF tests + guard invalid POSTHOG_HOST After auditing all cubic review threads, only two were actually still open against the current code: - admin-feature-flags.service: new URL(apiHost) at line 55 could throw if the configured host lacks a protocol (e.g. 'posthog.internal'). Wrap it in try/catch and return [] with a logged error instead of crashing the whole list endpoint. - admin-feature-flags.controller/service: no tests existed. Add a controller spec covering the NotFoundException path and happy-path delegation, plus a service spec covering: missing REST config, the SSRF guard refusing foreign-origin pagination, multivariate variant strings recognized as enabled, and setFlagForOrganization calling groupIdentify + flush. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(flags): use strict host match in fetch mock to silence CodeQL The URL filter `u.includes('us.posthog.com')` could match `evil.com/us.posthog.com/`. Harmless in a unit test but CodeQL flags the pattern. Switch to `new URL(u).host === 'us.posthog.com'` so it's an exact-host check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(app): stop tracking generated Prisma client apps/app/.gitignore had src/generated/ but not prisma/src/generated/, which is where the schema's output setting actually writes. A previous merge-conflict sweep committed ~96 generated files. Extend the gitignore and untrack them — postinstall runs 'prisma generate' so the client is recreated on install / build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(db): squash 9 timeline migrations into one via prisma migrate diff Generated a single migration using 'prisma migrate diff' with --from-migrations pointing at a copy of packages/db/prisma/migrations that excludes the 9 timeline ones, and --to-schema pointing at the current schema. The resulting SQL is byte-for-byte what main + the squashed migration would produce (verified: migrate diff from the final migrations dir against the schema reports 'No difference detected'). Dropped: - 20260410130000_add_timeline_models - 20260410130001_add_phase_group_label - 20260410130002_add_auto_policies_people_completion_types - 20260413182638_add_timeline_lock_flags - 20260413200000_add_timeline_lock_state_and_regression - 20260413212000_add_timeline_template_progression_keys - 20260413231500_add_timeline_track_keys - 20260414113000_add_auto_findings_completion_type - 20260417155556_timeline Replaced with: - 20260420000000_timeline_feature Local devs who already applied any of the dropped migrations will need to 'prisma migrate reset' — the old names still sit in their _prisma_migrations table. Staging/prod haven't applied yet (branch isn't merged) so they're unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(app): remove the duplicated prisma/ setup apps/app/prisma/ was a full duplicate of packages/db: - client.ts was byte-identical to packages/db/src/client.ts - prisma/schema/ duplicated packages/db/prisma/schema/ and was already drifting (a recent timeline.prisma comment existed in packages/db but not here) - prisma.config.ts + postinstall + db:generate + db:getschema all existed to sync and generate from the local duplicated schema The app never imports @trycompai/db directly — it uses the @db alias (351 call sites) which maps to ./prisma/{index,server}.ts. Those files now just re-export from @prisma/client (populated by packages/db's build) so the local schema and generation are dead code. Dropped: - apps/app/prisma.config.ts - apps/app/prisma/schema/ (45 files) - apps/app/prisma/src/ (previously-generated tree) - package.json: build:docker prisma-generate prefix, db:generate, db:getschema, prebuild, postinstall - .gitignore: stale prisma/generated + prisma/schema/*.prisma rules Kept: apps/app/prisma/{client,index,server}.ts — these wire up the app's PrismaClient instance (with the PG adapter) and expose it via the @db alias. packages/db doesn't export a ready-made db instance, so these three thin files stay for now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(db,docker): restore Prisma client generation after app prisma cleanup When apps/app lost its own postinstall + build:docker prisma-generate step, nothing was left to populate @prisma/client. Cubic was right that 'next build' would fail — the deps stage already uses --ignore-scripts and PRISMA_SKIP_POSTINSTALL_GENERATE=true, so neither the app nor packages/db generated anything. Fix in two places: - packages/db/package.json: add a postinstall that runs generate-prisma-client-js. Covers local dev — 'bun install' now populates node_modules/@prisma/client. '|| true' mirrors the app's old pattern so installs in envs without DATABASE_URL don't fail. - Dockerfile app-builder stage: after combine-schemas, explicitly run generate-prisma-client-js so the Docker build gets the client too (the deps stage skipped it intentionally). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Mariano Fuentes <marfuen98@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0d59e8f commit 26c04d8

113 files changed

Lines changed: 12381 additions & 95 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.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ packages/*/dist
8888

8989
# Generated Prisma Client
9090
**/src/db/generated/
91+
packages/db/prisma/src/generated/
9192

9293
# Release script
9394
scripts/sync-release-branch.sh
@@ -97,3 +98,4 @@ scripts/sync-release-branch.sh
9798

9899
.superpowers/*
99100
.claude/worktrees/
101+
.worktrees/

Dockerfile

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,13 @@ COPY apps/app ./apps/app
6363
# Bring in node_modules for build and prisma prebuild
6464
COPY --from=deps /app/node_modules ./node_modules
6565

66-
# Pre-combine schemas for app build
67-
RUN cd packages/db && node scripts/combine-schemas.js
68-
RUN cp packages/db/dist/schema.prisma apps/app/prisma/schema.prisma
66+
# Pre-combine schemas and generate the Prisma client into
67+
# node_modules/@prisma/client. The deps stage ran `bun install` with
68+
# `--ignore-scripts` so packages/db's postinstall was skipped; we run
69+
# it explicitly here so `next build` can resolve the generated runtime
70+
# + types when it imports @prisma/client.
71+
RUN cd packages/db && node scripts/combine-schemas.js \
72+
&& node scripts/generate-prisma-client-js.js
6973

7074
# Ensure Next build has required public env at build-time
7175
ARG NEXT_PUBLIC_BETTER_AUTH_URL

apps/api/package.json

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,57 +7,57 @@
77
"@ai-sdk/anthropic": "^2.0.53",
88
"@ai-sdk/groq": "^2.0.32",
99
"@ai-sdk/openai": "^2.0.65",
10-
"@aws-sdk/client-ec2": "^3.911.0",
11-
"@aws-sdk/client-s3": "3.1013.0",
1210
"@aws-sdk/client-acm": "^3.948.0",
11+
"@aws-sdk/client-api-gateway": "^3.948.0",
12+
"@aws-sdk/client-apigatewayv2": "^3.948.0",
13+
"@aws-sdk/client-appflow": "^3.948.0",
14+
"@aws-sdk/client-athena": "^3.948.0",
1315
"@aws-sdk/client-backup": "^3.948.0",
16+
"@aws-sdk/client-cloudfront": "^3.948.0",
1417
"@aws-sdk/client-cloudtrail": "^3.948.0",
1518
"@aws-sdk/client-cloudwatch": "^3.948.0",
16-
"@aws-sdk/client-cost-explorer": "^3.948.0",
1719
"@aws-sdk/client-cloudwatch-logs": "^3.948.0",
20+
"@aws-sdk/client-codebuild": "^3.948.0",
21+
"@aws-sdk/client-cognito-identity-provider": "^3.948.0",
1822
"@aws-sdk/client-config-service": "^3.948.0",
23+
"@aws-sdk/client-cost-explorer": "^3.948.0",
1924
"@aws-sdk/client-dynamodb": "^3.948.0",
25+
"@aws-sdk/client-ec2": "^3.911.0",
2026
"@aws-sdk/client-ecr": "^3.948.0",
2127
"@aws-sdk/client-ecs": "^3.948.0",
2228
"@aws-sdk/client-efs": "^3.948.0",
2329
"@aws-sdk/client-eks": "^3.948.0",
30+
"@aws-sdk/client-elastic-beanstalk": "^3.948.0",
2431
"@aws-sdk/client-elastic-load-balancing-v2": "^3.948.0",
32+
"@aws-sdk/client-elasticache": "^3.948.0",
33+
"@aws-sdk/client-emr": "^3.948.0",
34+
"@aws-sdk/client-eventbridge": "^3.948.0",
35+
"@aws-sdk/client-glue": "^3.948.0",
2536
"@aws-sdk/client-guardduty": "^3.948.0",
2637
"@aws-sdk/client-iam": "^3.948.0",
2738
"@aws-sdk/client-inspector2": "^3.948.0",
39+
"@aws-sdk/client-kafka": "^3.948.0",
40+
"@aws-sdk/client-kinesis": "^3.948.0",
2841
"@aws-sdk/client-kms": "^3.948.0",
2942
"@aws-sdk/client-lambda": "^3.948.0",
3043
"@aws-sdk/client-macie2": "^3.948.0",
44+
"@aws-sdk/client-network-firewall": "^3.948.0",
3145
"@aws-sdk/client-opensearch": "^3.948.0",
3246
"@aws-sdk/client-rds": "^3.948.0",
3347
"@aws-sdk/client-redshift": "^3.948.0",
3448
"@aws-sdk/client-route-53": "^3.948.0",
49+
"@aws-sdk/client-s3": "3.1013.0",
50+
"@aws-sdk/client-sagemaker": "^3.948.0",
3551
"@aws-sdk/client-secrets-manager": "^3.948.0",
3652
"@aws-sdk/client-securityhub": "^3.948.0",
37-
"@aws-sdk/client-sns": "^3.948.0",
38-
"@aws-sdk/client-sqs": "^3.948.0",
39-
"@aws-sdk/client-wafv2": "^3.948.0",
40-
"@aws-sdk/client-api-gateway": "^3.948.0",
41-
"@aws-sdk/client-apigatewayv2": "^3.948.0",
42-
"@aws-sdk/client-appflow": "^3.948.0",
43-
"@aws-sdk/client-athena": "^3.948.0",
44-
"@aws-sdk/client-cloudfront": "^3.948.0",
45-
"@aws-sdk/client-codebuild": "^3.948.0",
46-
"@aws-sdk/client-cognito-identity-provider": "^3.948.0",
47-
"@aws-sdk/client-elastic-beanstalk": "^3.948.0",
48-
"@aws-sdk/client-elasticache": "^3.948.0",
49-
"@aws-sdk/client-emr": "^3.948.0",
50-
"@aws-sdk/client-eventbridge": "^3.948.0",
51-
"@aws-sdk/client-glue": "^3.948.0",
52-
"@aws-sdk/client-kafka": "^3.948.0",
53-
"@aws-sdk/client-kinesis": "^3.948.0",
54-
"@aws-sdk/client-network-firewall": "^3.948.0",
55-
"@aws-sdk/client-sagemaker": "^3.948.0",
5653
"@aws-sdk/client-sfn": "^3.948.0",
5754
"@aws-sdk/client-shield": "^3.948.0",
55+
"@aws-sdk/client-sns": "^3.948.0",
56+
"@aws-sdk/client-sqs": "^3.948.0",
5857
"@aws-sdk/client-ssm": "^3.948.0",
5958
"@aws-sdk/client-sts": "^3.948.0",
6059
"@aws-sdk/client-transfer": "^3.948.0",
60+
"@aws-sdk/client-wafv2": "^3.948.0",
6161
"@aws-sdk/s3-request-presigner": "3.1013.0",
6262
"@browserbasehq/sdk": "2.6.0",
6363
"@browserbasehq/stagehand": "^3.2.1",
@@ -103,6 +103,7 @@
103103
"nanoid": "^5.1.6",
104104
"pdf-lib": "^1.17.1",
105105
"playwright-core": "^1.57.0",
106+
"posthog-node": "^5.29.2",
106107
"prisma": "7.6.0",
107108
"react": "^19.1.1",
108109
"react-dom": "^19.1.0",
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { NotFoundException } from '@nestjs/common';
2+
import { Test, type TestingModule } from '@nestjs/testing';
3+
4+
jest.mock('@db', () => ({
5+
db: {
6+
organization: {
7+
findUnique: jest.fn(),
8+
},
9+
},
10+
}));
11+
12+
jest.mock('../auth/platform-admin.guard', () => ({
13+
PlatformAdminGuard: class MockGuard {
14+
canActivate() {
15+
return true;
16+
}
17+
},
18+
}));
19+
20+
jest.mock('../admin-organizations/admin-audit-log.interceptor', () => ({
21+
AdminAuditLogInterceptor: class MockInterceptor {
22+
intercept(_ctx: unknown, next: { handle: () => unknown }) {
23+
return next.handle();
24+
}
25+
},
26+
}));
27+
28+
// eslint-disable-next-line @typescript-eslint/no-require-imports
29+
import { AdminFeatureFlagsController } from './admin-feature-flags.controller';
30+
import { AdminFeatureFlagsService } from './admin-feature-flags.service';
31+
import { PlatformAdminGuard } from '../auth/platform-admin.guard';
32+
import { AdminAuditLogInterceptor } from '../admin-organizations/admin-audit-log.interceptor';
33+
34+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
35+
const mockDb = require('@db').db as {
36+
organization: { findUnique: jest.Mock };
37+
};
38+
39+
describe('AdminFeatureFlagsController', () => {
40+
let controller: AdminFeatureFlagsController;
41+
let service: {
42+
listForOrganization: jest.Mock;
43+
setFlagForOrganization: jest.Mock;
44+
};
45+
46+
beforeEach(async () => {
47+
jest.clearAllMocks();
48+
service = {
49+
listForOrganization: jest.fn(),
50+
setFlagForOrganization: jest.fn(),
51+
};
52+
53+
const module: TestingModule = await Test.createTestingModule({
54+
controllers: [AdminFeatureFlagsController],
55+
providers: [{ provide: AdminFeatureFlagsService, useValue: service }],
56+
})
57+
.overrideGuard(PlatformAdminGuard)
58+
.useValue({ canActivate: () => true })
59+
.overrideInterceptor(AdminAuditLogInterceptor)
60+
.useValue({ intercept: (_ctx: unknown, next: { handle: () => unknown }) => next.handle() })
61+
.compile();
62+
63+
controller = module.get(AdminFeatureFlagsController);
64+
});
65+
66+
describe('list', () => {
67+
it('throws NotFoundException when the org does not exist', async () => {
68+
mockDb.organization.findUnique.mockResolvedValue(null);
69+
await expect(controller.list('org_missing')).rejects.toBeInstanceOf(
70+
NotFoundException,
71+
);
72+
expect(service.listForOrganization).not.toHaveBeenCalled();
73+
});
74+
75+
it('returns flags wrapped in { data } when the org exists', async () => {
76+
mockDb.organization.findUnique.mockResolvedValue({ id: 'org_1' });
77+
service.listForOrganization.mockResolvedValue([
78+
{
79+
key: 'is-timeline-enabled',
80+
name: 'is-timeline-enabled',
81+
description: '',
82+
active: true,
83+
enabled: true,
84+
createdAt: null,
85+
},
86+
]);
87+
88+
const result = await controller.list('org_1');
89+
90+
expect(service.listForOrganization).toHaveBeenCalledWith('org_1');
91+
expect(result.data).toHaveLength(1);
92+
expect(result.data[0].key).toBe('is-timeline-enabled');
93+
});
94+
});
95+
96+
describe('update', () => {
97+
it('throws NotFoundException when the org does not exist', async () => {
98+
mockDb.organization.findUnique.mockResolvedValue(null);
99+
await expect(
100+
controller.update('org_missing', {
101+
flagKey: 'is-timeline-enabled',
102+
enabled: true,
103+
}),
104+
).rejects.toBeInstanceOf(NotFoundException);
105+
expect(service.setFlagForOrganization).not.toHaveBeenCalled();
106+
});
107+
108+
it('delegates to the service with orgId, orgName, flagKey, and enabled', async () => {
109+
mockDb.organization.findUnique.mockResolvedValue({
110+
id: 'org_1',
111+
name: 'Acme',
112+
});
113+
service.setFlagForOrganization.mockResolvedValue({
114+
key: 'is-timeline-enabled',
115+
enabled: false,
116+
});
117+
118+
const result = await controller.update('org_1', {
119+
flagKey: 'is-timeline-enabled',
120+
enabled: false,
121+
});
122+
123+
expect(service.setFlagForOrganization).toHaveBeenCalledWith({
124+
orgId: 'org_1',
125+
orgName: 'Acme',
126+
flagKey: 'is-timeline-enabled',
127+
enabled: false,
128+
});
129+
expect(result.data.enabled).toBe(false);
130+
});
131+
});
132+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {
2+
Body,
3+
Controller,
4+
Get,
5+
NotFoundException,
6+
Param,
7+
Patch,
8+
UseGuards,
9+
UseInterceptors,
10+
UsePipes,
11+
ValidationPipe,
12+
} from '@nestjs/common';
13+
import { ApiExcludeController, ApiOperation, ApiTags } from '@nestjs/swagger';
14+
import { Throttle } from '@nestjs/throttler';
15+
import { db } from '@db';
16+
import { PlatformAdminGuard } from '../auth/platform-admin.guard';
17+
import { AdminAuditLogInterceptor } from '../admin-organizations/admin-audit-log.interceptor';
18+
import { AdminFeatureFlagsService } from './admin-feature-flags.service';
19+
import { UpdateFeatureFlagDto } from './dto/update-feature-flag.dto';
20+
21+
@ApiExcludeController()
22+
@ApiTags('Admin - Feature Flags')
23+
@Controller({ path: 'admin/organizations', version: '1' })
24+
@UseGuards(PlatformAdminGuard)
25+
@UseInterceptors(AdminAuditLogInterceptor)
26+
@Throttle({ default: { ttl: 60000, limit: 60 } })
27+
export class AdminFeatureFlagsController {
28+
constructor(private readonly service: AdminFeatureFlagsService) {}
29+
30+
@Get(':orgId/feature-flags')
31+
@ApiOperation({
32+
summary:
33+
'List all admin-managed feature flags with their current state for an organization',
34+
})
35+
async list(@Param('orgId') orgId: string) {
36+
const org = await db.organization.findUnique({ where: { id: orgId } });
37+
if (!org) throw new NotFoundException('Organization not found');
38+
39+
const flags = await this.service.listForOrganization(orgId);
40+
return { data: flags };
41+
}
42+
43+
@Patch(':orgId/feature-flags')
44+
@ApiOperation({
45+
summary: 'Enable or disable a feature flag for an organization',
46+
})
47+
@UsePipes(
48+
new ValidationPipe({
49+
whitelist: true,
50+
forbidNonWhitelisted: true,
51+
transform: true,
52+
}),
53+
)
54+
async update(
55+
@Param('orgId') orgId: string,
56+
@Body() dto: UpdateFeatureFlagDto,
57+
) {
58+
const org = await db.organization.findUnique({ where: { id: orgId } });
59+
if (!org) throw new NotFoundException('Organization not found');
60+
61+
const result = await this.service.setFlagForOrganization({
62+
orgId,
63+
orgName: org.name,
64+
flagKey: dto.flagKey,
65+
enabled: dto.enabled,
66+
});
67+
return { data: result };
68+
}
69+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Module } from '@nestjs/common';
2+
import { AdminFeatureFlagsController } from './admin-feature-flags.controller';
3+
import { AdminFeatureFlagsService } from './admin-feature-flags.service';
4+
import { PostHogService } from './posthog.service';
5+
6+
@Module({
7+
controllers: [AdminFeatureFlagsController],
8+
providers: [AdminFeatureFlagsService, PostHogService],
9+
exports: [AdminFeatureFlagsService, PostHogService],
10+
})
11+
export class AdminFeatureFlagsModule {}

0 commit comments

Comments
 (0)