Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6d7b467
Add OpenAPI spec and an MCP server for the external API
Coriana Jul 9, 2026
19b0912
Housekeeping: drop dead Supabase/bun leftovers, fix scripts, add CLAU…
Coriana Jul 9, 2026
9c31616
Add automatic SQLite backups
Coriana Jul 9, 2026
66b0a2d
Create the DB directory on boot so a fresh checkout runs
Coriana Jul 9, 2026
5a14755
Migrate Dashboard to TanStack Query
Coriana Jul 9, 2026
bcd7976
Migrate Clients and Jobs list pages to TanStack Query
Coriana Jul 9, 2026
b4d56c1
Migrate Inventory/Assets/Issues/Locations lists to TanStack Query
Coriana Jul 9, 2026
772f523
Fix UTC date defaults and setup wizard invoice numbering
Coriana Jul 10, 2026
2af106d
Add mark-as-sent and record-payment actions to the invoice page
Coriana Jul 10, 2026
9a9e142
Support nested relations in the CRUD shim and fail loudly on unknown …
Coriana Jul 10, 2026
98b081c
Compute dashboard stats from full data instead of limit-10 lists
Coriana Jul 10, 2026
e85ac1c
Replace native confirm() with a shared AlertDialog confirm flow
Coriana Jul 10, 2026
1a60a43
Standardize on Client terminology for the client entity
Coriana Jul 10, 2026
57b4459
Honor branding currency and localize table dates app-wide
Coriana Jul 10, 2026
78d5a08
Group the sidebar into sections and add a Ctrl/Cmd+K command palette
Coriana Jul 11, 2026
f68ed81
Add person-centric contacts with client/vendor affiliation history
Coriana Jul 11, 2026
02eab1d
Add Contacts pages and rebuild client/vendor panels on affiliations
Coriana Jul 11, 2026
77d2a63
Consolidate vendor_contacts into the unified contacts model
Coriana Jul 11, 2026
c77e686
Sync legacy contact_* columns from the current primary affiliation
Coriana Jul 11, 2026
1d134e7
Support concurrent affiliations, date editing, and primary dropdowns
Coriana Jul 11, 2026
7eebead
Add mobile card layouts to the eleven primary list pages
Coriana Jul 11, 2026
ab6961b
Route remaining hardcoded currency and date displays through branding
Coriana Jul 11, 2026
38b176f
Add a status filter pill row to the invoice list
Coriana Jul 11, 2026
46c9b6d
Give list pages real empty states with create CTAs
Coriana Jul 11, 2026
84413d8
Add CSV export to every report
Coriana Jul 11, 2026
33aaecc
Add dark mode with a system-aware theme toggle
Coriana Jul 12, 2026
a9dec7c
Generate invoice PDFs server-side instead of popup-and-print
Coriana Jul 12, 2026
f65d6d8
Theme chart axes, tooltips, and hover cursors for dark mode
Coriana Jul 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,11 @@ SMTP_PORT=587
SMTP_USER=
SMTP_PASS=
SMTP_FROM=

# Automatic SQLite backups (optional - sensible defaults are used if unset)
# Directory where backup files are written (default: ./data/backups)
BACKUP_DIR=./data/backups
# How often to run a backup, in hours. Set to 0 to disable backups entirely (default: 24)
BACKUP_INTERVAL_HOURS=24
# How many of the newest backups to keep; older ones are deleted (default: 7)
BACKUP_RETENTION=7
116 changes: 116 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# CLAUDE.md — guidance for AI coding agents

Client Flow is a business-management app: a React/Vite frontend with a local
Express/SQLite (better-sqlite3) backend. The frontend talks to the backend
through a **Supabase-compatible client shim** (`src/integrations/api/client.ts`),
so app code reads like Supabase but there is no Supabase at runtime.

## Commands

```bash
npm run dev # frontend (:8080) + backend (:3001) together
npm run dev:client # Vite only
npm run dev:server # API server only (tsx watch)
npm run build # production build
npm run lint # ESLint (note: repo has pre-existing lint debt)
npm run typecheck # tsc --noEmit for the server project
npm run test # Vitest (watch)
npm run test:run # Vitest (once) — server integration tests
npm run db:reset # delete data/app.db and restart the server
```

## Architecture

