Commit Graph

246 Commits

Author SHA1 Message Date
dependabot[bot]
ee7f7a8916
chore(deps): bump actions/setup-node from 4 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 18:35:36 +00:00
dependabot[bot]
d8b6b86525
chore(deps): bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [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/v4...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 18:35:35 +00:00
Arnas Donauskas
9c25971c27
Merge pull request #73 from ArnasDon/chore/contributor-experience
chore: fork-first contributor docs + housekeeping
2026-04-20 21:34:52 +03:00
Arnas Donauskas
45574ec7ee
chore: reframe contributor docs fork-first
WaCRM is a template — users are expected to fork and customise their
own deployment, not to send feature work back upstream. The previous
CONTRIBUTING.md read like a standard collaborative-project onboarding
guide and set the wrong expectation.

Reframed docs:

- CONTRIBUTING.md is now "Using WaCRM". Opens with "fork it and make
  it yours", covers forking + keeping-in-sync commands, explains
  which PRs do tend to land (security, correctness, docs, small
  nits) vs. which belong in a fork (new features, stack swaps,
  opinionated refactors). Keeps the filename so GitHub's built-in
  "Contributing guidelines" link still surfaces it.

- .github/ISSUE_TEMPLATE/feature_request.yml: the intro block now
  tells the forker up front that feature requests usually become
  "build it in your fork", and describes the narrow cases where an
  upstream request is useful.

- .github/ISSUE_TEMPLATE/config.yml: adds a third contact link
  pointing at CONTRIBUTING.md so people see the fork-first framing
  before filing.

- .github/pull_request_template.md: prepends a heads-up that most
  changes belong in the forker's own repo, with a pointer to
  CONTRIBUTING.md.

Nothing else in the PR changes — CoC, Dependabot, EditorConfig,
Prettier configs, issue bug_report.yml, and the package.json changes
are orthogonal to the fork-first framing and stay as-is.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:31:50 +03:00
Arnas Donauskas
3799c224d8
chore: contributor experience — CoC, templates, Dependabot, editor configs
Nine paperwork files + one devDep. All support work for a repo that's
about to be public; zero runtime behaviour change.

- CONTRIBUTING.md at root. Dev loop, script reference, PR style, code
  style, review process. Short on prose, long on specifics.
- .github/CODE_OF_CONDUCT.md adopts Contributor Covenant 2.1 by
  reference (no inlined text). Points reports to the maintainer's
  email and GitHub's private flow.
- .github/ISSUE_TEMPLATE/ — structured YAML forms for bug and feature
  reports, plus config.yml that disables blank issues and surfaces
  the security-disclosure + docs links. The bug form asks for
  version, runtime, and logs up front so triage doesn't start with
  a round of "where is it running?".
- .github/pull_request_template.md — short checklist; nudges toward
  typecheck / lint / build before submit without being long enough
  to skip.
- .github/dependabot.yml — weekly npm + github-actions scans,
  grouped so Supabase / @types / dev-only patches batch into single
  PRs instead of six per week. Next / React / Tailwind / eslint-
  config-next are ignored so majors stay manual.
- .editorconfig — LF, UTF-8, 2 spaces, final newline, strip trailing
  whitespace (except in Markdown where trailing spaces are line
  breaks).
- .prettierrc — single quotes + trailing-comma es5, matches the
  plurality quote style in the codebase (336 single vs 236 double
  in imports). Tailwind plugin sorts class names.
- .prettierignore — node_modules, build output, lockfile, migrations.
- package.json adds "format" / "format:check" scripts and pins
  prettier + prettier-plugin-tailwindcss as devDeps.
- package-lock.json refreshed for the new devDeps.

Important: existing source files are NOT reformatted in this PR. 153
files would change if we ran `npm run format`, which would balloon
the diff and make review impossible. The reformat pass is a separate,
deliberate PR.

Typecheck clean; `npx prettier --check` on the new config files
passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:16:21 +03:00
Arnas Donauskas
1965075ea2
Merge pull request #72 from ArnasDon/chore/oss-metadata
chore: OSS metadata + LICENSE + SECURITY + complete env example
2026-04-20 20:45:03 +03:00
Arnas Donauskas
bce345ed03
chore: OSS metadata + LICENSE + SECURITY + complete env example
Prep work so the repo is ready to be public. None of this is runtime
code; it's the governance a forker expects on day one.

- LICENSE: MIT at repo root. README already claimed MIT; this makes
  GitHub actually recognise it and show the license badge.

- .github/SECURITY.md: private disclosure path via GitHub Security
  Advisories or email. Spells out scope, safe harbour, and what the
  reporter should expect — so a researcher finds it before they try
  to file a public issue.

- .env.local.example: fleshed out with the three vars it was missing
  — META_APP_SECRET (required, from PR 1), NEXT_PUBLIC_SITE_URL
  (recommended), and AUTOMATION_CRON_SECRET (optional). Grouped as
  Required / Recommended / Optional with a one-line comment per
  entry. Includes the openssl/node generators for the secrets.

- package.json: description, repository, bugs, homepage, keywords,
  license, author, engines.node >=20. No runtime impact; feeds npm
  registry metadata, GitHub's repo sidebar, and prevents forkers from
  silently installing on Node <20 (Next 16's floor).

Typecheck clean; npm install run to refresh the lockfile for the
package.json metadata additions (no dependency changes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:41:14 +03:00
Arnas Donauskas
d6a4677e6c
Merge pull request #71 from ArnasDon/security/encrypt-aes-gcm
security: switch WhatsApp token encryption from AES-CBC to AES-GCM
2026-04-20 20:32:24 +03:00
Arnas Donauskas
e570e1a8f2
security: switch WhatsApp token encryption from AES-CBC to AES-GCM
CBC without a MAC is unauthenticated — anyone who can write rows to
whatsapp_config can flip bits in the stored access_token or
verify_token without the decrypt throwing. You'd silently get garbled
bytes; worst case, if the mutation happens to produce a valid token,
messages go out under a spoofed account. GCM appends a 16-byte
authentication tag; any tampering fails the decrypt.

## What changed

src/lib/whatsapp/encryption.ts
  - encrypt() now emits AES-256-GCM ciphertexts with a 12-byte IV,
    formatted as `<iv>:<ciphertext>:<authTag>` (three colons).
  - decrypt() auto-detects format by splitting on `:`:
      3 parts → GCM, validated via auth tag.
      2 parts → legacy CBC, read-only compatibility path.
    Callers are unchanged; same signature, same return type.
  - isLegacyFormat() exposes the format check so hot paths can
    opportunistically upgrade old rows.
  - IV length is validated on decrypt (12 for GCM, 16 for CBC) to
    fail fast on corrupted input.

## Self-healing existing rows

Two call sites opportunistically upgrade a legacy row to GCM after a
successful decrypt — fire-and-forget, no latency impact on the request
path, idempotent under concurrent writes.

  - src/app/api/whatsapp/send/route.ts (hot path)
      After decrypting access_token, if it was CBC, we schedule an
      UPDATE that writes a fresh GCM ciphertext back to the same row.
      The first send after deploy upgrades the token; subsequent
      sends are no-ops because isLegacyFormat returns false.

  - src/app/api/whatsapp/webhook/route.ts (subscribe handshake)
      When a config's verify_token matches the inbound
      hub.verify_token, we upgrade it in place if it was legacy.
      Runs during Meta's webhook re-subscription events, which
      happen on config changes and periodic refreshes.

No schema migration required — the DB column holds whatever string
encrypt() produces. Operators who want to force an upgrade for a
particular user can clear and re-save the WhatsApp settings form.

## Verified (tsx smoke test)
  - GCM roundtrip works (plaintext in → same plaintext out).
  - Legacy CBC decrypt works on 2-part format.
  - isLegacyFormat distinguishes new/legacy correctly.
  - Bit-flipped GCM ciphertext throws on decrypt.
  - Forged auth tag throws on decrypt.
  - Same plaintext produces different ciphertexts across calls
    (IV randomness).

## Rollback

Revert this file. Existing GCM rows would then fail to decrypt
because the previous encrypt.ts has no branch for the 3-part format.
Safer rollback: keep the decrypt branches, revert only the encrypt()
body to CBC — but since GCM roundtrips cleanly and the PR's bug
surface is small (pure crypto utility), a straight revert should only
matter if a forker had time between merging this PR and reverting to
accumulate GCM rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:25:48 +03:00
Arnas Donauskas
7182abe7d8
Merge pull request #70 from ArnasDon/fix/inbox-messages-stale-after-deeplink
fix(inbox): don't drop messages when ?c= URL param updates
2026-04-20 20:04:32 +03:00
Arnas Donauskas
6cceb81c46
fix(inbox): don't drop messages when ?c= URL param updates
Reports: clicking a conversation sometimes left the message thread
stuck on "No messages yet" until the user hit browser refresh.

Root cause — a three-way race kicked off by any click on a conversation
in the list:

1. handleSelectConversation runs:
      setActiveConversation(B)
      setMessages([])
      router.replace("/inbox?c=B")

2. The URL change flips `deepLinkConvId` (from useSearchParams).
   handleConversationsLoaded is a useCallback with
   [deepLinkConvId], so its identity changes. That cascades into
   ConversationList, whose fetchConversations useCallback has
   [onConversationsLoaded], whose useEffect has [fetchConversations] —
   so the list refetches conversations purely because the URL changed.

3. When the refetch lands, handleConversationsLoaded runs its deep-link
   auto-select branch. autoSelectedForDeepLinkRef still points at its
   previous value (null, or the previous deep-link), so the guard
   `ref !== deepLinkConvId` passes and the branch re-applies B:
      setActiveConversation(B)     // no-op, same id
      setMessages([])              // BLOWS AWAY the messages
                                   // MessageThread just fetched

4. MessageThread's fetch effect only re-runs when conversationId
   changes. B is still B. It does not refetch. Messages stuck at [].

A hard reload unsticks things because state starts from scratch and
the initial deep-link flow is the only path that runs the auto-select.

Three defense-in-depth fixes:

- handleSelectConversation now writes the clicked id into
  autoSelectedForDeepLinkRef BEFORE the router.replace, so by the
  time any downstream refetch lands, the auto-select branch sees
  ref === deepLinkConvId and skips.

- handleConversationsLoaded early-returns if the deep-linked
  conversation is already the active one. This is redundant with
  the ref update above but protects against any other code path
  we don't foresee triggering a refetch.

- ConversationList's fetch is now driven by a plain useEffect with
  [] deps, using a ref for the latest onConversationsLoaded
  callback. URL changes no longer cause a conversations refetch.
  Matches the same stabilisation pattern used in
  src/hooks/use-auth.tsx and src/components/inbox/message-thread.tsx.

Verified: typecheck passes; lint count unchanged (37 pre-existing
problems, zero added). Couldn't exercise the rendered /inbox from the
preview sandbox — it's auth-gated and no test credentials are
available. Please confirm in the authenticated inbox:
  - Click back and forth between two conversations with different
    message histories; messages should populate every time.
  - Open /inbox?c=<id> directly; thread opens with messages.
  - Switch conversations after a deep-link landing; still works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:01:59 +03:00
Arnas Donauskas
460b5bf259
Merge pull request #69 from ArnasDon/security/rate-limit-send-broadcast
security: rate-limit send and broadcast per user
2026-04-20 19:51:01 +03:00
Arnas Donauskas
1f295a831a
security: rate-limit send and broadcast per user
A fork running WaCRM publicly had no server-side guard on the
WhatsApp send endpoints — a compromised session or an aggressive UI
loop could call /api/whatsapp/send or /broadcast as fast as the
network allowed and blow through Meta's per-number quota (or worse,
get the number suspended).

This PR adds a minimal in-memory fixed-window rate limiter keyed by
user id.

Budgets (tuned for a template default — forkers can edit):
  - /api/whatsapp/send      60 per minute per user  (1/s sustained)
  - /api/whatsapp/broadcast  5 per minute per user  (campaign dispatch
                                                    rate, not fan-out)

Behaviour:
  - Check runs right after the Supabase auth check; unauthenticated
    calls 401 as before and never consume a bucket entry.
  - When a user exceeds the budget the route returns 429 with
    Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining,
    X-RateLimit-Reset headers.
  - Send and broadcast buckets are independent; two users are
    independent.

Design trade-off:
  The limiter lives in a process-local Map. On a single Hostinger
  Managed Node.js instance — the template's default deployment —
  that's the whole limit. On a horizontally-scaled deploy (multiple
  regions, serverless fan-out) each instance has its own Map and the
  effective rate is N * limit. Swap src/lib/rate-limit.ts for a
  Redis/Upstash/Durable-Objects backend if you scale past one
  instance; keep the checkRateLimit return shape and the call sites
  don't move.

Memory is self-cleaning: a 1-in-1000 opportunistic sweep drops
expired keys without a background timer, so the Map stays bounded
without running anything outside the request path.

Smoke test (tsx):
  - 7 broadcasts → 5 ok, 2 denied.
  - Different user → independent bucket.
  - Same user, different route → independent bucket.
  - Retry-after = 60s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:48:30 +03:00
Arnas Donauskas
185df34500
Merge pull request #68 from ArnasDon/fix/composer-alignment
fix(inbox): align composer send and template buttons with the textarea
2026-04-20 19:37:28 +03:00
Arnas Donauskas
a90ae1d991
fix(inbox): align composer send and template buttons with the textarea
The \"Type '/' for quick replies\" hint used to live inside the
textarea's flex-1 column. With items-end on the parent row, the
column's height grew by the hint's line box and the buttons aligned
to *that* bottom — sitting one row below the textarea on screen.

Move the hint out to a sibling of the flex row. The row now only
measures the textarea, items-end aligns the buttons level with the
textarea's bottom, and the hint drops neatly below with a pl-11
indent so it stays aligned under the textarea's left edge
(w-9 button + gap-2 = 44px).

Verified structurally via source; the /inbox route itself requires
auth so the preview couldn't exercise the rendered UI. Typecheck
clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:35:06 +03:00
Arnas Donauskas
1707aeafb6
Merge pull request #67 from ArnasDon/security/baseline-headers
security: baseline response headers (CSP report-only + OWASP set)
2026-04-20 19:27:11 +03:00
Arnas Donauskas
f199b7a7ed
security: baseline response headers (CSP report-only + OWASP set)
Adds a catch-all rule in next.config.ts that merges a set of baseline
security headers into every response — served alongside the existing
Cache-Control rules, not instead of them.

Enforced today:
  - Strict-Transport-Security: 2yr, includeSubDomains, preload-eligible.
    No-op on http://localhost.
  - X-Content-Type-Options: nosniff
  - X-Frame-Options: DENY  (same defense as frame-ancestors below, but
    older browsers only read this one)
  - Referrer-Policy: strict-origin-when-cross-origin
  - Permissions-Policy: deny camera/mic/geolocation/payment/usb. We
    don't use any of those APIs; closing them off prevents a supply-
    chain compromise or a forgotten plugin from silently enabling them.

Report-only:
  - Content-Security-Policy-Report-Only with a pragmatic allowlist:
      default-src 'self'
      script-src  'self' 'unsafe-inline' 'unsafe-eval'
      style-src   'self' 'unsafe-inline'
      img-src     'self' data: blob: https:
      font-src    'self' data:
      connect-src 'self' https://*.supabase.co wss://*.supabase.co
      frame-ancestors 'none'
      base-uri    'self'
      form-action 'self'
    'unsafe-inline' / 'unsafe-eval' are there because Next's inline
    hydration script + dev-mode require them; nonce-based CSP is a
    later project. Violations appear in the browser console but
    nothing is blocked until we flip the header name to
    `Content-Security-Policy`.

Verified against the running dev server:
  - /, /docs, /login, /api/whatsapp/webhook, /_next/static/* all
    return the six headers.
  - No CSP violations on / or /docs in the browser console.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:24:41 +03:00
Arnas Donauskas
fa257e0bd0
Merge pull request #66 from ArnasDon/ci/pr-checks
ci: typecheck + build on every PR
2026-04-20 19:17:30 +03:00
Arnas Donauskas
2f88498080
ci: typecheck + build on every PR
Adds .github/workflows/ci.yml running lint, typecheck, and build on
pull_request + push to main. Node 20 (matches the engines we'll pin
in the metadata PR), npm cache keyed on package-lock.json,
concurrency cancel so force-pushes don't queue runs.

Also adds a `typecheck` script to package.json — `tsc --noEmit`. Next
runs its own typecheck during `build`, but exposing it as a script
means editors and pre-commit hooks can invoke it cheaply, and CI can
surface type errors separately from build errors.

Build passes with dummy env vars (tested locally with .env.local
moved aside); NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY,
and ENCRYPTION_KEY are set to placeholder values inside the workflow.

Lint is wired up but marked continue-on-error for now: the repo has
14 pre-existing errors from React 19's new react-hooks/set-state-in-
effect rule, across 8 files, that are out of scope for this PR. They
still show up in the CI logs so regressions are visible; a follow-up
PR will fix them and drop the non-blocking flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:09:32 +03:00
Arnas Donauskas
8309b2a95b
Merge pull request #65 from ArnasDon/fix/webhook-fail-closed
fix(webhook): require META_APP_SECRET, fail closed
2026-04-20 18:56:28 +03:00
Arnas Donauskas
be7edfd180
fix(webhook): require META_APP_SECRET, fail closed
The webhook signature verifier used to fall open with a warning log
when META_APP_SECRET was unset — a deliberate soft-rollout choice that
is unsafe for a public template. A forker who forgets the env var
would be running a fully spoofable webhook: anyone knowing the URL
could POST fabricated status updates and drift broadcast counts.

Now:
- `verifyMetaWebhookSignature` returns false when the secret is missing
  and logs at error level, so the route responds 401 to every POST
  until the operator configures it.
- Docs mark META_APP_SECRET as **required**:
  - docs/environment-variables.md lists it under Required, not
    Recommended.
  - docs/whatsapp-setup.md §6 is now "Signature verification (required)".
  - docs/troubleshooting.md updated accordingly.
- .env.local.example lists the var with guidance on where to find it.

Also fixes a pre-existing gitignore bug: `.env*` was swallowing
`.env.local.example`, which meant forkers running
`cp .env.local.example .env.local` from the getting-started guide
hit "no such file". Added a negation for `*.example`.

Upgrade note for existing deploys: set META_APP_SECRET (Meta → App
Settings → Basic → App Secret) before updating, or inbound webhooks
will 401 until it's configured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:54:52 +03:00
Arnas Donauskas
0b8377d2f1
Merge pull request #64 from ArnasDon/feat/profile-management
feat(settings): user profile management
2026-04-20 17:43:27 +03:00
Arnas Donauskas
72a5e4cf70
feat(settings): user profile management
Adds a profile tab to /settings with the five things every B2B SaaS
user expects:

- Display name — edits profiles.full_name.
- Avatar — upload / replace / remove, stored in a new `avatars`
  Supabase Storage bucket with RLS scoped to the owner's folder.
  Migration 008_profile_avatars_storage.sql creates the bucket (public
  read, 2 MB cap, image mimes only) and the four storage.objects
  policies.
- Email — goes through supabase.auth.updateUser({ email }), which
  confirms to both the old and new addresses. UI surfaces a "check
  both inboxes" banner after save.
- Password — three-field form with a current-password gate implemented
  via a throwaway signInWithPassword() before calling updateUser.
- Active sessions — one-click "sign out of all devices" using
  supabase.auth.signOut({ scope: 'global' }), with a confirm dialog.

Plus read-only role, joined date, and user id for operator debugging.

The header avatar and sidebar user row are now both dropdown menus
linking to /settings?tab=profile and /settings?tab=whatsapp; the
settings page reads ?tab= to land on the right tab. useAuth gets a
refreshProfile() callback so the header/sidebar update immediately
after a save without a full reload.

Deferred (separate work): account deletion, 2FA, phone/timezone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 17:40:39 +03:00
Arnas Donauskas
c2839e6305
Merge pull request #63 from ArnasDon/fix/drop-default-favicon
fix(landing): use WaCRM favicon, drop Next.js default
2026-04-20 09:18:30 +03:00
Arnas Donauskas
fe14d4894f
fix(landing): drop hardcoded shortcut-icon pointing at removed favicon
layout.tsx still had `icons.shortcut: ["/favicon.ico"]`, which kept
emitting the default Next.js favicon link in <head> even after the
file was deleted. icon.tsx is the single source now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 08:59:24 +03:00
Arnas Donauskas
27479e3ebd
fix(landing): drop leftover next.js favicon.ico
We already have src/app/icon.tsx generating the WaCRM emerald mark,
but the default create-next-app favicon.ico was still in src/app/
so Next.js kept emitting <link rel="shortcut icon" href="/favicon.ico">
in <head>. Browsers prefer the ICO, so tabs still showed the default
Vercel triangle on the landing page.

Removing favicon.ico leaves icon.tsx as the sole source, which is
what the comment in that file already claimed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 08:58:02 +03:00
Arnas Donauskas
f7d576190a
Merge pull request #62 from ArnasDon/chore/require-code-owner-review
chore: require Arnas as reviewer on every PR
2026-04-19 22:22:46 +03:00
Arnas Donauskas
5f3486bb36
chore: require Arnas as reviewer on every PR (CODEOWNERS)
Adds a CODEOWNERS file mapping every path to @ArnasDon so GitHub
auto-requests his review on every PR. Pair with a branch-protection
rule on main that requires Code Owner approval to make review
mandatory before merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:09:10 +03:00
Arnas Donauskas
c28e4caa32
Merge pull request #61 from ArnasDon/feat/docs-viewer
feat(docs): in-app docs viewer at /docs
2026-04-19 21:52:27 +03:00
Arnas Donauskas
45fd2a9845
feat(docs): in-app docs viewer at /docs
Renders every file in docs/*.md as a proper documentation site styled
like Stripe / Vercel / Linear docs: sticky header, grouped left sidebar
(Setup / Deploy / Operate), dark prose with inline code pills, syntax
-ready code blocks, responsive table styling, and prev/next pagination.

- src/lib/docs/content.ts — parses md with marked and decorates the
  HTML with Tailwind utility classes via post-processing regex. Also
  rewrites inter-doc `./other.md` links to `/docs/other` and extracts
  an h2/h3 TOC for future use.
- src/app/docs/layout.tsx + src/components/docs/shell.tsx — shared
  frame with docs-scoped header (GitHub, Home, Get started), collapsing
  sidebar on mobile.
- src/app/docs/page.tsx — grouped index cards per section.
- src/app/docs/[slug]/page.tsx — individual doc + prev/next nav.
- Adds "Docs" to landing nav, footer, and a "Read the full
  documentation" CTA under the open-source cards.
- Adds `marked` dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:28:36 +03:00
Arnas Donauskas
9fc3eb772b
Merge pull request #60 from ArnasDon/fix/hostinger-managed-nodejs
fix(hostinger): recommend Managed Node.js Hosting, not VPS
2026-04-19 21:03:44 +03:00
Arnas Donauskas
029a20e407
fix(hostinger): recommend Managed Node.js Hosting, not VPS
Swaps the Hostinger recommendation on the landing page and throughout the
docs from VPS to Managed Node.js Hosting
(https://www.hostinger.com/web-apps-hosting).

The deploy walkthrough is rewritten around hPanel's Node.js app flow
(Git-based deploy, env vars panel, AutoSSL, cron via Advanced → Cron Jobs)
instead of a raw Ubuntu VPS with nginx + PM2 + certbot. VPS stays
mentioned at the bottom of the deploy page as an alternative when a
user actually needs one (background workers, custom binaries, etc.).

Also touched:
  - docs/README.md, environment-variables.md, automations-and-cron.md,
    troubleshooting.md to drop VPS-specific assumptions.
  - footer link URL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:02:10 +03:00
Arnas Donauskas
f18adf3b47
Merge pull request #59 from ArnasDon/docs/full-setup-guide
docs: end-to-end self-host setup guide
2026-04-19 20:55:45 +03:00
Arnas Donauskas
02d008b51b
Merge pull request #58 from ArnasDon/feat/landing-open-source-section
feat(landing): open-source + Hostinger deploy section
2026-04-19 20:53:24 +03:00
Arnas Donauskas
7148c209a0
docs: end-to-end self-host setup guide
Adds docs/ with a reading-order flow that covers:
  - getting started (fork, install, run locally)
  - supabase setup + migrations
  - whatsapp business api + webhook wiring
  - environment variable reference
  - hostinger vps deploy walkthrough
  - automations cron scheduling
  - troubleshooting for the common issues

Rewrites the top-level README as a pointer into docs/ instead of the
create-next-app boilerplate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:52:08 +03:00
Arnas Donauskas
dd65e532a1
feat(landing): open-source + Hostinger deploy section
Adds a #self-host landing section with two cards — one linking to
the GitHub repo (fork / clone) and one recommending Hostinger VPS
for deploying the template. Also surfaces the GitHub link from the
nav (desktop icon + mobile drawer) and the footer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:45:00 +03:00
Arnas Donauskas
5a2d11ea64
Merge pull request #57 from ArnasDon/fix/broadcasts-table-responsive
feat(mobile): tiered columns on the broadcasts table
2026-04-19 12:59:34 +03:00
Arnas Donauskas
9173e8e4aa
Merge pull request #51 from ArnasDon/feat/mobile-automation-builder
feat(mobile): automation builder fits + flows on narrow screens
2026-04-19 12:58:11 +03:00
Arnas Donauskas
9f4c34e945
Merge pull request #50 from ArnasDon/feat/mobile-pipelines-kanban
feat(mobile): snap-scroll kanban with viewport-sized columns
2026-04-19 12:56:59 +03:00
Arnas Donauskas
a7dec71589
Merge pull request #49 from ArnasDon/feat/mobile-inbox-single-pane
feat(mobile): inbox single-pane flow on phone, unchanged on desktop
2026-04-19 12:55:21 +03:00
Arnas Donauskas
6a30858341
Merge pull request #56 from ArnasDon/feat/mobile-app-shell-reapply
feat(mobile): app shell drawer + responsive header (re-apply)
2026-04-19 12:53:12 +03:00
Arnas Donauskas
8a43cc6e2f
feat(mobile): tiered columns on the broadcasts table
Every column rendered at every width, relying on horizontal scroll.
A 375px phone had to swipe sideways just to read a name. Matches
the tiering contacts already uses:

- Mobile (<sm): Name + Status only.
- sm+: adds Recipients, Date.
- md+: adds Template.
- lg+: adds Delivery + Read rate bars.

Desktop (≥lg) unchanged — all seven columns visible as before.

Salvaged from the closed PR #52 — the OG-image portion of that PR
is already on main via PR #55, so only the broadcasts-table change
is shipped here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:51:49 +03:00
Arnas Donauskas
08eb6e192d
feat(mobile): app shell drawer + responsive header
First of a series to make the app mobile-friendly. The sidebar used
to be a fixed 240px column always visible, eating most of a 375px
phone screen and leaving ~135px for content. Now:

- Sidebar becomes a slide-in drawer on <lg, static on ≥lg. Controlled
  via a drawer-open prop wired from the shell; transform + duration
  feels native on touch devices.
- Backdrop overlay closes it on tap; Escape closes it; body scroll
  locks while open; route changes auto-close it.
- Header gains a 44×44 hamburger button on <lg that opens the drawer,
  hidden on ≥lg. Title shrinks slightly on narrow widths so the
  hamburger + avatar + sign-out row never wraps.
- Nav rows are taller (py-2.5) on mobile for comfortable touch
  targets, slim (py-2) back on desktop.
- Main content padding is p-4 on mobile, p-6 on desktop so cards
  have room to breathe without shouldering into the viewport edge.

No desktop changes visible beyond tighter padding on very narrow
lg breakpoints (main content width is unchanged).

Next PRs in this series will adapt:
- Inbox (3-pane → single-pane with back)
- Pipelines (kanban snap-scroll)
- Automation builder (card widths + bottom-sheet edit)
- Tables + forms polish

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:44:31 +03:00
Arnas Donauskas
6a38fce6f6
Merge pull request #55 from ArnasDon/fix/opengraph-image-build-crash
feat(seo): full SEO pass (reapply with OG-image build fix)
2026-04-19 12:29:57 +03:00
Arnas Donauskas
15e4b5366d
feat(seo): full SEO pass (reapply with OG-image build fix)
Re-applies the original SEO work from PR #47 that was reverted in
PR #54 because its opengraph-image.tsx crashed `next build` during
static prerender of /opengraph-image. Satori (next/og's renderer)
errored with:

  Error: Expected <div> to have explicit "display: flex",
         "display: contents", or "display: none" if it has more
         than one child node.
  Failed to load dynamic font for ✓ . Status: 400

Root cause in the OG image JSX:
- Several <div>s held text + a sibling element (e.g. "Run your
  WhatsApp business from <span>one inbox.</span>") without an
  explicit display value. Satori requires it whenever there is
  more than one child.
- The ✓ Unicode glyph isn't covered by the default font Satori
  loads, so next/og tried to download a dynamic font and failed.

Fix:
- Every <div> with more than one child now has display: flex.
- Multi-span headline sits in a flex-wrap row of two <span>s.
- ✓ replaced with plain text dot separators in the feature row.

Verified end-to-end: `npm run build` now completes with 27/27
pages prerendered, /opengraph-image among them.

All the other SEO-pass content is identical to the originally
merged-and-reverted work: metadataBase + title template, OpenGraph
+ Twitter Cards defaults, JSON-LD (WebSite + Organization +
SoftwareApplication + FAQPage), sitemap.ts, robots.ts, noindex on
auth + dashboard route-group layouts, shared faq-data + site-config
modules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:25:59 +03:00
Arnas Donauskas
3d90cc17dd
Merge pull request #54 from ArnasDon/revert-47-feat/seo-pass
Revert "feat(seo): full SEO pass — metadata, structured data, sitemap, robots"
2026-04-19 12:15:54 +03:00
Arnas Donauskas
adf78bb2ee
Revert "feat(seo): full SEO pass — metadata, structured data, sitemap, robots" 2026-04-19 12:15:42 +03:00
Arnas Donauskas
ad6c5eba75
Merge pull request #53 from ArnasDon/revert-48-feat/mobile-app-shell
Revert "feat(mobile): app shell drawer + responsive header"
2026-04-19 12:14:35 +03:00
Arnas Donauskas
aca0d3be8b
Revert "feat(mobile): app shell drawer + responsive header" 2026-04-19 12:14:24 +03:00
Arnas Donauskas
d913e6c8c3
Merge pull request #48 from ArnasDon/feat/mobile-app-shell
feat(mobile): app shell drawer + responsive header
2026-04-19 12:12:51 +03:00