Commit Graph

631 Commits

Author SHA1 Message Date
dependabot[bot]
0e7c42b7ef
chore(deps-dev): bump prettier in the dev-dependencies group
Bumps the dev-dependencies group with 1 update: [prettier](https://github.com/prettier/prettier).


Updates `prettier` from 3.8.4 to 3.9.1
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.4...3.9.1)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.9.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-29 10:35:06 +00:00
Arnas Donauskas
39aeac8c0c
Merge pull request #303 from ArnasDon/fix/webhook-after-processing
fix(webhook): process inbound WhatsApp messages in after() so they survive serverless freeze (#301)
2026-06-29 09:55:29 +03:00
bellopushon
377c1b738a feat(automations): add deal pipeline selectors 2026-06-26 15:57:06 -04:00
Arnas Donauskas
11d1c622d0
fix(webhook): run inbound processing in after() so it survives serverless freeze
The POST handler ack'd Meta with 200 and then ran processWebhook() as a
detached promise. On Vercel the function can be frozen/terminated the moment
the response is sent, so those DB writes were not guaranteed to complete.

This dropped a non-deterministic subset of inbound messages: the contact and
conversation rows (written first) landed, but the message insert (written
after) often did not — leaving conversations that appear in the inbox with an
empty thread, with no durable logs to explain it (issue #301).

Move the work into next/server `after()`, which keeps the function alive until
the callback resolves (within the route's maxDuration) while still ack'ing Meta
fast enough to avoid retry-driven duplicate inserts. Add maxDuration=60 for
headroom since processing can fan out to per-media Meta verification calls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:34:44 +03:00
Arnas Donauskas
b2ed58a41f
Merge pull request #302 from ArnasDon/feat/flows-editor-redesign
Redesign flow editor UI to match Flow Builder design
2026-06-26 16:57:32 +03:00
Arnas Donauskas
1f4d1a3a11
Redesign flow editor UI to match Flow Builder design
Reworks the flow editor (/flows/[id]) to the Flow Builder design
handoff, adapted to wacrm's existing tokens, components, and
light+dark theming. Keeps all behaviour — React Flow canvas,
drag-to-connect, typed config forms, validation, save/activate/delete,
per-browser view persistence, mobile-forces-list.

- shared.tsx: per-node-type color system (nodeColors) + one-line
  blurbs, and a shared NodeIconChip component. Label color blends
  toward --foreground via color-mix so it stays readable in both modes.
- header.tsx: compact single-row toolbar (back / icon / inline name /
  status chip / edited dot · Runs / Delete / Activate / Save) plus a
  subtle inline description note.
- flow-editor-shell.tsx: app-like full-height layout (toolbar → mode
  row → stage → validation bar) with a segmented Canvas/List control
  and a complete node-type legend derived from NODE_META.
- flow-canvas.tsx: per-type colored node cards, branch ports, dot-grid
  background, rounded controls/minimap, top-left primary Add-node menu,
  colored inspector header.
- flow-builder.tsx: list rows tied to the same per-type color language.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 16:53:03 +03:00
Arnas Donauskas
b72e7a6e62
Merge pull request #299 from ArnasDon/fix/broadcast-media-header-url
fix(broadcasts): collect header media URL for image/video templates
2026-06-25 17:25:20 +03:00
Arnas Donauskas
33942dc8ac
fix(broadcasts): collect header media URL for image/video templates (#298)
Templates with an IMAGE/VIDEO/DOCUMENT header could not be broadcast:
the wizard never offered a place to provide the header media URL, so
the send either shipped the template's stale stored URL or — when none
was stored — threw "image header requires a media link or id at send
time", failing the whole broadcast. The send path already supported
`messageParams.headerMediaUrl`; only the UI/hook never collected it.

- step3-personalize: show a required "Header media" URL field (with
  http(s) validation and an image preview) when the selected template
  has a media header; seed it from the template's stored sample URL and
  block Next until a valid URL is provided.
- new-broadcast page: thread headerMediaUrl through wizard state.
- use-broadcast-sending: pass it as messageParams.headerMediaUrl to the
  broadcast API for media-header templates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 17:17:18 +03:00
Arnas Donauskas
d8d8cf2b2a
Merge pull request #297 from ArnasDon/fix/account-context-schema-cache
fix(auth): load account context without an embedded FK join (#294)
2026-06-25 13:38:51 +03:00
Arnas Donauskas
738db87847
fix(auth): load account context without an embedded FK join
getCurrentAccount() and the client AuthProvider resolved the caller's
account through a PostgREST embedded join (`account:accounts!inner(...)`)
on profiles. That embed requires PostgREST to have the
profiles.account_id -> accounts.id relationship in its schema cache.
When the cache is stale -- a common Supabase state right after a
migration adds the FK, or when migrations are applied out of band -- the
embed fails hard with PGRST200 and the whole account context collapses.

It is the only query shape in the app that depends on relationship
inference (everything else reads accounts with plain `.eq('id', ...)`
filters), which is why the dashboard kept working while Settings ->
API keys / Team members returned 403 "Could not load account context"
and the Overview's cards hung loading (issue #294).

Replace the embed with a plain account-by-id lookup in both the server
helper and the client AuthProvider. Same accounts RLS, no relationship
inference, graceful degradation. The client now also keeps account_id /
account_role even if the account-name lookup itself fails. Adds a
regression test for getCurrentAccount.

Fixes #294

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 13:34:26 +03:00
Arnas Donauskas
0779932664
Merge pull request #290 from ArnasDon/feat/public-api-groundwork
feat(api): public API groundwork — scoped API keys + /api/v1/me
2026-06-23 17:16:39 +03:00
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