Content Board Refresh, Profile Merge, and Billing
This page records the work done in one pass on 2026-10-16: the content board was restyled to a task-card look, /app/account was folded into /app/profile, a new billing layer was added, and Snapchat was connected end to end.
1. Content board (/app/business/[id]/content)
What changed. Columns are now bordered panels with a header that carries the column mark, its name, its count, a + and a … menu, and a footer Add task. Cards became task cards: a chip row (the deliverable, then up to two platforms), the title (struck through with a green check in Published), and a meta line with a short board code, a priority mark, a relative date and an avatar for whoever made the card. Right-clicking — or the … — opens a context menu: Open, Edit, Write now, a Status submenu (the columns the state machine will actually accept), Copy link, and Delete.
Why. The old board was single-line rows with no way to see the deliverable, the platform or the age of a card at a glance.
How it works. The board keeps its existing state machine; the new presentation is derived from data the server already returns:
packages/connect/app/utils/content-card.ts— pure helpers:contentCardCode(CNT-+ the last three id characters),cardDayLabel(Today / Yesterday / weekday /Mon D),cardDate(published → scheduled → updated) andpriorityTier.ContentBoardCard.vue/ContentBoardColumn.vue— the presentation. The avatar showsAIfor agent-made cards and the signed-in owner's initials for their own.- Card i18n lives in
packages/site/app/pages/app/business/[id]/content.json.
Honest limits. The reference design shows a progress bar; the board has no progress data, so no bar is drawn rather than a fabricated one. "Add task" appears on Planned only, because that is where a new card actually lands — the other columns would have to lie about filing. The reference's Assign to / Priority / Move to project / favourites / Duplicate / Archive items are omitted: the board has no assignee, project, or duplicate/archive action.
2. Single profile view
What changed. /app/profile is now the one profile + account page: a header (avatar, name, role badge, email, verified state, member-since), a four-up strip (businesses, linked accounts, role, email), then tabs for Profile, Account, Linked accounts, AI model and Danger zone, with the avatar uploader and quick links in a side column. /app/account was removed and every internal link now points at /app/profile.
How it works. The account panels (AccountSettings, LinkedAccounts, AiModelSettings, DangerZone) are reused unchanged; their translation file moved out of the pages/ tree to packages/auth/app/components/auth/account/components/account.json so no route directory holds a stray file. The four-up strip is composed from data the page already loads (useBusinessManager, UseUser().getUserAccountList).
3. Billing layer (/app/billing)
What changed. A new Nuxt layer, packages/billing, plus a page at /app/billing with three tabs: Plan (current subscription, usage meters, plan comparison), Payment (card, billing email/address, tax id) and Invoices (past invoices). All three read the Stripe customer through /api/v1/billing/overview.
Why. There was no billing surface at all.
How it works.
- The layer (
$meta.name: 'BaseBilling') extendsdb,uiandauth, and is registered inpackages/site/nuxt.config.ts. packages/billing/app/utils/billing-plans.tsholds the display catalog (prices, limits, feature keys). Stripe remains the authority for charges.packages/billing/server/services/billing.service.tsreads the current subscription through the better-auth Stripe plugin (auth.api.listActiveSubscriptions) and the customer, payment method and invoices through the Stripe SDK. WithoutSTRIPE_SECRET_KEYit returnsconfigured: falseand the page renders its empty state.packages/billing/app/composables/useBilling.tsowns the read and hands upgrades / the customer portal toauthClient.subscription.
Enabling Stripe. Set STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_TEAM and STRIPE_PRICE_BUSINESS; the plugin in packages/auth/lib/auth.ts registers itself only when the key and webhook secret are present. Point the Stripe webhook at /api/auth/stripe/webhook. The subscription table and the two stripe_customer_id columns are added by migration 0012_complete_blonde_phantom.sql. The full walkthrough — products, prices, webhook events, customer portal, migration, testing — is Connecting Stripe.
Who may use the app. packages/billing/server/utils/entitlement.ts decides, server-side:
- Admins never pay.
role === 'admin'is entitled unconditionally, and the subscription lookup is skipped so admin access never depends on Stripe. - Every new account gets a 10-day free trial (
TRIAL_DAYS), counted fromcreatedAt— derived, so it cannot drift from the user row. - A paid subscription (
active/trialing) wins over the trial.
The states are admin, active, trialing, trial, expired, and the billing page shows a banner for the one that applies. Blocking is off by default (BILLING_ENFORCE_ACCESS=true, or NUXT_PUBLIC_BILLING_ENFORCE at runtime) — inverting the default would lock out every account that predates billing. With it on, 05.billing-access.global.ts redirects an expired account to /app/billing and fails open if the lookup errors.
4. Snapchat
See Snapchat Integration for the full setup guide. In short: an inline genericOAuth provider (no extra dependency), a SnapchatPlugin, and the platform registered in the shared constants, the database enum, the plugin matcher, and the platform icon / filter / preview tables.