* chore: add comprehensive .gitignore * ci: update CI workflow for GitHub Flow - Change triggers from develop to main (PRs to main + pushes to main) - Add concurrency controls to cancel stale runs - Update docker/build-push-action to v6 - Add descriptive job names for branch protection status checks - Update screenshot refresh and docs sync to trigger on main pushes * ci: update docker-publish for GitHub Flow - Remove develop branch trigger (no more dev tag) - Keep v* tag trigger for releases - Update docker/build-push-action to v6 * docs: add community and governance files - CONTRIBUTING.md with dev setup and PR guidelines - SECURITY.md with vulnerability reporting policy - CODE_OF_CONDUCT.md (Contributor Covenant v2.1 reference) - PR template with conventional commits checklist - Issue templates for bug reports and feature requests - CODEOWNERS defaulting to @AnsoCode - Dependabot config for npm (root, backend, frontend) and GitHub Actions * docs: add README with badges, quick start, and contributing section * chore: add LICENSE placeholder and open license decision issue (#100) * docs: update CLAUDE.md for GitHub Flow branching model - Replace develop-based Git Flow with GitHub Flow (main only) - All branches now created off main, PRs target main - Simplify release checklist (no develop-to-main merge step) - Update testing strategy to reference Vitest and Playwright - Fix docs.json reference (was mint.json) * chore: track CLAUDE.md in version control Remove CLAUDE.md from .gitignore so project workflow instructions are versioned alongside the code they govern. * docs: add MANUAL_STEPS.md for GitHub settings that require UI configuration
17 KiB
CLAUDE.md
Every session: Read this file fully before starting any task. Every commit: Before committing, run through the checklist below.
Project Overview
Sencho is a self-hosted Docker Compose management dashboard - a React + Express full-stack app that provides a GUI for managing Docker Compose stacks. Architecture Model: It uses a "Distributed API" model. It manages local stacks directly. To manage remote nodes, it acts as a transparent HTTP/WebSocket proxy, routing requests to other autonomous Sencho instances running on those remote servers via long-lived JWT Bearer tokens.
Development Commands
Backend
cd backend
npm install
npm run dev # nodemon + ts-node, watches src/
npm run build # tsc → dist/
npm start # node dist/index.js (production)
Frontend
cd frontend
npm install
npm run dev # Vite dev server (proxies /api → localhost:3000)
npm run build # tsc -b && vite build
npm run lint # ESLint 9 flat config
npm run preview # preview production build
Full Docker build
docker build -t sencho . # multi-stage: frontend build → backend build → runtime
Versioning & Release Flow
Sencho uses Semantic Versioning (MAJOR.MINOR.PATCH) driven by Conventional Commits and automated by the release-please GitHub Action.
How it works (GitHub Flow)
- Create a short-lived feature branch off
main(e.g.,feat/my-feature,fix/my-bug). - Commit using conventional commit prefixes (see below).
- Open a PR into
main. CI runs automatically. - Once CI is green, merge the PR (squash-merge recommended for clean history).
- On every push to
main, therelease-pleaseworkflow automatically opens or updates a Release PR titledchore(main): release vX.Y.Z. This PR contains:- An updated
CHANGELOG.mdentry generated from commit messages. - A
package.jsonversion bump.
- An updated
- When you're ready to publish, merge the Release PR. This triggers:
- A
vX.Y.Zgit tag created by release-please. - The
docker-publish.ymlworkflow fires on the tag → builds and pusheslatest+X.Y.Zto Docker Hub.
- A
You never create tags manually. Merging the Release PR is the only release action required. There is no
developbranch.mainis the only long-lived branch.
Conventional Commit format
<type>(optional scope): <short description>
# Examples
fix(auth): prevent login loop on remote node 401
feat(app-store): add category filter pill bar
feat!: replace SSH with Distributed API proxy ← BREAKING CHANGE
docs(quickstart): update Docker run command
chore(deps): bump express to 4.19.2
ci: add arm64 platform to docker-publish
Version bump rules
| Commit type | Example | Version bump |
|---|---|---|
fix: / perf: / revert: |
Bug fixes, perf improvements | PATCH — 0.1.0 → 0.1.1 |
feat: |
New user-facing feature | MINOR — 0.1.x → 0.2.0 |
feat!: / BREAKING CHANGE: footer |
Breaking API or UX change | MAJOR — 0.x.y → 1.0.0 |
docs: / chore: / ci: / test: / refactor: |
Infrastructure, no user impact | No bump (hidden in CHANGELOG) |
While the version is
0.x,feat:bumps the minor digit (0.1 → 0.2), not the patch.fix:still bumps patch (0.1.0 → 0.1.1). Afeat!:will bump to1.0.0.
Enriching the auto-generated Release PR
release-please generates CHANGELOG entries from commit subject lines. If a change deserves more context (e.g. a security fix or complex feature), edit the Release PR description and CHANGELOG entry directly before merging — release-please preserves manual edits on subsequent updates.
Architecture & Request Flow
The Distributed API Proxy Flow
- Frontend: Makes API calls via
apiFetch(frontend/src/lib/api.ts), which injects thex-node-idheader (or?nodeId=query param for WebSockets) fromNodeContext. - Backend Gateway (
index.ts): ThenodeContextMiddlewareevaluates the node ID.- If Local: The request passes through standard auth and hits the local Express route handlers.
- If Remote: The request hits
http-proxy-middleware. The backend strips thex-node-idheader, injectsAuthorization: Bearer <api_token>, and seamlessly proxies the entire HTTP or WebSocket request to the remote Sencho instance'sapi_url.
Backend Services (backend/src/services/)
All core services execute locally only. (Remote execution is handled by proxying the request to the remote Sencho instance, which then executes locally on its own machine).
DockerController- Dockerode wrapper for local containers, images, volumes, networks, stats, exec.ComposeService- Spawnsdocker composeCLI child processes locally.FileSystemService- Simplefs.promiseswrapper. All reads/writes happen against the localCOMPOSE_DIR.DatabaseService- SQLite viabetter-sqlite3. Stores alerts, settings, metrics, and node routing configs at/app/data/sencho.db.NodeRegistry- Manages node configurations. Returns the local Docker socket for local ops, and returns{ api_url, api_token }targets for remote proxies.
Frontend Structure
App.tsx- Root router: setup flow → login →EditorLayout.context/AuthContext.tsx- JWT auth state; handles first-boot setup detection.context/NodeContext.tsx- Active node selection state.components/NodeManager.tsx- UI for adding nodes (requires Name, Sencho API URL, and API Token).lib/api.ts-apiFetchwrapper that handles headers and 401 redirects.
Authentication
- Browser-to-Backend: JWT stored in
httpOnlycookies. - Node-to-Node (Proxy): Long-lived JWT passed via
Authorization: Bearer <token>header. - Middleware:
authMiddlewareaccepts both cookies and Bearer tokens seamlessly.
🛑 AGENT DIRECTIVES (CRITICAL INSTRUCTIONS)
To prevent hallucinations, regressions, and context loss, you MUST adhere to the following rules:
1. Targeted Reading of index.ts (Avoid Full-File Context Waste)
- Before modifying
index.ts, do NOT read the entire ~1500-line file. Read only the relevant section: Lines 50-250 (Auth/Setup), Lines 300-500 (Stacks/Compose), Lines 550-750 (Docker Entities), Lines 800-1000 (System/Metrics), Lines 1050-1250 (Nodes/Proxy), Lines 1300+ (WebSockets/Errors). - For middleware ordering, check lines 100-300.
- Confirm exact placement by searching for the prefix.
-
Respect the Architecture:
- We do NOT use SSH, SFTP, or remote Docker TCP sockets. For remote file reading, you must use the HTTP Proxy model.
- Ensure
http-proxy-middlewarecatches remote requests before local route handlers.
-
Limit Repetition & Be Concise:
- Do not output massive blocks of unchanged code. Use precise edits, diffs, or surgical replacements.
- Do not over-explain your steps unless asked. State your intent, perform the file modifications, and report the result.
-
Strict GitHub Flow & Conventional Commits:
- All feature work and bug fixes must happen on a newly created branch off
main. - Every commit MUST use a conventional commit prefix (
fix:,feat:,docs:,chore:,ci:,refactor:,test:,perf:). This is what drives automated versioning — no prefix means no version bump and no CHANGELOG entry. - Update
CHANGELOG.mdunder## [Unreleased]before committing (the auto-generated entry from release-please will be terse; add context here for significant changes). - Stage, commit, push to
origin, and open a PR intomain. This is required to trigger automated CI/CD actions. - Do NOT manually create git tags or push version commits. Versioning is fully automated by
release-please— it fires on every push tomainand creates the Release PR automatically.
- All feature work and bug fixes must happen on a newly created branch off
-
Strict Security & API Standards:
- Default Deny: EVERY new endpoint in
index.tsunder/api/MUST be protected byauthMiddlewareunless explicitly designated as public. - SQL Injection Prevention: ALWAYS use
better-sqlite3parameterized statements. Do not manually sanitize or string-escape inputs. - No Secrets in Code: Never hardcode secrets. Rely on
DatabaseService.getInstance().getGlobalSettings().
- Default Deny: EVERY new endpoint in
-
UX & UI Engineering Constraints:
- Use only Tailwind CSS and existing
shadcn/uicomponents. - Handle loading states. Every frontend API call must be wrapped in
try/catch. - When using
sonnertoast, use this exact defensive pattern:toast.error(error?.message || error?.error || error?.data?.error || 'Something went wrong.'); - Do not assume
erroris a standard Error instance.
- Use only Tailwind CSS and existing
-
Backend Error Handling:
- Do Not Swallow Errors: Do not write empty
catchblocks. - Standardized Responses: If a backend operation fails, log the actual error to the console for debugging, but return a clean, standard JSON error to the frontend:
res.status(500).json({ error: "Clear message" });. - No Server Crashes: Ensure all asynchronous proxy streams (WebSockets/SSE) have robust error handling and
.on('error')listeners to prevent the Node event loop from crashing.
- Do Not Swallow Errors: Do not write empty
-
WebSocket Nuances & Middleware Bypasses:
- Express middlewares (
authMiddleware,nodeContextMiddleware,cookieParser) DO NOT RUN on WebSocket (server.on('upgrade')) connections. - You MUST manually parse cookies/headers and manually execute
jwt.verify()inside theupgradeevent handler. - Extract
nodeIdfrom URL query parameters (?nodeId=) for WebSockets, NOT from thex-node-idheader.
- Express middlewares (
-
Filesystem Security & Path Traversal:
- Perform strict directory traversal validation at the Express route level before any
FileSystemServicecall. - Use this exact pattern:
const safePath = path.resolve(COMPOSE_DIR, stackName, filename);if (!safePath.startsWith(path.resolve(COMPOSE_DIR))) { return res.status(400).json({ error: 'Invalid path' }); } - Reject inputs with
..or absolute paths.
- Perform strict directory traversal validation at the Express route level before any
-
React Dependency Traps (Infinite Loops):
- Be extremely cautious fixing
react-hooks/exhaustive-depswarnings in global context files likeNodeContext.tsx. - We intentionally use
useRef(e.g.,activeNodeRef) to hold current state insideuseCallbackfunctions. DO NOT blindly add state variables to dependency arrays to satisfy ESLint, as it will trigger infinite API-fetching loops.
- The Monolithic index.ts Constraint:
- The backend
index.tsfile is currently a monolith (~1500 lines). - Continue appending new routes into their logical groups (e.g., Auth, Stacks, System, Nodes). DO NOT attempt to proactively split or refactor this file into a multi-router architecture unless explicitly instructed by the user.
- Environment & File System Context:
FileSystemServicestrictly operates against theCOMPOSE_DIRenvironment variable.- DO NOT hardcode
/app/composeas an immutable path or implicitly assume it is the only path. It is merely the Docker default. Your code must dynamically readprocess.env.COMPOSE_DIR(or rely onFileSystemService.getInstance(nodeId).getBaseDir()) to respect each user's unique volume mounts or local OS paths.
- Mandatory Test Validation Before Completing Any Task:
- Validate behavior before considering a task done.
- Always kill existing services first — do not assume a running process has the latest code (it may have been started with
npm startornode dist/index.js, not nodemon):kill $(lsof -ti:3000) 2>/dev/null; kill $(lsof -ti:5173) 2>/dev/null - Then start fresh in the background using
npm run dev(nodemon) only:cd backend && npm run dev & cd frontend && npm run dev & - Wait for readiness before proceeding:
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/health # expect 200 curl -s -o /dev/null -w "%{http_code}" http://localhost:5173 # expect 200 - If Playwright MCP is available, use it to navigate and interact with the affected UI and confirm expected behavior.
- If Playwright is unavailable: explicitly state "Playwright validation unavailable; recommend manual testing: [describe exact steps]".
- Always kill both services when done with validation — never leave orphan processes:
kill $(lsof -ti:3000) 2>/dev/null; kill $(lsof -ti:5173) 2>/dev/null - Explicitly confirm in your response that services have been stopped after validation.
- Never open a PR without documented validation.
- TypeScript Strictness Rules
tsconfig.jsonhas"strict": true.- Write code that compiles without
anycasts or@ts-ignore. Prefer proper types overany. - If a library lacks types, import
@types/...or useunknown+ narrowing. - Never add casts to silence errors.
- Testing Strategy Reference
- Backend unit tests: Vitest (
cd backend && npm test). Add tests for new backend logic. - E2E tests: Playwright (
npm run test:e2e). Covers auth, stack management, and node management flows. - Manual validation: Use the Playwright MCP server for UI changes, or
curlfor backend-only changes. - For UI changes: use the Playwright MCP server to navigate to
http://localhost:5173, interact with the affected components, and confirm expected behavior. - For backend-only changes: validate via
curlagainstlocalhost:3000/api/...with a validAuthorization: Bearer <token>or session cookie.
- Environment Variables Reference
- Read vars from
process.envorDatabaseService. - Never hardcode. Key vars:
COMPOSE_DIR(base directory),JWT_SECRET(token signing),PORT(listen port),NODE_ENV. Check.env.examplefor the full list.
- Code Review Standards
- After completing any implementation, review the code for:
- Functions longer than 30 lines (likely doing too much)
- Logic duplicated more than twice (extract to utility)
- Any
anytype usage in TypeScript (replace with real types) - Components with more than 3 props that could be grouped into an object
- Missing error handling on async operations
- Run /simplify before presenting code to the user.
Documentation
Whenever you create, modify, or remove a user-facing feature, API endpoint, or configuration option, you MUST also update or create the relevant .mdx file inside the /docs folder.
- Follow the existing structure in
/docs/docs.jsonnavigation - Write docs from the perspective of the end user, not internal implementation
- Use Mintlify MDX components where useful (e.g.
<Card>,<CodeGroup>,<Note>) - Add the new/updated doc page to the
navigationarray inmint.jsonif it's a new file - Keep tone friendly and concise
Screenshots
When adding or updating a doc page for a user-facing feature:
- Follow the service lifecycle in Directive 13: kill any running processes on ports 3000/5173, restart with
npm run dev, wait for readiness, and kill again when done - Use the Playwright MCP server to navigate to the relevant page/feature in the app
- Take a screenshot and save it to
/docs/images/<feature-name>/<screenshot-name>.png - Use descriptive filenames (e.g.
dashboard-overview.png,settings-dark-mode.png) - For multi-step flows, highlight the relevant UI element by injecting a CSS border
before screenshotting:
element.style.border = '3px solid #0F172A' - Reference screenshots in MDX files using:
<img src="/images/<feature-name>/<screenshot-name>.png" alt="..." />or Mintlify's<Frame>component for a polished look:<Frame><img src="/images/..." alt="..." /></Frame> - Always take screenshots at 1280x800 desktop resolution for consistency
Pre-Commit Checklist
- Am I on a feature branch off
main? (never commit to main directly) - Does my commit message start with a conventional prefix? (
fix:,feat:,docs:,chore:,ci:,refactor:,test:,perf:) - Did I update CHANGELOG.md under
## [Unreleased]? (add context beyond what the commit subject captures) - Did I update or create the relevant
/docs/*.mdxfile? - Did I take/update screenshots via Playwright MCP if UI changed?
- Did I validate behavior via Playwright MCP (UI) or
curl(backend), and kill the dev servers afterwards? (Directive 13) - Is the PR going into
main?
Release Checklist (when shipping to users)
- Is
mainstable and CI green? - Wait for the
release-pleaseworkflow to open the Release PR (titledchore(main): release vX.Y.Z). - Review and optionally enrich the auto-generated CHANGELOG entry in the Release PR.
- Merge the Release PR → tag is created → Docker Hub
latest+ semver tags are published automatically.