Replaces the static template list with a working multi-entity command
palette tied to the active workspace.
Server (lib/appwrite/search-actions.ts):
- globalSearchAction(query): runs after the user has typed >= 2 chars.
Pulls up to 200 rows per entity for the active tenant via requireTenant
and admin SDK, then in-memory filters on:
customers: name, email, phone, taxId
invoices: number, notes, customer name (via id->name map)
tasks: title, description
services: name, description
software: name, version, description
calendar events: title, description
finance entries: description, customer name, amount string
- Returns at most 8 hits per group. Each hit has { title, subtitle, url,
group } so the client doesn't need extra lookups. Turkish-aware
toLocaleLowerCase('tr-TR').
Client (components/command-search.tsx):
- Rewritten. cmdk Command with shouldFilter=false (we provide filtered
results from the server).
- 220ms debounce on input; spinner during fetch.
- Ordered groups: Müşteriler / Faturalar / Görevler / Takvim / Finans /
Hizmetler / Yazılımlar — each with its own icon.
- Static groups always evaluated client-side from the typed query:
* Sayfalar (8 nav items)
* Hızlı aksiyonlar (5 — yeni müşteri / fatura / görev / etkinlik /
finans girişi)
* Ayarlar (3 — şirket bilgileri / ekip / profil)
- Empty-state message ('Sonuç bulunamadı') only shown when the query
is non-trivial AND nothing matches anywhere.
- Footer hint row with ↵/↑↓/Esc/⌘K legend.
- Invoice hits navigate to /invoices/[id]; other entity hits go to the
list page (no per-id detail routes for those yet).
Trigger button (SearchTrigger): localized to 'Hızlı ara...'.
Replaces template's mock account form with real Appwrite-backed actions.
Server actions (lib/appwrite/profile-actions.ts):
- updateNameAction: account.updateName via session SDK; revalidates layout
so the new name shows in sidebar/header right away.
- updateEmailAction: account.updateEmail (requires current password as
Appwrite confirmation). Maps user_email_already_exists to a friendly
Turkish message.
- updatePasswordAction: account.updatePassword(new, old). Validates
old != empty, new >= 8 chars, new === confirm. Maps
user_password_recently_used / user_password_mismatch.
- All audit-logged with entityType user_name / user_email / user_password
and tenantId of the user's currently-active workspace (or 'global' if
none). Audit failures swallowed.
UI:
- /settings/account is now an async server page that pulls the user via
getCurrentUser, renders an account info card ($id, registration date,
email verification, MFA), then 3 separate small forms — one card each
— for name, email, password. Each form clears its own state and gives
toast feedback independently.
- Removed the template's react-hook-form-based mock page.
Side note: skipped email-verification flow + MFA setup for later.
Owner/admin edit, member read-only.
Schema/validation:
- lib/validation/workspace.ts (workspaceSettingsSchema)
- lib/appwrite/workspace-actions.ts:
* updateWorkspaceSettingsAction — requireRole owner|admin, upserts the
tenant_settings row (creates one with team-scoped perms if absent,
e.g. for tenants created before tenant_settings was a table; just
defense). Audit-logged.
* Forces invoicePrefix to uppercase. defaultVatRate clamped to [0, 100].
* revalidatePath('/', 'layout') so the new company name updates in
sidebar header and dashboard greeting on next render.
UI:
- /settings/workspace page (server) — pulls active tenant settings
via requireTenant, shows form pre-filled.
- WorkspaceSettingsForm: 2 cards
* Şirket — name (required), tax id, phone, email, address
* Faturalama — invoicePrefix, defaultVatRate, read-only invoiceCounter
- All inputs disabled if user is a member (canEdit=false). Submit button
hidden in that case. Description on the page changes accordingly.
- Toast feedback for success/error.
Skipped: logo upload (storage bucket pending). Will revisit.
Multi-tenant teams need a way to focus on their own work without seeing
the whole team's board. Added a server-side filter:
- /tasks?view=mine_or_unassigned (default): own assigned + unassigned
- /tasks?view=mine: only assigned to me
- /tasks?view=unassigned: claimable, unassigned tasks
- /tasks?view=all: full team board (managers / overview)
Implementation:
- Server page reads ?view= query, validates against allowed list,
filters tasks before passing to the client. Also computes total
counts (across all rows) for each filter so the dropdown can show
'(N)' badges that don't change when the user switches views.
- TasksBoard top-bar gets a Select that updates the URL via
router.push() (preserves Next's full SSR + revalidatePath flow).
- Default-filter URL drops the ?view= param to keep the canonical
/tasks URL clean.
Card visual cues:
- Tasks assigned to current user get a primary-tinted ring + a 'Bana
atanmış' badge (replaces the assignee name pill).
- Unassigned tasks get a dashed border + 'Atanmamış' badge so they
visually invite ownership.
Dashboard:
- 'Açık görevler' metric is now 'Açık görevlerim' — sums only own +
unassigned tasks. Subtext updates accordingly. Same scoping for
urgent count. Managers can still see team-wide via /tasks?view=all.
Dashboard is no longer mock data. Single getDashboardData(tenantId) server
query computes everything in one pass.
New aggregator (lib/appwrite/dashboard-queries.ts):
- Pulls customers, invoices, finance_entries, tasks, services in parallel.
- Derives:
* metrics: totalCustomers, activeCustomers, monthIncome,
prevMonthIncome (for delta), outstanding (unpaid invoice total),
overdueCount, openTasks, urgentTasks
* monthlyIncome: 12-month income+expense series for area chart
* topCustomers: 5 highest-grossing customers by paid invoice total
* recentTransactions: 8 newest finance entries
* topServices: 5 services by aggregate unit price (placeholder, will
refine when we have invoice line analytics)
* newCustomersMonthly: 6-month new customer count for bar chart
Components (dashboard/components/):
- Metrics: 4 cards with trend indicator on income (delta vs previous
month), warning tone on overdue invoices and urgent tasks.
- IncomeChart: Recharts Area chart, dual income/expense series with
gradient fills, Turkish month labels.
- TopCustomers: ranked list with progress bars relative to top earner.
- RecentTransactions: list with type badge, signed amount, link to
/finance for full list.
- CustomerGrowth: BarChart of new customers per month (last 6).
- QuickActions: 4 buttons linking to /customers, /invoices, /calendar,
/tasks (replaced template's New User/Add Product/etc).
Layout: 4 metric cards row, then income chart + top customers (2-col),
then recent transactions + customer growth (2-col).
Removed:
- src/app/(dashboard)/dashboard-2/ (was the demo page; same components
re-exported into the real /dashboard from there. Now /dashboard owns
its components.)
- 'Dashboard 2' entry from CommandSearch; replaced with our actual
module list (Müşteriler / Hizmetler / Yazılımlarımız / Takvim /
Görevler / Gelir-Gider / Faturalar).
Marking an invoice as paid now creates a finance_entry (type=income) for
the customer with amount = invoice.total, linked via invoiceId. Reverting
status removes the entry. Idempotent: re-saving while already paid keeps
the existing entry (resyncs amount if invoice total changed in the
meantime).
- syncPaymentEntry(tenantId, userId, invoice) helper:
* status === 'paid': create entry if none exists; otherwise update
amount to match current invoice.total.
* status !== 'paid': delete any income entries linked to the invoice.
* Best-effort — failures are swallowed so the invoice mutation always
succeeds even if Appwrite hiccups on the finance write.
* Each create/delete writes an audit row tagged auto: 'invoice_paid' /
'invoice_unpaid' so we can trace later.
- updateInvoiceAction now calls syncPaymentEntry after persisting.
- recomputeTotals (run on every item add/update/delete) also re-syncs
the linked entry's amount when the invoice is currently paid.
- deleteInvoiceAction now cascade-deletes any linked finance_entries in
addition to items.
- /invoices and /invoices/[id] both revalidate /finance after writes.
UI:
- Invoice form shows a hint under the status select explaining the
finance side effect.
- Finance table tags rows with a 'Faturadan' badge when invoiceId is
set, so users can tell auto-generated entries apart from manual ones.
The most complex module. Two-table model: invoices (header) +
invoice_items (lines). Auto-numbering via tenant_settings.invoiceCounter,
auto-totals on item changes.
Schema/validation:
- lib/validation/invoices.ts: invoiceSchema (header) + invoiceItemSchema
(line). Both coerce comma decimals.
- lib/appwrite/invoice-actions.ts:
* createInvoiceAction — fetches tenant_settings, increments
invoiceCounter, formats number as '{prefix}-{year}-{0000}',
persists totals as 0/0/0 (recomputed when items added).
* updateInvoiceAction / deleteInvoiceAction — header CRUD; delete
cascades to remove all items first then header.
* addInvoiceItemAction / updateInvoiceItemAction /
deleteInvoiceItemAction — line CRUD. Each computes lineTotal
(qty*unit + vat) and triggers recomputeTotals(invoiceId) which
re-sums all items and updates the header subtotal/vatTotal/total.
All audit-logged.
Queries:
- listInvoices, getInvoice (with tenant cross-check), listInvoiceItems.
UI:
- /invoices index: 4 stat cards (Toplam / Tahsil edildi / Bekleyen /
Gecikmiş), table with overdue-aware due date coloring, status badges,
number is a Link to detail.
- InvoiceFormSheet: customer + dates (default issue=today, due=+30d) +
status + notes. After create, redirects to /invoices/[id] for adding
items.
- /invoices/[id] detail: header strip with status, dates, customer name;
print/edit/delete actions; items editor card; subtotal/VAT/total card;
notes card.
- InvoiceItemsEditor: rows are clickable to edit, X button to delete.
ItemFormSheet for add/edit (description + qty + unitPrice + VAT %).
Print is just window.print() for now — relies on browser dialog. Detail
page deliberately uses tabular-nums for amounts.
Multi-tenant cash flow tracker. All amounts in TRY, decimals preserved.
Schema/validation:
- lib/validation/finance.ts: financeEntrySchema with type enum, positive
amount, date required, optional customer/invoice link, optional payment
method.
- lib/appwrite/finance-actions.ts: create/update/delete with audit; date
HTML input normalized to ISO before write.
- lib/appwrite/finance-queries.ts: listFinanceEntries ordered by date desc.
UI:
- /finance server page passes entries + customers to FinanceClient.
- 5 stat cards: Gelir / Gider / Net (income-expense, color-coded by sign)
/ Alacaklar / Borçlar.
- Type filter dropdown (Tümü/Gelir/Gider/Alacaklar/Borçlar) + global
search (description/customer/amount).
- 4 quick-add buttons let users start a new entry pre-filled with the
desired type. Single FinanceFormSheet handles all 4 types via a Select.
- Table: type badge (color-coded), signed amount (+ for income/receivable,
− for expense/debt), date, customer, payment method label, description
preview. Row dropdown: Edit / Delete.
- Inline destructive Sil button in Sheet footer when editing.
Replaces the template's static-data calendar with a multi-tenant calendar
backed by Appwrite calendar_events.
Schema/validation:
- lib/validation/calendar.ts (calendarEventSchema with cross-field check
end >= start)
- lib/appwrite/calendar-actions.ts: createCalendarEventAction,
updateCalendarEventAction, deleteCalendarEventAction. Date inputs
(HTML datetime-local 'YYYY-MM-DDTHH:mm', date 'YYYY-MM-DD') are
normalized to ISO 8601 before write.
- lib/appwrite/calendar-queries.ts: listCalendarEvents with optional
start/end range queries.
UI:
- /calendar server page: pulls events + customers, hands to CalendarClient.
- CalendarClient: month grid (6 rows × 7 cols), Monday-first, today badge,
prev/next/Bugün nav. Multi-day events show on every day in their range.
Each day cell shows up to 3 event chips with start time prefix; '+N
daha' for overflow. Hover reveals a + button to add an event on that day.
- EventFormSheet: title, all-day switch (toggles input type between
date and datetime-local), start/end with validation, customer FK,
color preset (blue/green/amber/red/violet/slate). Sentinel '__none__'
for nullable Selects. When editing, footer shows a destructive 'Sil'
ghost button on the left that triggers the parent's confirm dialog.
Color tokens centralized in COLOR_BG map; falls back to primary tint.
Removed all template calendar files (calendars.tsx, calendar-main, etc.)
since the data model didn't match.
Replaces the template's /tasks demo (deleted) with a real multi-tenant
Kanban board.
Schema/validation:
- lib/validation/tasks.ts (taskSchema with status/priority enums + dueDate
optional + assignee/customer optional)
- lib/appwrite/task-actions.ts: createTaskAction, updateTaskAction,
deleteTaskAction, moveTaskAction (used by drag-drop). All audit-logged;
moveTaskAction only audits when status actually changes.
- lib/appwrite/task-queries.ts: listTasks ordered by 'order' asc.
UI:
- /tasks server page assembles { tasks, customers, teamMembers } and
passes to TasksBoard. Removed the template's data-table demo files.
- TasksBoard (client): 4 droppable columns. Columns use @dnd-kit/core
useDroppable; cards inside each column are SortableContext+useSortable
for intra-column ordering. closestCorners collision detection.
- Drag-end computes new 'order' as midpoint between adjacent tasks
(no full reindex), updates UI optimistically, then persists via
moveTaskAction. Rolls back on server error with toast.
- TaskCard: priority badge (color-coded), due-date badge (red if
overdue), assignee badge, customer subtitle, dropdown (Edit/Delete)
on hover.
- TaskFormSheet: title/description/status/priority/dueDate/assignee/
customer. Uses sentinel '__none__' for nullable Selects (Radix Select
forbids empty string values), stripped before submit.
- DragOverlay shows the dragged card rotated 3deg with shadow.
Both files use client-only hooks (useTheme in sonner; useState/useMemo/
useId in chart via React) but the shadcn CLI shipped them without the
'use client' directive. Mounting <Toaster /> in the root server layout
crashed in production with:
Attempted to call useTheme() from the server but useTheme is on the
client.
Same risk on chart.tsx once any dashboard page renders <Chart>.
Verified all of src/components/ui/*.tsx — only these two needed the fix.
Future shadcn additions: check 'head -1' for the directive before
importing into server components.
Software catalog with per-customer assignments via the customer_software
join table. Two tabs in one /software page:
Catalog tab:
- Software CRUD: name, version, description, defaultFee (TRY).
- Deleting a software cascades and removes all its assignments first
(best-effort loop, then the catalog row), all wrapped in audit logs.
Assignments tab:
- M2M between customer and software with own fee (overrides defaultFee),
billingPeriod (monthly default), startDate/endDate, notes.
- Form auto-fills fee from selected software's defaultFee.
- Both Sheet forms localized; date inputs round-tripped via toIsoDate
(Appwrite expects ISO 8601 with TZ; HTML date input gives YYYY-MM-DD).
- Delete dialogs differentiated for catalog ('siliniyor') vs assignment
('kaldırılıyor').
New files:
- lib/validation/software.ts (softwareSchema + customerSoftwareSchema)
- lib/appwrite/software-actions.ts (6 server actions)
- lib/appwrite/software-queries.ts (listSoftware, listAssignments)
- lib/appwrite/software-types.ts (form state)
- /software route with SoftwareClient (Tabs), SoftwareFormSheet,
AssignmentFormSheet, inline delete dialogs.
Empty states surface the right next-step CTA: 'önce müşteri ekleyin', or
'önce yazılım ekleyin', as appropriate.
Same pattern as customers. Each service belongs to one customer (FK), has
unit price (TRY), billing period (monthly/yearly/onetime), and recurring
flag.
- lib/validation/services.ts: Zod schema with TRY price coercion (accepts
comma decimal), enum billing period, recurring boolean coercion.
- lib/appwrite/service-actions.ts: createServiceAction, updateServiceAction,
deleteServiceAction. Same tenant guard, audit log, team-scoped row perms.
- lib/appwrite/service-queries.ts: listServices, listServicesByCustomer.
- lib/format.ts: formatTRY (Intl.NumberFormat tr-TR), formatDate/DateTime,
BILLING_PERIOD_LABEL mapping (Aylık/Yıllık/Tek seferlik).
- /services page (server): joins services with customer names via
in-memory map.
- ServicesClient: TanStack table with global filter (name/customer/desc),
Repeat icon next to recurring badges, formatted TRY column.
- ServiceFormSheet: customer dropdown (disabled if no customers exist),
unit price + billing period + recurring switch.
- DeleteServiceDialog: same destructive confirmation pattern.
Empty state on /services CTA's user back to add a customer first if none
exist.
Establishes the multi-tenant module pattern. Subsequent modules (services,
software, calendar, tasks, finance, invoices) will copy this structure.
Validation:
- lib/validation/customers.ts: Zod schema with Turkish messages, optional
fields normalized to undefined.
Server actions (lib/appwrite/customer-actions.ts):
- createCustomerAction, updateCustomerAction, deleteCustomerAction
- All call requireTenant() guard, write team-scoped row permissions
(read+update by team, delete by owner|admin), and emit audit log.
- Update/delete cross-check tenantId on the existing row before mutating
(defense in depth even though row-level perms already enforce it).
- Field-level errors flattened from Zod for inline form display.
Server-side queries (lib/appwrite/customer-queries.ts):
- listCustomers(tenantId), getCustomer(tenantId, id) — admin SDK with
Query.equal('tenantId',...) tenant scope.
UI:
- /customers page (server component): pulls active tenant context, lists
customers, hands off to CustomersClient.
- CustomersClient: TanStack Table with global filter (name/email/phone/
taxId), column sorting on name + createdAt, pagination (20/page),
status badges, row actions (Edit/Delete dropdown), empty-state CTA.
- CustomerFormSheet: shadcn Sheet-based add/edit form with all fields,
toast feedback (sonner), inline field errors. Reused for create + update
by switching the action.
- DeleteCustomerDialog: confirmation modal with destructive button.
Infrastructure:
- Added sonner Toaster to root layout (richColors, closeButton).
- Updated metadata to 'İşletmem KovakCRM' and html lang='tr'.
- Renamed theme storage key to isletmem-ui-theme.
Multi-tenant invite system without SMTP dependency. Designed for dev/early
stage; promotes to email-driven later by adding SMTP to Appwrite.
New schema:
- invite_links table (code, email, role, status, expiresAt, invitedBy)
with unique index on code, indexes on (tenantId,status) and (tenantId,email)
New code:
- lib/appwrite/audit.ts: logAudit() helper writes to audit_logs with
X-Forwarded-For/User-Agent capture; never throws.
- lib/appwrite/tenant-guard.ts: requireTenant() returns
{ user, tenantId, role, settings }; pulls highest role from team
memberships. requireRole() guard.
- lib/appwrite/team-actions.ts:
* inviteMemberAction — creates short code (8 char nanoid-style),
inserts invite_links row with team-scoped perms, returns shortUrl.
Reuses existing pending invite for same email instead of duplicating.
Blocks self-invite, blocks invite of existing members.
* cancelInviteAction — owner/admin only, marks status=cancelled.
* removeMemberAction — owner/admin only; protects self-removal and
requires owner-on-owner.
* updateMemberRoleAction — owner only.
* resolveInviteCode — public-ish lookup by code (admin SDK).
* acceptInviteAction — verifies session.email matches invite.email,
creates membership via admin SDK, marks invite accepted.
All mutations write to audit_logs.
UI:
- /d/[code] short-URL accept page (server). Logged-in matching user
sees 'Daveti kabul et' button; non-matching user sees error; logged-out
user gets sign-up / sign-in CTAs that preserve the code.
- /settings/members page (server): InviteForm, PendingInvitesTable,
MembersTable. Owner/admin gates respected; only owner can change roles.
- Sign-up and sign-in forms accept ?invite=CODE (and ?email= for sign-up):
hidden input -> server action redirects to /d/CODE on success.
Other:
- next.config.ts: removed eslint config block (deprecated in Next 16);
kept typescript.ignoreBuildErrors for template legacy.
Template files (src/components/ui/chart.tsx, src/app/(dashboard)/tasks/
components/data-table-toolbar.tsx) carry pre-existing type errors that
block 'next build' but don't affect runtime. Our own code (lib/appwrite/*,
auth pages, dashboard) typechecks cleanly.
This unblocks Coolify deploys to isletmem.kovakcrm.com. Will revisit and
clean up the template files in a follow-up so we can re-enable strict
build-time checks.
- Layout split: (dashboard)/layout.tsx is now async server component that
fetches active context and passes user/company to (dashboard)/dashboard-shell.tsx
(client). Redirects to /onboarding if no tenant.
- AppSidebar:
* Header shows 'İşletmem' + the active company name (companyName from
tenant_settings), instead of mock 'ShadcnStore / Admin Dashboard'.
* Nav rebuilt for our modules in Turkish: Genel bakış, Müşteriler,
Hizmetler, Yazılımlarımız, Takvim, Görevler, Gelir/Gider, Faturalar,
Çalışma alanı (with submenu), Profil, Plan.
* Removed SidebarNotification (template promo widget).
* Accepts user/company props (typed via ShellUser/ShellCompany).
- NavUser:
* Real user name + email, no more 'ShadcnStore / store@example.com'.
* Avatar shows initials from name in primary/10 tinted square.
* Logout wired to signOutAction (server action) via useTransition.
* Menu items localized (Profil, Plan & Faturalama, Bildirimler, Çıkış yap).
- SiteHeader:
* Removed Blocks / Landing / GitHub external links (template demo links).
* Shows company name with Building2 icon between sidebar trigger and
search trigger.
* Search trigger moved to right side next to ModeToggle.
- Dropped UpgradeToProButton from the shell (template promo).
- Deleted dead-code src/components/layouts/base-layout.tsx (unused alt
layout that wasn't compatible with the new AppSidebar props).
- /dashboard now a server component:
* fetches active user + active tenant settings via getActiveContext()
* redirects to /onboarding if user has no tenant yet
* header shows companyName + 'Hoş geldiniz, {firstName}' + Turkish description
- Body reuses dashboard-2 components (Metrics/Sales/Revenue/Transactions/
TopProducts/CustomerInsights/QuickActions). Mock data for now; will be
swapped for live Appwrite queries as modules ship.
- New lib/appwrite/active-context.ts: getActiveContext() returns
{ user, tenantId, settings } for any server component / action.
- Made schema.ts SDK-agnostic by replacing Models.Document import with a
local SystemRow type ({ $id, $createdAt, $updatedAt, $permissions, ... }).
Avoids type clashes between appwrite (browser) and node-appwrite (server).
Two fixes triggered by user-reported error 'Invalid document structure:
Unknown attribute createdBy' during onboarding:
1) tenant_settings has no createdBy column by design (one row per tenant,
creator metadata is redundant). Removed createdBy from the row payload.
2) Made the action atomic: if any step after teams.create fails (row write,
prefs, cookie), delete the just-created team using the admin client.
Without this, two failed attempts left two orphan teams; reload then
redirected the user to /dashboard with no tenant_settings, trapping them.
Already cleaned up the two orphan teams via Appwrite MCP.
- /onboarding page (server component): redirects to /sign-in if not authed,
to /dashboard if user already has a team. Otherwise renders form.
- createWorkspaceAction:
* teams.create (user becomes owner via session SDK)
* tablesDB.createRow on tenant_settings (admin SDK) with team-scoped permissions:
Permission.read(Role.team(id)), update(owner|admin), delete(owner)
* account.updatePrefs({ activeTenant }) — persisted source of truth
* isletmem-tenant cookie for fast access
* redirect to /dashboard
- setActiveTenantAction stub for future workspace switcher
- lib/appwrite/tenant.ts: getUserTeams, getActiveTenantId helpers (server-only)
- tenant-types.ts holds WorkspaceState + ACTIVE_TENANT_COOKIE (no 'use server')
node-appwrite 24.x and appwrite 25.x target Appwrite server 1.9.1.
Our self-hosted server runs 1.9.0, which produced an SDK-version
mismatch warning on every API call.
Pinned:
node-appwrite: ^23.1.0 (was ^24.0.0)
appwrite: ^24.2.0 (was ^25.0.0)
v23/v24 keep both positional and params-object overloads, so existing
auth-actions.ts calls (createEmailPasswordSession, createRecovery,
account.create) compile and run unchanged. When we upgrade Appwrite to
1.9.1 we can bump the SDKs back.
Server-action files ('use server') can only export async functions.
Exporting initialAuthState (object) caused:
'A use server file can only export async functions, found object'
when sign-up form was submitted.
Moved AuthState type and initialAuthState const to lib/appwrite/auth-types.ts.
Updated 3 form components to import the const from the new location.
- Server actions in lib/appwrite/auth-actions.ts:
signInAction, signUpAction, forgotPasswordAction, signOutAction
All use node-appwrite admin client; session secret stored as httpOnly
cookie (isletmem-session). Errors localized to Turkish.
- Redesigned /sign-in and /sign-up using sign-in-3 split-card layout,
branded as 'İşletmem' with gradient brand panel (no external image).
Removed social login buttons (email/password only for now).
- /forgot-password localized; success state shows email-sent confirmation.
- Auth pages redirect to /dashboard if user already has a session.
- middleware.ts:
* Protects /dashboard, /onboarding, /settings — redirects to /sign-in?redirect=...
* Auth pages redirect logged-in users to /dashboard
* Keeps legacy /login and /register redirects