13 KiB
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:
- When understanding what data a feature needs (check the GraphQL queries)
- When understanding business logic (how tasks, sessions, scopes work)
- When you need UI inspiration for similar features
- 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
# 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
# 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=productionPORT=3000HOST=0.0.0.0
To change the external port, modify docker-compose.yml:
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 athttp://192.168.100.174:5500/graphql/ - Schema:
schema.graphqlat project root - Generated Code:
.houdini/directory (gitignored, auto-generated) - GraphQL Documents: Write queries/mutations in
.gqlfiles or inline in.sveltecomponents - Config:
houdini.config.js- Includes authentication headers for schema watching:X-USER-IDX-USER-PROFILE-TYPEX-OATHKEEPER-SECRETX-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/typesfor Houdini type integration - Module resolution set to
bundler
Vite Plugins
Configured in vite.config.ts:
houdini- GraphQL client integrationtailwindcss- Tailwind CSS v4sveltekit- SvelteKit integrationdevtoolsJson- Development toolsmkcert- 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-undefdisabled for TypeScript files (as recommended by typescript-eslint)
Working with GraphQL
Schema Updates
When the GraphQL schema changes:
- Update
schema.graphqlmanually, or - Houdini's
watchSchemawill auto-update during development - Run
npm run prepareor restart dev server to regenerate types
Writing Queries
Queries can be written:
- In separate
.gqlfiles - Inline in
.sveltecomponents using Houdini's Svelte integration
GraphQL documents are automatically discovered in:
**/*.gqlfiles**/*.sveltecomponents.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). IncludesStatusBadge,TeamMemberList,AddressCard,IdSidebar, scope cards, etc.src/lib/graphql/: GraphQL documents organized by entity type. Queries and mutations are in separate.gqlfiles.src/lib/utils/: Helper functions likeformatDate(),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:
<svg fill="none" viewBox="0 0 24 24">
<svg fill="currentColor" viewBox="0 0 24 24">
✅ Correct:
<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:
<button>
<div class="flex items-center gap-3">
<svg>...</svg>
<span>Text</span>
</div>
</button>
✅ Correct:
<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:
let scopeData = $state<{ scope?: any } | null>(null);
tasks.filter((t: any) => ...)
✅ Correct:
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:
<!-- 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:
<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:
-
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, andpageSizeto client for subsequent fetches
-
Client State (
+page.svelte):- Uses
$stateto accumulate items (initialized from server data via$effect) - Tracks cursor and
hasMorestate per tab - "Load More" button triggers client-side fetch using Houdini store classes
- Uses
-
Tab-specific behavior:
- Scheduled:
SCHEDULED+CANCELLEDstatuses, ascending date order - In Progress:
IN_PROGRESSstatus only, no pagination (typically few items) - Completed:
COMPLETEDstatus only, descending date order
- Scheduled:
Client-side load more pattern:
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