The soft reset to 77073ba inadvertently staged deletions of all phase 2
and 3 artifacts. This commit restores them from their source commits so
subsequent task commits build on the complete prior-phase foundation:
- components/mobile/{BottomNav,HeaderBar,KpiCardMobile,MoreDrawer,NeedsAttentionStrip,WorkerStatusRow}
- app/mobile/layout.tsx, dashboard/page.tsx, analyzer/page.tsx
- app/api/mobile/dashboard/route.ts
- All .planning/** files from phases 01-04
- CLAUDE.md, app/layout.tsx, app/styles/brand.css, public/manifest.json
263 lines
9.3 KiB
Markdown
263 lines
9.3 KiB
Markdown
# Coding Conventions
|
|
|
|
**Analysis Date:** 2026-05-03
|
|
|
|
## Naming Patterns
|
|
|
|
**Files:**
|
|
- kebab-case for all files and directories (e.g., `postgres-client.ts`, `invite-user-form.tsx`, `entity-sync.ts`)
|
|
- Nested directories use kebab-case (e.g., `lib/services/analyzer/`, `components/admin/users/`)
|
|
|
|
**Functions:**
|
|
- camelCase for all functions (e.g., `getAutotaskClient()`, `transformCompany()`, `extractExplicitFromText()`)
|
|
- Factory functions prefixed with `get` (e.g., `getAutotaskClient()`, `getDattoRmmClient()`)
|
|
- Helper functions suffixed with descriptive intent (e.g., `relTime()`, `deriveSigningKey()`)
|
|
- Private/internal functions prefixed with underscore: `_INTERNALS` objects expose internals for test access
|
|
|
|
**Variables:**
|
|
- camelCase for all variables (e.g., `isLoading`, `setData`, `ticketNumber`)
|
|
- Constants in UPPER_SNAKE_CASE (e.g., `MAX_EXPLICIT_LINKS`, `TICKET_NUMBER_REGEX`)
|
|
- Database column names are always snake_case (e.g., `company_name`, `is_active`, `created_at`)
|
|
|
|
**Types:**
|
|
- PascalCase for all type names (e.g., `ClassificationRule`, `WorkflowExecution`, `TicketData`)
|
|
- Single-letter generics are acceptable (e.g., `queryEntity<T>()`)
|
|
- Union types as literal strings (e.g., `type RuleType = 'branch_routing' | 'ticket_type'`)
|
|
|
|
**Components:**
|
|
- PascalCase exported from kebab-case files (e.g., export `InviteUserForm` from `invite-user-form.tsx`)
|
|
- Page components: `export default function ComponentName()` at end of file
|
|
- Form components: follow `[Resource]Form` naming (e.g., `InviteUserForm`, `SignInForm`, `UserForm`)
|
|
|
|
## Code Style
|
|
|
|
**Formatting:**
|
|
- TypeScript strict mode enabled (`"strict": true` in `tsconfig.json`)
|
|
- No explicit formatter config (ESLint handles style)
|
|
- Indentation: 2 spaces (inferred from existing code)
|
|
|
|
**Linting:**
|
|
- ESLint: `eslint.config.mjs` with Next.js config (`eslint-config-next/core-web-vitals`, `eslint-config-next/typescript`)
|
|
- No additional custom rules beyond Next.js defaults
|
|
- Type checking: `npx tsc --noEmit --pretty` (must pass before commit)
|
|
|
|
## Import Organization
|
|
|
|
**Order:**
|
|
1. Node.js built-ins (e.g., `fs`, `path`)
|
|
2. Third-party packages (e.g., `next/server`, `zod`, `vitest`)
|
|
3. Type imports (e.g., `import type { ... } from '...'`)
|
|
4. Local imports from `@/*` (using path alias)
|
|
5. Local imports from `./` (relative, less common)
|
|
|
|
**Path Aliases:**
|
|
- Configured as `"@/*": ["./*"]` in `tsconfig.json`
|
|
- Use `@/lib/...`, `@/components/...`, `@/app/...` always
|
|
- Never use relative paths like `../../../` for imports
|
|
|
|
**Example import block** (from `/opt/stacks/pulse/components/admin/users/invite-user-form.tsx`):
|
|
```typescript
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { z } from "zod";
|
|
import { Loader2, Send } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormDescription,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/components/ui/form";
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
**Pattern:**
|
|
- All async functions use `try/catch` blocks
|
|
- API routes: catch errors and return `NextResponse.json({ error, message }, { status })`
|
|
- Standard status codes: `500` for runtime errors, `503` for missing/bad config, `401`/`403` from auth helpers
|
|
- Error messages: include `error instanceof Error ? error.message : 'fallback message'`
|
|
|
|
**Example from `/opt/stacks/pulse/app/api/companies/route.ts`:**
|
|
```typescript
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const result = await postgresClient.query('SELECT * FROM companies ...');
|
|
return NextResponse.json({ companies: result.rows.map(transformCompany) });
|
|
} catch (error) {
|
|
console.error('Error fetching companies from database:', error);
|
|
return NextResponse.json(
|
|
{ error: error instanceof Error ? error.message : 'Failed to fetch companies' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Client-side:** Use try/catch with `.json()` nulling:
|
|
```typescript
|
|
const [overviewRes, trendsRes] = await Promise.all([
|
|
fetch('/api/dashboard/overview', { cache: 'no-store' }),
|
|
fetch('/api/dashboard/trends', { cache: 'no-store' }),
|
|
]);
|
|
if (!overviewRes.ok) {
|
|
const body = (await overviewRes.json().catch(() => ({}))) as { error?: string };
|
|
throw new Error(body.error ?? `HTTP ${overviewRes.status}`);
|
|
}
|
|
```
|
|
|
|
## Logging
|
|
|
|
**Framework:** Plain `console` (no structured logging library)
|
|
|
|
**Patterns:**
|
|
- `console.error()` for exceptions caught in try/catch (usually in API routes and services)
|
|
- Include context: `console.error('Failed to fetch companies:', error)`
|
|
- No `console.log()` for debugging (remove before commit per linter checks)
|
|
|
|
## Comments
|
|
|
|
**When to Comment:**
|
|
- Explain *why*, not what (code shows the what)
|
|
- Non-obvious logic or business rules
|
|
- Performance-critical sections
|
|
- Workarounds or hacks (mark with `// HACK:` or `// NOTE:`)
|
|
|
|
**JSDoc/TSDoc:**
|
|
- Used sparingly on complex functions
|
|
- Example from `lib/services/analyzer/link-discovery.ts`:
|
|
```typescript
|
|
/**
|
|
* Marks refs in a RELATED TICKETS: block as high confidence
|
|
*/
|
|
export function extractExplicitFromText(text: string, source: string) { ... }
|
|
```
|
|
- Not required for simple getters/setters or obvious functions
|
|
|
|
## Function Design
|
|
|
|
**Size:**
|
|
- Keep functions focused: one responsibility per function
|
|
- Aim for <50 lines for page components, <30 for utilities
|
|
- Complex operations broken into smaller helpers
|
|
|
|
**Parameters:**
|
|
- Prefer object parameters for >3 arguments
|
|
- Don't use `any` — use specific types
|
|
- Use `Partial<T>` for optional object shapes
|
|
|
|
**Return Values:**
|
|
- Async functions always return `Promise<T>` explicitly
|
|
- Prefer `null` over `undefined` for missing values
|
|
- Use discriminated unions for success/error returns in critical paths (see analyzer pipeline)
|
|
|
|
## Module Design
|
|
|
|
**Exports:**
|
|
- Prefer `export` at declaration point rather than grouped re-exports
|
|
- One main export per file (exception: barrel files in `components/ui/`)
|
|
- Internal utilities prefixed with underscore: `_INTERNALS` object for test access
|
|
|
|
**Barrel Files:**
|
|
- `components/ui/index.ts` exports all shadcn primitives
|
|
- `lib/types/` has domain-specific barrel files (e.g., `lib/types/workflow.ts`, `lib/types/autotask.ts`)
|
|
- Avoid deep nesting — import from files, not directories unless barrel exists
|
|
|
|
## Database Transformations
|
|
|
|
**Pattern:** All columns are `snake_case` in database. API responses transform to `camelCase`.
|
|
|
|
**Example from `/opt/stacks/pulse/app/api/companies/route.ts`:**
|
|
```typescript
|
|
function transformCompany(row: any) {
|
|
return {
|
|
id: Number(row.id),
|
|
companyName: row.company_name,
|
|
companyType: row.company_type,
|
|
isActive: row.is_active,
|
|
// ... all snake_case → camelCase
|
|
};
|
|
}
|
|
```
|
|
|
|
No ORM is used — all transforms are manual per handler.
|
|
|
|
## Shared Components & Libraries
|
|
|
|
**UI Components:**
|
|
- shadcn primitives live in `components/ui/`
|
|
- Feature-specific components in sibling directories (e.g., `components/dashboard/`, `components/admin/`)
|
|
- Icons: Always use `lucide-react` (e.g., `import { Loader2, Send } from 'lucide-react'`)
|
|
|
|
**Tables:**
|
|
- Use `@tanstack/react-table` via `components/admin/DataTable.tsx` wrapper
|
|
- Example: `<DataTable columns={columns} data={data} />`
|
|
|
|
**Modals:**
|
|
- Use `components/admin/DetailModal.tsx` for entity details
|
|
- Follows card + tabs pattern (formatted/raw)
|
|
|
|
**Navigation:**
|
|
- Use `components/navigation/app-navigation.tsx` (`NavigationMenu` from Radix)
|
|
- Dropdowns prefer `@radix-ui/react-dropdown-menu` over submenus
|
|
|
|
**Toasts:**
|
|
- Use `sonner` library: `import { toast } from 'sonner'`
|
|
- Patterns: `toast.success()`, `toast.error()`, `toast.info()`
|
|
|
|
**Forms:**
|
|
- Use `react-hook-form` + Zod for validation
|
|
- Only in admin/auth forms — NOT in every page
|
|
- Pattern: `useForm()` with `zodResolver()`, then `<Form>` wrapper from shadcn
|
|
|
|
**Charts:**
|
|
- Use `recharts` for data visualization (e.g., `<BarChart>`, `<LineChart>`)
|
|
|
|
## What NOT to Introduce
|
|
|
|
**Forbidden:**
|
|
- No ORMs (Prisma, TypeORM, etc.) — use `postgresClient` singleton and manual transforms
|
|
- No server actions (`'use server'`) — use API routes called via `fetch()` from clients
|
|
- No additional state libraries (SWR, react-query, TanStack Query) — match local `useState` + `fetch` pattern
|
|
- No change to authentication (Better Auth is final)
|
|
- No editing of committed migrations — always create new numbered ones
|
|
|
|
**Rationale:**
|
|
- Keeps codebase lean and explicit
|
|
- Reduces abstraction overhead
|
|
- Makes data flow (DB → API → Client) visible
|
|
|
|
## Migrations
|
|
|
|
**Creating a new migration:**
|
|
1. Number it sequentially: if last is `041_create_engagement_tables.sql`, next is `042_*.sql`
|
|
2. Use `IF NOT EXISTS` for CREATE statements
|
|
3. Use `ON CONFLICT DO NOTHING` for seed data INSERT
|
|
4. Never drop columns or tables without explicit guard
|
|
5. Include audit columns: `created_at`, `updated_at`, `synced_at`, `is_deleted`, `deleted_at`
|
|
|
|
**Example structure:**
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS new_table (
|
|
id BIGINT PRIMARY KEY,
|
|
name VARCHAR(255),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
INSERT INTO new_table (id, name) VALUES (1, 'Example')
|
|
ON CONFLICT DO NOTHING;
|
|
```
|
|
|
|
**Note:** Migrations are applied in alphabetical order. Existing duplicates (002, 004, 009) exist; respect that order.
|
|
|
|
---
|
|
|
|
*Convention analysis: 2026-05-03*
|