```
Frontend (React/Vite :8080) <--> Backend (Express/SQLite :3001)
src/integrations/api/client.ts server/
(Supabase-compatible shim) ├── index.ts app wiring, CORS, rate limiting
├── db/{schema.sql, seed.sql, database.ts, columns.ts}
├── routes/{auth,crud,storage,functions,
│ external-api,bill-import,mail}.ts
├── middleware/auth.ts JWT + permissions
└── utils/{numbering,activityLogger,email}.ts
mcp-server/ local MCP server exposing the external API to AI agents
openapi.yaml OpenAPI 3.1 contract for /api/external
```

The `crud.ts` router implements the Supabase-style query surface (select with
relations, filters like `col.eq.value`, order, limit) over SQLite. Table and
**column names are validated** against real columns (`db/columns.ts`) before
being interpolated — never interpolate a caller-supplied identifier without
validating it.

## Security model (do not regress)

- Auth is JWT (`middleware/auth.ts`); `authMiddleware` re-checks the user is
active on every request. Passwords are bcrypt.
- Onboarding is **invite-based**: `POST /auth/users` (requires `team:write`)
creates a user with an invite token; `POST /auth/accept-invite` sets the
password. Login never auto-creates passwords.
- The external API (`/api/external`, `routes/external-api.ts`) authenticates
with hashed API keys and is gated by the key's **scopes** AND the owning
user's role. Keys are created at `/api-keys`.
- In production the server refuses to boot with a placeholder `JWT_SECRET`.

## Numbering

Invoice/job numbers come from atomic counters in `company_settings`, allocated
via `server/utils/numbering.ts` (`allocateInvoiceNumber` / `allocateJobNumber`)
inside a transaction. The frontend reserves a number at save time via
`POST /api/functions/allocate-invoice-number`. Do NOT generate numbers with
`COUNT(*)` or a client-side read-modify-write — both reuse/collide.

## Project structure (frontend)

```
src/
├── components/ui/ shadcn/ui — DO NOT modify (add via `npx shadcn@latest add`)
├── components/ feature components (PascalCase)
├── pages/ route pages (PascalCase), lazy-loaded in App.tsx
├── hooks/ custom hooks (useXxx.ts)
├── contexts/ React contexts (Auth, Permission, Branding)
├── integrations/api/ the API client shim
└── lib/utils.ts cn(), uuid
```

## Conventions

- Import order: React → external libs → `@/components/ui/*` → `@/components/*`
→ `@/hooks/*` → `@/contexts/*` → `@/integrations/*` → `@/lib/*` → types.
Always use the `@/` alias.
- Naming: Pages/Components PascalCase; ui components lowercase-hyphen; hooks
`useX`; handlers `handleX`; types PascalCase.
- Styling: Tailwind only; use `cn()` for conditional classes. No inline styles.
- User-facing errors go through `useToast` →
`toast({ title, description, variant: 'destructive' })`.
- TypeScript strictness is relaxed on the frontend; the **server project
typechecks cleanly** (`npm run typecheck`) — keep it that way.

## Testing

Server integration tests live in `server/__tests__/` (Vitest + supertest over a
temp SQLite). They assert the security and numbering invariants — run
`npm run test:run` before committing server changes, and add a test when you fix
a backend bug. CI (`.github/workflows/ci.yml`) gates on typecheck + tests +
build + docker build.

## Gotchas

- better-sqlite3 is **synchronous** and built with `SQLITE_DQS=0`: use
single-quoted SQL string literals (`datetime('now')`, not `"now"`), or the
statement throws.
- The DB is a singleton keyed off `DATABASE_PATH`; tests set env vars before
dynamically importing the app.
- `server/uploads/` and `data/` are gitignored; storage paths are contained to
their bucket dir (no traversal).

## Environment

```bash
VITE_API_URL=http://localhost:3001/api
API_PORT=3001
DATABASE_PATH=./data/app.db
JWT_SECRET=change-me # required; must be non-placeholder in production
CORS_ORIGIN=http://localhost:8080
# SMTP_* optional, for invoice/invite email
```
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,17 @@ Programmatic access for integrations and AI tools:
- **Vendor Spend** - Spending by vendor
- **GST Summary** - Tax summary

### Backups

The server automatically backs up the SQLite database on startup and on a recurring interval, using better-sqlite3's online backup API (safe to run while the app is live):

