nexus-5-frontend-2/CLAUDE.md
2026-01-26 11:28:04 -05:00

440 lines
13 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a **SvelteKit 5** frontend application for Nexus, built with:
- **SvelteKit** as the meta-framework
- **Houdini** for GraphQL client operations
- **Tailwind CSS v4** for styling
- **TypeScript** with strict mode enabled
- **Node adapter** for production deployment
### Relationship to nexus-5-frontend-1
This project (nexus-5-frontend-2) is a **streamlined team-focused application** that abstracts functionality from nexus-5-frontend-1 (the admin interface).
**Key Differences**:
- **nexus-5-frontend-1**: Admin-focused dashboard with comprehensive controls
- **nexus-5-frontend-2**: Team-focused app with simplified workflows for field/service operations
**Using nexus-5-frontend-1 as Reference**:
-**DO** reference nexus-5-frontend-1 for:
- Understanding business logic and data flow
- GraphQL query structures and data models
- UI component patterns and layouts
- Understanding feature requirements and scope
-**DO NOT** replicate from nexus-5-frontend-1:
- Dashboard patterns (this app has no dashboard)
- Admin-specific features
- Complex data loading patterns (this app uses simpler SvelteKit patterns)
- Overly complex UI components
**When to Reference**:
1. When understanding what data a feature needs (check the GraphQL queries)
2. When understanding business logic (how tasks, sessions, scopes work)
3. When you need UI inspiration for similar features
4. When clarifying requirements for features that exist in both apps
**Important**: Always ask the user if you're unclear whether a pattern from nexus-5-frontend-1 should be replicated or simplified for this team-focused app.
## Development Commands
```bash
# Start development server (runs on https://local.example.com)
npm run dev
# Build for production
npm run build
# Preview production build
npm run preview
# Type checking
npm run check
# Type checking in watch mode
npm run check:watch
# Format code
npm run format
# Lint code (runs prettier check + eslint)
npm run lint
```
## Docker Deployment
The application is containerized with Docker for production deployment.
### Docker Files
- **`Dockerfile`**: Multi-stage build (Node 22 Alpine)
- Stage 1: Install dependencies and build the SvelteKit app
- Stage 2: Production image with only built output and production dependencies
- **`docker-compose.yml`**: Simple deployment configuration
- **`.dockerignore`**: Excludes node_modules, .houdini, build artifacts, etc.
### Running with Docker
```bash
# Build and run with docker-compose
docker-compose up --build
# Run in detached mode
docker-compose up -d --build
# Stop the container
docker-compose down
```
### Configuration
The container exposes port 3000 internally, mapped to port 7000 on the host by default. Environment variables:
- `NODE_ENV=production`
- `PORT=3000`
- `HOST=0.0.0.0`
To change the external port, modify `docker-compose.yml`:
```yaml
ports:
- 'YOUR_PORT:3000'
```
## Architecture
### GraphQL Integration (Houdini)
This project uses **Houdini** for type-safe GraphQL operations:
- **Client Configuration**: `src/client.ts` - Houdini client connects to GraphQL endpoint at `http://192.168.100.174:5500/graphql/`
- **Schema**: `schema.graphql` at project root
- **Generated Code**: `.houdini/` directory (gitignored, auto-generated)
- **GraphQL Documents**: Write queries/mutations in `.gql` files or inline in `.svelte` components
- **Config**: `houdini.config.js` - Includes authentication headers for schema watching:
- `X-USER-ID`
- `X-USER-PROFILE-TYPE`
- `X-OATHKEEPER-SECRET`
- `X-DJANGO-PROFILE-ID`
Authentication headers are read from environment variables in `.env` file.
### Path Aliases
- `$houdini``.houdini/` - Access Houdini generated types and runtime
- Standard SvelteKit aliases (`$lib`, `$app`, etc.) are available
### Development Server
The dev server is configured to run on `local.example.com` with HTTPS enabled via `vite-plugin-mkcert`.
### TypeScript Configuration
- **Strict mode** enabled
- **Root dirs** include `.houdini/types` for Houdini type integration
- Module resolution set to `bundler`
### Vite Plugins
Configured in `vite.config.ts`:
1. `houdini` - GraphQL client integration
2. `tailwindcss` - Tailwind CSS v4
3. `sveltekit` - SvelteKit integration
4. `devtoolsJson` - Development tools
5. `mkcert` - Local HTTPS certificates
### Environment Variables
The `.env` file contains:
- API endpoints and keys for Calendar and Email services
- Authentication credentials for GraphQL schema introspection
- All Houdini auth headers (USER_ID, USER_PROFILE_TYPE, OATHKEEPER_SECRET, DJANGO_PROFILE_ID)
**Important**: These credentials are for development only.
### ESLint Configuration
Uses the new flat config format (`eslint.config.js`) with:
- TypeScript ESLint recommended rules
- Svelte plugin with recommended rules
- Prettier integration
- `no-undef` disabled for TypeScript files (as recommended by typescript-eslint)
## Working with GraphQL
### Schema Updates
When the GraphQL schema changes:
1. Update `schema.graphql` manually, or
2. Houdini's `watchSchema` will auto-update during development
3. Run `npm run prepare` or restart dev server to regenerate types
### Writing Queries
Queries can be written:
- In separate `.gql` files
- Inline in `.svelte` components using Houdini's Svelte integration
GraphQL documents are automatically discovered in:
- `**/*.gql` files
- `**/*.svelte` components
- `.houdini/graphql/documents.gql`
## Project Structure
```
src/
├── app.d.ts # SvelteKit app types
├── app.css # Global styles (Tailwind imports)
├── app.html # HTML template
├── client.ts # Houdini client setup
├── lib/
│ ├── assets/ # Static assets (favicon, etc.)
│ ├── components/ # Reusable Svelte components
│ │ ├── entity/ # Entity-related components (StatusBadge, TeamMemberList, etc.)
│ │ ├── layout/ # Layout components (Container, Navigation, etc.)
│ │ └── ui/ # Generic UI components
│ ├── graphql/ # GraphQL documents
│ │ ├── queries/ # Query .gql files organized by entity
│ │ └── mutations/ # Mutation .gql files
│ └── utils/ # Utility functions (date formatting, relay helpers, etc.)
└── routes/ # SvelteKit file-based routing
├── +layout.svelte # Root layout with navigation
├── +page.svelte # Home page
├── services/ # Services list and detail pages
├── projects/ # Projects list and detail pages
├── reports/ # Reports list and detail pages
├── messages/ # Messaging pages
└── sessions/ # Session pages (services/projects)
```
### Key Directories
- **`src/lib/components/entity/`**: Shared components for displaying entity data (services, projects). Includes `StatusBadge`, `TeamMemberList`, `AddressCard`, `IdSidebar`, scope cards, etc.
- **`src/lib/graphql/`**: GraphQL documents organized by entity type. Queries and mutations are in separate `.gql` files.
- **`src/lib/utils/`**: Helper functions like `formatDate()`, `fromGlobalId()` (Relay ID conversion), etc.
## Important Notes
- This project uses **Svelte 5** - use modern Svelte 5 syntax (runes, snippets, etc.)
- **Tailwind CSS v4** is configured - use the latest v4 features and syntax
- The project uses **adapter-node** for production builds
- HTTPS is enabled in development - the server runs on `local.example.com`
## Coding Standards
### Svelte 5 Specific Rules
#### SVG `fill` Attribute
**IMPORTANT**: Never use the `fill` attribute directly on SVG elements in Svelte 5. Always use the `style` attribute instead.
**Incorrect**:
```svelte
<svg fill="none" viewBox="0 0 24 24">
<svg fill="currentColor" viewBox="0 0 24 24">
```
**Correct**:
```svelte
<svg style="fill: none" viewBox="0 0 24 24">
<svg style="fill: currentColor" viewBox="0 0 24 24">
```
**Reason**: The `fill` attribute is considered obsolete in Svelte 5 and will trigger warnings. Use inline styles instead.
#### HTML Semantics - Button Content
**IMPORTANT**: Buttons can only contain phrasing content (inline elements). Never place block-level elements like `<div>` inside `<button>` elements.
**Incorrect**:
```svelte
<button>
<div class="flex items-center gap-3">
<svg>...</svg>
<span>Text</span>
</div>
</button>
```
**Correct**:
```svelte
<button>
<span class="flex items-center gap-3">
<svg>...</svg>
<span>Text</span>
</span>
</button>
```
**Valid phrasing content elements** (safe to use in buttons):
- `<span>`, `<strong>`, `<em>`, `<b>`, `<i>`, `<small>`, `<mark>`
- `<svg>`, `<img>`, `<abbr>`, `<code>`, `<kbd>`, `<sup>`, `<sub>`
**Invalid block elements** (never use in buttons):
- `<div>`, `<p>`, `<h1-h6>`, `<ul>`, `<ol>`, `<section>`, `<article>`
**Reason**: HTML5 semantic rules require button elements to only contain phrasing content. Block elements will trigger validation warnings and may cause accessibility issues.
### TypeScript Standards
#### Use Houdini Generated Types
**IMPORTANT**: Never use `any` or `unknown` when Houdini provides generated types.
**Incorrect**:
```typescript
let scopeData = $state<{ scope?: any } | null>(null);
tasks.filter((t: any) => ...)
```
**Correct**:
```typescript
import type { GetScope$result } from '$houdini';
let scopeData = $state<GetScope$result | null>(null);
tasks.filter((t) => ...)
```
**Available Houdini types**:
- Import result types: `GetQueryName$result`
- Import input types: `GetQueryName$input`
- Types are automatically generated in `.houdini/artifacts/`
**Reason**: Houdini generates complete TypeScript types from GraphQL schema. Using these types provides full type safety and autocomplete.
## UI Patterns
### Responsive Design
The app targets mobile devices (360px minimum width) as well as desktop. Common patterns:
**Mobile Cards / Desktop Table Pattern:**
For data-heavy pages like reports, use separate layouts:
```svelte
<!-- Mobile cards: visible below md breakpoint -->
<div class="space-y-4 md:hidden">
{#each items as item}
<a href="/item/{item.id}" class="block rounded-lg border p-4">
<!-- Card content -->
</a>
{/each}
</div>
<!-- Desktop table: hidden below md breakpoint -->
<div class="hidden md:block">
<table class="w-full">
<!-- Table content -->
</table>
</div>
```
**Clickable Table Rows:**
Make entire rows clickable by wrapping each cell's content in anchor tags:
```svelte
<tr class="group cursor-pointer transition-colors hover:bg-[rgb(var(--bg-tertiary))]">
<td class="px-4 py-3">
<a href="/item/{id}" class="block text-primary-500 group-hover:underline">
{name}
</a>
</td>
<td class="px-4 py-3">
<a href="/item/{id}" class="block">
{otherValue}
</a>
</td>
</tr>
```
## Data Patterns
### Pagination Pattern (Services & Projects)
The services (`/services`) and projects (`/projects`) pages use a hybrid server/client pagination pattern:
**How it works:**
1. **Server Load** (`+page.ts`):
- Fetches first 20 items per tab (scheduled, in_progress, completed)
- Applies month/year filter from URL params (`?month=X&year=Y`)
- Passes `teamProfileId`, `dateFilter`, and `pageSize` to client for subsequent fetches
2. **Client State** (`+page.svelte`):
- Uses `$state` to accumulate items (initialized from server data via `$effect`)
- Tracks cursor and `hasMore` state per tab
- "Load More" button triggers client-side fetch using Houdini store classes
3. **Tab-specific behavior:**
- **Scheduled**: `SCHEDULED` + `CANCELLED` statuses, ascending date order
- **In Progress**: `IN_PROGRESS` status only, no pagination (typically few items)
- **Completed**: `COMPLETED` status only, descending date order
**Client-side load more pattern:**
```typescript
import { GetServicesByTeamMemberStore } from '$houdini';
import { ServiceChoices, DateOrdering } from '$houdini';
async function loadMoreScheduled() {
if (!scheduledCursor || loadingMoreScheduled) return;
loadingMoreScheduled = true;
try {
const store = new GetServicesByTeamMemberStore();
const result = await store.fetch({
variables: {
teamProfileId: data.teamProfileId,
first: data.pageSize,
after: scheduledCursor,
filters: {
date: data.dateFilter,
status: { inList: [ServiceChoices.SCHEDULED, ServiceChoices.CANCELLED] }
},
ordering: DateOrdering.ASC
}
});
if (result.data?.getServicesByTeamMember) {
const connection = result.data.getServicesByTeamMember;
scheduledItems = [...scheduledItems, ...connection.edges.map((e) => e.node)];
scheduledCursor = connection.pageInfo.endCursor;
scheduledHasMore = connection.pageInfo.hasNextPage;
}
} finally {
loadingMoreScheduled = false;
}
}
```
**Key points:**
- Month/year filter determines the data set; pagination loads more within that set
- Tab counts show "20+" when more pages exist
- No URL navigation on "Load More" - results append in place