Commit Graph

620 Commits

Author SHA1 Message Date
Arnas Donauskas
456321a2f2
feat(api): public API groundwork — scoped API keys + /api/v1/me
Adds the machine-to-machine auth layer the public REST API needs so
external scripts and automations can drive wacrm (issue #245). This is
the groundwork release; data endpoints land one at a time on top of it.

- Migration 026: account-scoped `api_keys` table. SHA-256 hash at rest
  (plaintext shown once), display `key_prefix`, `scopes[]`, RLS via
  `is_account_member` (members read, admin+ manage).
- `src/lib/api-keys/`: key gen/hash (wacrm_live_ prefix, timing-safe),
  scope vocabulary, service-role auth-path store.
- `requireApiKey(request, scope?)` (src/lib/auth/api-context.ts):
  bearer parse -> hash lookup -> liveness -> per-key rate limit ->
  scopes-only authz. Returns a service-role-scoped account context.
- `/api/v1` envelope ({data} / {error:{code,message}}) + GET /api/v1/me
  reference endpoint that exercises the whole stack.
- Dashboard: admin+ key management routes + Settings -> API keys tab.
- Docs (docs/public-api.md), CHANGELOG, README, .env note.

Checks: typecheck clean, lint 0 errors, full suite green (+27 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 15:35:43 +03:00
Arnas Donauskas
12438dca78
Merge pull request #289 from ArnasDon/fix/middleware-preserve-refreshed-cookies
fix(auth): preserve refreshed Supabase cookies across middleware redirects
2026-06-23 10:11:51 +03:00
Arnas Donauskas
1b512fb17c
fix(auth): preserve refreshed Supabase cookies across middleware redirects
When getUser() refreshes an expired access token it rotates the refresh
token and writes the new cookies onto `supabaseResponse` via setAll().
The redirect and 401-JSON branches returned brand-new NextResponse
objects that didn't carry those Set-Cookie headers, so the rotated token
never reached the browser. The next request replayed the old, now-
consumed refresh token, the refresh failed, and the session wedged —
users got a broken reload after idling and could only recover by
manually clearing cookies.

Copy the refreshed cookies onto every response the middleware returns.

Fixes #288

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 10:08:05 +03:00
Arnas Donauskas
4036ccbb48
Merge pull request #280 from ArnasDon/dependabot/github_actions/actions/checkout-7
chore(deps): bump actions/checkout from 6 to 7
2026-06-23 08:39:07 +03:00
Arnas Donauskas
ac7a98fa3d
Merge pull request #286 from ArnasDon/feat/contacts-tag-filter-migration
feat(contacts): add filter_contacts_by_tags RPC migration
2026-06-22 16:00:22 +03:00
Arnas Donauskas
450233de89
feat(contacts): add filter_contacts_by_tags RPC migration
Server-side tag filter used by the Contacts page tag filter: join +
DISTINCT (OR across tags) + windowed total count + LIMIT/OFFSET in one
query, so a tag covering many contacts can't silently truncate results
or skew the count. SECURITY INVOKER, so existing account RLS applies.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 15:50:40 +03:00
Arnas Donauskas
981901ed82
Merge pull request #285 from ArnasDon/feat/contacts-tag-filter
feat(contacts): filter contacts by tags on the Contacts page
2026-06-22 15:41:05 +03:00
Arnas Donauskas
b066182b03
feat(contacts): filter contacts by tags on the Contacts page
Adds a "Filter by tags" popover (OR across selected tags) next to the
search box, with active-tag chips and clear-all. Resolves #270.

Filtering is resolved server-side via the filter_contacts_by_tags RPC so
pagination and the total count stay accurate even when a tag covers many
contacts (avoids PostgREST's row/IN-clause caps). A fetch-sequence guard
prevents out-of-order responses from rendering stale rows, and stale tag
selections are pruned when the tag list refreshes.

Note: requires migration 025_filter_contacts_by_tags.sql to be applied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 15:38:05 +03:00
Arnas Donauskas
1bacaf6372
Merge pull request #284 from ArnasDon/feat/team-member-presence
feat(team): member presence (online / away / offline) on the roster
2026-06-22 14:43:27 +03:00
Arnas Donauskas
054c9e4e47
feat(team): member presence (online / away / offline) on the roster
Adds a live presence layer so the Settings → Team members roster and the
inbox Assign dropdown show who is actively using the dashboard, idle, or
gone. Implements #269.

How it works
- New `member_presence` table + `touch_presence` SECURITY DEFINER RPC
  (migration 024). Account-scoped SELECT RLS mirrors the read-only
  roster; the RPC derives account_id server-side, never client-supplied.
- The active client heartbeats 'online'/'away' every ~30s; 'offline' is
  derived by viewers from staleness, so a closed tab resolves to offline
  without an unreliable unload write.
- Pure, unit-tested derivation/formatting in src/lib/presence.ts.
- `usePresence` reads the table, subscribes to Realtime (subscribe-then-
  merge so events landing mid-fetch aren't clobbered), and re-derives
  offline on a 15s tick.
- Roster: avatar badge + tooltip + "N online · N away · N offline"
  summary, with an aria-label so AT/keyboard users get presence too.
  Inbox Assign dropdown: matching dot before each teammate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 14:37:29 +03:00
dependabot[bot]
7560167312
chore(deps): bump @base-ui/react from 1.5.0 to 1.6.0
Bumps [@base-ui/react](https://github.com/mui/base-ui/tree/HEAD/packages/react) from 1.5.0 to 1.6.0.
- [Release notes](https://github.com/mui/base-ui/releases)
- [Changelog](https://github.com/mui/base-ui/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mui/base-ui/commits/v1.6.0/packages/react)

---
updated-dependencies:
- dependency-name: "@base-ui/react"
  dependency-version: 1.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-22 10:35:25 +00:00
dependabot[bot]
0c9b67fa08
chore(deps): bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-22 10:34:41 +00:00
AlexTPaul
ab6a511d31 feat: Notifications 2026-06-22 13:12:28 +05:30
Arnas Donauskas
c56ef7d5af
Merge pull request #278 from ArnasDon/fix/keyword-match-default-match-type
fix(automations): persist default match_type for Keyword Match triggers
2026-06-22 10:27:27 +03:00
Arnas Donauskas
ca3aa29c3a
fix(automations): persist default match_type for Keyword Match triggers
Creating a keyword_match automation with Match type left on the default
"Contains" failed activation with `match type must be "exact" or
"contains"` (path trigger.match_type). The dropdown showed "Contains"
via a display-only fallback, but the value was never written to the
config, so the saved payload omitted match_type entirely.

Both engines already treat a missing match_type as "contains"
(automations/engine.ts, flows/engine.ts), so the activation validator
was stricter than runtime. Two coordinated fixes:

- validate.ts: accept a missing match_type (defaults to "contains");
  only an explicit, unrecognised value is rejected. Aligns the
  validator with the engines and any non-UI client.
- automation-builder.tsx: seed match_type: "contains" on mount of
  KeywordMatchConfig so the displayed default is actually persisted,
  keeping the saved record explicit and self-describing.

Adds a regression test that a keyword_match config with no match_type
passes activation validation.

Fixes #271

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 10:20:38 +03:00
Arnas Donauskas
9d13a1d308
Merge pull request #277 from ArnasDon/fix/017-rls-rewrite-idempotent
fix(migrations): make 017 RLS rewrite re-runnable
2026-06-22 09:52:21 +03:00
Arnas Donauskas
79c60eeaea
fix(migrations): make 017 RLS rewrite re-runnable
017 claimed to be idempotent but only dropped the *legacy* policy names
before recreating; the new policies (contacts_select, …) were created
unguarded. A second run therefore failed with
`42710: policy "contacts_select" for table "contacts" already exists`.

Drop every policy on the tables 017 manages (it owns all RLS on them —
no later migration adds others) in a DO block before the CREATEs, so the
rewrite re-establishes the full set on every run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:48:47 +03:00
Arnas Donauskas
c8502207f8
Merge pull request #276 from ArnasDon/fix/orphan-profile-account-sharing-migration
fix(migrations): heal orphaned auth.users before account backfill (017)
2026-06-22 09:42:26 +03:00
Arnas Donauskas
38d929bbbb
fix(migrations): heal orphaned auth.users before account backfill (017)
The pre-017 signup trigger (001) inserted the profiles row inside an
`EXCEPTION WHEN OTHERS ... RAISE WARNING; RETURN NEW` block, so a signup
could leave an auth.users row with no matching profiles row.

The 017 account-sharing backfill keyed accounts off `profiles`, so such
an orphaned user got no account and was never linked. If that user owned
any domain rows (pre-017 RLS only required `auth.uid() = user_id`, not a
profile), their account_id stayed NULL and the subsequent
`ALTER TABLE ... SET NOT NULL` aborted the whole migration — exactly the
upgrade failure reported in #274.

Add a step (0) to the backfill DO block that creates the missing profile
for every auth.users row lacking one, before accounts are created. This
keys the backfill off auth.users instead of profiles, so every
authenticated user is migrated and no domain row can be left without an
account. Idempotent and safe to re-run, matching the rest of 017.

Fixes #274

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:33:42 +03:00
Arnas Donauskas
e714fb0db5
Merge pull request #268 from ArnasDon/security/bump-transitive-vulns-jun2026
fix(security): patch transitive vulns (hono, js-yaml, @babel/core)
2026-06-19 16:07:34 +03:00
Arnas Donauskas
453336e280
fix(security): patch transitive vulns (hono, js-yaml, @babel/core)
Pin patched versions via overrides to clear open Dependabot alerts:

- hono ^4.12.25 (was 4.12.23) — fixes GHSA CORS wildcard-with-
  credentials reflection (high), serve-static %5C path traversal,
  Lambda/ALB Set-Cookie & header dropping, and body-limit bypass
  (alerts #31–#35)
- js-yaml ^4.2.0 (was 4.1.1) — fixes quadratic-complexity DoS in
  merge-key alias handling (alert #30)
- @babel/core ^7.29.6 (was 7.29.0) — fixes arbitrary file read via
  sourceMappingURL comment (alert #29)

All three are transitive (build/lint/tooling) deps; bumps are
patch/minor. npm audit clean, typecheck + lint + 421 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 15:50:41 +03:00
Arnas Donauskas
85c2513a3a
Merge pull request #267 from ArnasDon/feat/inbox-collapsible-contact-sidebar
feat(inbox): collapsible contact sidebar on desktop
2026-06-19 14:35:26 +03:00
Arnas Donauskas
9b32954d1a
feat(inbox): collapsible contact sidebar on desktop
Add a desktop-only toggle in the message-thread header to show/hide the
contact sidebar (tags, deals, notes). On smaller laptops the always-on
panel crowds the message thread; agents who only want to read and reply
can now reclaim that horizontal space.

The page owns the open/closed state and conditionally renders the
sidebar; the thread renders the toggle button and reflects the state.
The choice is persisted to localStorage (device-scoped, like the theme
prefs) so it survives reloads. To avoid a hydration mismatch we default
to open on the server and reconcile from storage in an effect after
mount. Mobile is unchanged — the sidebar is never a permanent panel
there and the toggle is hidden below lg.

Closes #258

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 14:06:02 +03:00
Arnas Donauskas
c2fee3f2c5
Merge pull request #266 from ArnasDon/fix/settings-fullbleed-width
Settings: full-bleed layout for wide screens
2026-06-18 15:34:26 +03:00
Arnas Donauskas
1de99be73a
fix(settings): full-bleed layout so the page fills wide screens
The Settings page was the only dashboard route capped at
max-w-[1040px] mx-auto, so it floated centered with large empty
gutters on wide monitors while every other page is full-bleed.

Remove the cap so the page fills the available width like the rest of
the app, and spend the space where it helps readability:
- Overview status tiles go 3-up on xl (from 2-up).
- Templates list goes 2-up on xl.
- Form-heavy panels (Profile, Login & security, Deals) keep a readable
  max-w-2xl; Fields & tags and Appearance use max-w-3xl — so inputs
  don't stretch edge-to-edge on large displays.
- Grid uses minmax(0,1fr) to avoid blowout from long content.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 15:30:41 +03:00
Arnas Donauskas
7e531e6fbd
Merge pull request #265 from ArnasDon/feat/settings-redesign
Settings redesign: grouped rail + Overview landing
2026-06-18 15:11:41 +03:00
Arnas Donauskas
a45cbe6822
feat(settings): redesign Settings into a grouped rail + Overview landing
Replace the flat 8-tab Settings page with a cleaner information
architecture: a grouped left rail (Account / Workspace) plus a new
Overview landing with live status tiles. Related concerns are merged —
"Login & security" (password + sessions) and "Fields & tags" (tags +
custom fields).

- New shell: settings-sections (section model + legacy ?tab= mapping,
  tags/custom-fields -> fields), settings-rail, settings-overview
  (live counts + identity), security-panel, fields-and-tags-panel,
  settings-panel-head, settings-chip, role-meta (shared role chip
  metadata, de-duped from members-tab).
- Restyle the light panels to the mockup; reframe the heavy panels
  (WhatsApp, Templates) with the shared panel header, internals intact.
- Tokens: add --card-2 (light+dark) and @theme mappings for
  primary-hover/soft/soft-2 so the redesign stays fully tokenized.
- ?tab= remains the URL source of truth, keeping existing sidebar/header
  deep-links working.

Overview decouples the slow WhatsApp Meta health-ping from the cheap
count queries so a flaky ping never blanks the whole landing; template
counts use head-only count queries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 15:08:33 +03:00
Arnas Donauskas
3b55b3f1d9
Merge pull request #264 from ArnasDon/feat/template-image-upload
feat(templates): upload image for template headers; fix image-URL submit (#230)
2026-06-18 14:06:03 +03:00
Arnas Donauskas
4d23afc563
feat(templates): upload image for template headers; fix image-URL submit (#230)
Creating a template with an image header failed because the code sent
`example.header_url` — Meta only accepts a Resumable-Upload media handle
(`example.header_handle`) at creation time, never a plain URL.

- Resumable Upload: new `uploadResumableMedia` (meta-api) implements the
  two-step `/{app_id}/uploads` flow → media handle. Needs META_APP_ID.
- Handle derivation: `ensureImageHeaderHandle` fetches the header image
  (uploaded file OR pasted URL) and turns it into a handle before submit.
  Wired into both the submit and edit (PATCH) routes with actionable 400s
  (missing META_APP_ID, unreachable URL, wrong type, >5 MB). This fixes
  BOTH the new upload path and the previously-broken manual-URL path.
- Template builder: add an image file upload (account-scoped chat-media
  bucket via uploadAccountMedia) with preview + JPEG/PNG + 5 MB validation,
  alongside the URL field.
- Send-time fix: an approved image template now sends the public
  header_media_url as `link`; the resumable header_handle is NO LONGER
  used as a send media id (it's invalid for sends and would fail).
- META_APP_ID documented as required for image-header templates.

Scope: image headers (the issue). Helper is generic for video/document.
typecheck/lint/build green; 421 tests (10 new).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 14:00:54 +03:00
Arnas Donauskas
60466d14b7
Merge pull request #263 from ArnasDon/feat/chat-media-ga
feat(inbox): graduate chat media send to GA (remove beta flag)
2026-06-18 11:40:46 +03:00
Arnas Donauskas
ac9fc8dfc9
feat(inbox): graduate chat media send to GA (remove beta flag)
Photo/video/document/voice send has been validated in production, so it's
now available to every account. Removes the 'chat_media' beta gate:

- Composer always shows the attach menu + voice recorder (no
  useBetaFeature check).
- Send route no longer 403s media kinds for non-opted-in accounts.
- Deletes the now-unused beta-features helper, the useBetaFeature hook,
  and their test (added solely for this rollout; no other consumer).

The profiles.beta_features column (migration 011) is left in place for
future betas. typecheck/lint/build green; 411 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 11:38:54 +03:00
Arnas Donauskas
e855e6677f
Merge pull request #262 from ArnasDon/fix/chat-media-client-recording
refactor(inbox): record voice notes client-side, drop server ffmpeg
2026-06-18 11:29:10 +03:00
Arnas Donauskas
4fa29b7a45
refactor(inbox): record voice notes client-side, drop server ffmpeg
Follow-up to #259. Voice notes are encoded to Ogg/Opus in the browser via
opus-recorder (small WASM encoder, lazy-loaded on record), so there's no
server-side transcode and nothing to provision on the host. The server
stores the .ogg like any other attachment; WhatsApp renders it as a voice
note.

- Composer: MediaRecorder + /api/whatsapp/transcode-audio replaced with
  opus-recorder. Encoder worker vendored at public/opus/encoderWorker.min.js
  (no install-time download to fail), ignored by eslint + prettier.
- Removed the transcode route, the audio-transcode lib + tests, the
  ffmpeg-static dep + ambient types, the FFMPEG_PATH env doc, and the
  unused transcodeAudio rate limit.
- Added opus-recorder ambient types.

Still behind the 'chat_media' beta flag with per-kind size caps.
typecheck/lint/build green; 414 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 11:27:12 +03:00
Arnas Donauskas
34e8498a67
Merge pull request #261 from ArnasDon/fix/256-condition-three-outputs
fix(automations): remove spurious third output on condition steps
2026-06-18 11:25:59 +03:00
Arnas Donauskas
07e513690f
fix(automations): remove spurious third output on condition steps
A Condition (If/Else) step rendered three add-step connectors: Yes, No,
and a trailing linear "+". ConditionBranches already renders the Yes/No
branch StepLists (each with its own AddButton), while StepRenderer also
emitted a trailing AddButton after every step — including conditions.

Skip the trailing AddButton when step_type === "condition", since a
condition branches into Yes/No and has no linear continue path. Steps
after a condition are added inside one of its branches.

Fixes #256

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 11:20:55 +03:00
Arnas Donauskas
f35ae4ef1c
Merge pull request #259 from ArnasDon/feat/chat-media-send
feat(inbox): send photo, video, document & voice in chat (#213)
2026-06-18 11:02:37 +03:00
Arnas Donauskas
873ba1f4c6
feat(inbox): gate media send behind 'chat_media' beta flag
Media sends (photo/video/document/voice) are now opt-in per account via
the existing profiles.beta_features mechanism (migration 011 — same
pattern used to dogfood Flows before GA). No migration needed.

- New shared `hasBetaFeature` + `CHAT_MEDIA_BETA` ('chat_media') key, and
  a `useBetaFeature` client hook mirroring useCan (false while loading).
- Composer: the attach menu, hidden file inputs, and voice recorder only
  render when the flag is on; the hint indent reverts so non-beta users
  see the unchanged text + template composer.
- Server: the send route rejects media kinds (403) and the transcode
  route rejects (403) unless the caller's profile is opted in — so the
  endpoints can't be driven ahead of the rollout. Text + templates stay
  available to everyone.

Enable for one account via Supabase Studio:
  UPDATE profiles SET beta_features = array_append(beta_features, 'chat_media')
  WHERE email = 'someone@example.com';

Tests: hasBetaFeature null-safety + membership (419 total). typecheck/lint/build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:44:02 +03:00
Arnas Donauskas
9eaa15b9df
Merge pull request #260 from ArnasDon/fix/inbox-chat-sidebar-overlap
fix(inbox): chat thread overlapping contact sidebar on fullscreen desktop (#257)
2026-06-18 10:28:24 +03:00
Arnas Donauskas
96e7a56e3e
fix(inbox): harden media send — GC orphans, per-type caps, voice cap
Addresses senior-review findings on the media-send feature:

- Storage lifecycle: uploads are GC'd on the abandonment paths
  (discarded draft, failed/errored send, unmount with a staged draft,
  and replacing one draft with another) via a new deleteAccountMedia
  helper. Previously every cancel/failure orphaned a public object.
- Per-kind size caps (image 5MB, video/audio/document 16MB) mirror
  Meta's limits and are enforced BEFORE upload, so a too-large image no
  longer uploads then fails at the Meta call.
- Voice notes auto-stop at a 5-min cap and always render as a voice note
  (transcode anything that isn't already OGG/Opus — Safari MP4 included),
  not just Chromium WebM.
- Send route rejects unknown message_type and captions over Meta's 1024
  cap up front; transcode route guards its output size too.
- DraftPreview hoisted to a stable module-scope component so the caption
  input no longer remounts (and drops focus) on each keystroke.

Tests: per-kind cap assertions added (418 total). typecheck/lint/build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:28:19 +03:00
Arnas Donauskas
e7d8951936
fix(inbox): keep chat thread from overlapping contact sidebar (#257)
The page wrapper around the message thread already carries `min-w-0`
(issue #165), but the MessageThread root itself kept the default
`min-width: auto`. A single wide message (a long unbroken URL or word)
expanded the whole thread past its flex share, so at lg+ the chat
painted on top of the contact sidebar: outgoing bubbles were clipped on
the right and the hover toolbar floated over the Tags panel.

Adding `min-w-0` to the thread root lets it shrink to its column so the
bubbles' existing break-words / max-w-[75%] caps actually apply. Verified
in-browser: a 1272px-wide column went from 962px of overlap to 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:24:40 +03:00
Arnas Donauskas
312683ca05
feat(inbox): send photo, video, document & voice in chat (#213)
Agents can now attach a photo/video/document or record a voice note in
the inbox composer and send it live in a 1:1 thread — previously media
could only be received from customers or sent via the Flows builder.

- 023_chat_media.sql: new account-scoped `chat-media` Storage bucket
  (mirrors flow-media RLS from 020) plus Meta-accepted audio MIME types.
- meta-api: sendMediaMessage gains an `audio` kind (link-only — Meta
  rejects caption/filename on audio; WhatsApp renders OGG/Opus as a
  voice note).
- /api/whatsapp/send: media branch reusing the phone-variant retry loop.
- /api/whatsapp/transcode-audio: Node-runtime route remuxing the
  WebM/Opus that Chromium's MediaRecorder produces to OGG/Opus via
  ffmpeg-static (Firefox/Safari record accepted formats and skip it).
- composer: attach menu, file pickers, voice recorder + auto-transcode,
  preview with caption; thread: optimistic handleSendMedia.
- shared uploadAccountMedia helper (flows form refactored onto it).
- next.config: allow microphone=(self) (was fully blocked) + CSP
  media-src; document optional FFMPEG_PATH override.

Tests: meta-api audio payload shape, buildMediaPath, audio-transcode
helpers (14 new). typecheck/lint/build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:07:16 +03:00
Arnas Donauskas
b56e6b6e3a
Merge pull request #255 from ArnasDon/feat/bulk-delete-contacts
feat(contacts): allow bulk-deleting selected contacts (#239)
2026-06-17 17:12:06 +03:00
Arnas Donauskas
ee8da11985
feat(contacts): allow bulk-deleting selected contacts (#239)
Add row selection to the contacts table so multiple contacts can be
deleted at once instead of one at a time.

- New Checkbox UI primitive wrapping @base-ui/react/checkbox, matching
  the switch.tsx token conventions (incl. indeterminate state).
- Header select-all + per-row checkboxes; row-checkbox clicks stop
  propagation so they don't open the detail sheet.
- Bulk action bar with selected count, Clear, and a role-gated
  "Delete selected" action.
- handleBulkDelete via .delete().in('id', ids) — RLS already restricts
  deletes to account members, so no migration/API route is needed.
- Selection is page-scoped and resets whenever the contact set reloads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 16:44:41 +03:00
Arnas Donauskas
d6c595c0ed
Merge pull request #254 from ArnasDon/feat/inbox-unread-filter
feat(inbox): add "Unread" filter to conversation list
2026-06-17 13:41:29 +03:00
Arnas Donauskas
66a5bb51d2
feat(inbox): add "Unread" filter to conversation list
Adds an "Unread" option to the Inbox conversation-list filter dropdown,
showing only conversations with unread messages (unread_count > 0).

Closes #233

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:40:06 +03:00
Arnas Donauskas
6c5cfdd377
Merge pull request #253 from ArnasDon/feat/light-dark-mode
feat(theme): add light/dark mode toggle (#243)
2026-06-17 13:12:59 +03:00
Arnas Donauskas
43b842efb6
fix(theme): keep status-badge token contract green (CI test)
The sweep over-tokenized the neutral status badges in the three status
data modules to bg-muted/border-border, which broke
broadcast-status.test.ts ("each variant has the dark-theme class
triple") — it asserts every badge is bg-*/10 + text-* + border-*/20.

The badge system intentionally uses fixed palette colors at low opacity
(translucent fills are mode-tolerant). Restore the original slate fills
(so dark is identical and the triple contract holds) but keep
text-muted-foreground for the label, so neutral statuses stay legible in
light mode where a solid slate-400 would be too faint on white.

- broadcast-status: draft / recipient pending
- template-status: DRAFT / PENDING_DELETION
- trigger-meta: time_based / fallback

All 400 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:08:32 +03:00
Arnas Donauskas
1474657d03
fix(theme): light-mode polish from review pass
Self-review of the light/dark sweep surfaced neutral colors that lived
in raw inline-style / <style jsx> / gradient values (so the className
sweep correctly skipped them) and a few contrast/affordance nits:

- reply-quote: adapt to the outbound (primary) bubble via onPrimary so
  the quote/author stay legible on the violet fill in light mode; wire
  it from message-bubble.
- dashboard SVG chrome -> tokens: conversations-chart gridline/crosshair,
  pipeline-donut background ring, automation-builder dot-grid gradient.
- members-tab viewer chip: bg-card -> bg-transparent (outline) so it
  doesn't blend into a card surface in light mode.
- tag-manager / pipeline-settings color-swatch selection rings: raw
  slate/white -> var(--popover)/var(--foreground) so they flip.
- pipeline-board scrollbar (<style jsx>): slate rgb -> var tokens.
- activity-feed zebra stripe: bg-card/40 -> bg-muted/40 (survives light).
- forgot-password / import-modal: restore real hover (dead no-op hovers).
- themed-toaster: gate theme via useSyncExternalStore to avoid a
  hydration mismatch on sonner's theme attribute (no setState-in-effect).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 09:28:22 +03:00
Arnas Donauskas
4aa329b527
feat(theme): add light/dark mode toggle (#243)
Introduce a light/dark MODE axis orthogonal to the existing accent
themes. Neutral surface tokens now live in html[data-mode="light|dark"]
blocks; accent tokens (--primary*, --ring, --chart-1, --sidebar-primary*)
stay in html[data-theme] blocks, so any accent works in either mode.

- globals.css: split neutrals (mode) from accent (theme); add light
  palette; dark values unchanged (dark stays the default).
- lib/themes.ts: Mode type, DEFAULT_MODE=dark, MODE_STORAGE_KEY, isMode.
- use-theme.tsx: provider owns mode/setMode/toggleMode + cross-tab sync.
- layout.tsx: boot script applies persisted mode before first paint
  (no flash); colorScheme "dark light".
- mode-toggle.tsx (header sun/moon) + themed-toaster.tsx (mode-aware).
- appearance-panel.tsx: light/dark control + updated copy.
- Tokenize hardcoded neutral colors (slate/white/gray) -> semantic
  tokens across the app so every surface flips with mode. Flow canvas
  and message-bubble timestamps made mode-aware too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:29:46 +03:00
Arnas Donauskas
e1ab7b2b58
Merge pull request #248 from ArnasDon/dependabot/npm_and_yarn/dev-dependencies-d322b45e97
chore(deps-dev): bump the dev-dependencies group across 1 directory with 4 updates
2026-06-16 11:45:21 +03:00