- Backups are written to `BACKUP_DIR` (default `data/backups`, already gitignored) as `app-YYYYMMDD-HHmmss.db`
- Runs every `BACKUP_INTERVAL_HOURS` (default `24`); set to `0` to disable
- Only the newest `BACKUP_RETENTION` backups are kept (default `7`); older ones are deleted automatically
- A backup failure is logged but never crashes the server

Configure these in `.env.local` if you want different values - see `.env.example`.

## Notes

- The local database file lives at `data/app.db` and is gitignored.
Expand Down
9 changes: 9 additions & 0 deletions api-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,3 +431,12 @@ curl -s -X POST "$BASE_URL/clients" \
-H "Content-Type: application/json" \
-d '{"name": "Test Client", "is_active": 1}' | jq .
```

---

## API reference (OpenAPI)

The full machine-readable contract for this API — every resource, path, request/response
schema, auth scheme, and error shape — is defined in [`openapi.yaml`](./openapi.yaml) at the
repo root (OpenAPI 3.1). Load it into any OpenAPI-aware tool (Redoc, Swagger UI, Postman,
an LLM/codegen client, etc.) to browse or generate clients from it.
Binary file removed bun.lockb
Binary file not shown.
4 changes: 4 additions & 0 deletions mcp-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
dist/
*.log
.env
93 changes: 93 additions & 0 deletions mcp-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# clientflow-mcp

A local [MCP](https://modelcontextprotocol.io) server that lets an AI agent (Claude Desktop,
etc.) drive the **Client Flow** app through its external REST API (`/api/external`) using a
scoped API key. It exposes generic CRUD tools over every resource the external API supports.

> **Security:** this server adds no privileges. Every call is gated server-side by the API
> key's **scopes** and the owning user's **role**. Create narrowly-scoped keys at `/api-keys`
> and treat the key like a password.

## Requirements

- Node.js 18+
- A running Client Flow server
- An API key created in the app at **/api-keys**

## Build

```sh
cd mcp-server
npm install
npm run build
```

## Configuration

Two environment variables:

| Variable | Example | Notes |
| -------------------- | ----------------------------- | -------------------------------------------------- |
| `CLIENTFLOW_API_URL` | `http://localhost:3001/api` | Base URL of the app (with or without trailing `/api`). |
| `CLIENTFLOW_API_KEY` | `sk_live_...` | An API key from `/api-keys`. |

The server refuses to start if either is missing (and never prints the key).

## Add to Claude Desktop

Add this to your `claude_desktop_config.json` (adjust the path to `dist/index.js`):

```json
{
"mcpServers": {
"clientflow": {
"command": "node",
"args": ["/absolute/path/to/mcp-server/dist/index.js"],
"env": {
"CLIENTFLOW_API_URL": "http://localhost:3001/api",
"CLIENTFLOW_API_KEY": "sk_live_replace_me"
}
}
}
}
```

## Tools

| Tool | Purpose | Hints |
| ---------------- | --------------------------------------------------- | ------------------------------ |
| `list_resources` | List available resources and their permission groups | read-only |
| `list_records` | List records of a resource (max 100, no server filtering; use `limit`) | read-only |
| `get_record` | Fetch one record by id | read-only |
| `create_record` | Create a record from column/value pairs | write |
| `update_record` | Update fields on a record by id | write, idempotent |
| `delete_record` | Delete a record by id | write, **destructive** |

Resources (gated by the permission group in parentheses): clients, jobs, invoices, payments,
assets, issues, vendors, items (inventory), expenses, timesheets (jobs), bank-accounts (banking),
bank-transactions (banking), profiles (team), kb-articles/kb-attachments/kb-article-issues (kb),
locations, location-contacts (locations), bill-import-sessions (purchases).

The full REST contract is documented in [`../openapi.yaml`](../openapi.yaml).

## Example prompts

- "List all active clients."
- "How many invoices are still in `draft` status?" (list invoices, filter client-side)
- "Create a new client named Acme Pty Ltd with email accounts@acme.example."
- "Create a draft invoice for client `<id>` with a total of 1500."
- "Mark invoice `<id>` as sent."

## Test

```sh
npm test # builds, then runs the api-client unit tests with node --test
```

The tests cover URL normalization and the actionable error-message mapping (no network calls).

## Notes / limitations

- The list endpoint returns at most 100 rows and supports no server-side filtering or
pagination — retrieve the set and filter client-side.
- `get_record` returns `null` (not an error) when no record matches the id.
Loading
Loading