chore(01-01): scaffold monorepo, Docker Compose stack, and Vitest harness
- pnpm workspace with apps/api (Hono/Drizzle) and apps/pwa (Vite/React 19) - Pinned versions per RESEARCH: hono@4.12.23, drizzle-orm@0.45.2, mysql2@3.22.4, tsdav@2.2.2, ical.js@2.2.1, zod@^3.25.0, node-cron@^4.2.1 - docker-compose.yml with mariadb:11 healthcheck, api depends_on service_healthy, redis stub - docker-compose.dev.yml overrides for local dev (bind mounts, exposed ports) - .env.example lists all env vars (DB_*, OIDC_*, APP_PASSWORD_ENCRYPTION_KEY) - .gitignore excludes .env (never commit secrets) - apps/api/vitest.config.ts with environment: node - Wave 0 test stubs: health, auth/user, broker/crypto, broker/sync, broker/poller
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
# Database
|
||||||
|
DB_HOST=mariadb
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USER=familysync
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_NAME=familysync
|
||||||
|
DB_ROOT_PASSWORD=
|
||||||
|
|
||||||
|
# OIDC (Authelia) — fill in after registering the client
|
||||||
|
OIDC_AUTH_SECRET=
|
||||||
|
OIDC_ISSUER=
|
||||||
|
OIDC_CLIENT_ID=familysync
|
||||||
|
OIDC_CLIENT_SECRET=
|
||||||
|
OIDC_REDIRECT_URI=https://familysync.yourdomain.com/callback
|
||||||
|
OIDC_AUTH_EXTERNAL_URL=https://familysync.yourdomain.com
|
||||||
|
|
||||||
|
# CalDAV broker encryption key — generate with:
|
||||||
|
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||||
|
APP_PASSWORD_ENCRYPTION_KEY=
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
.dist/
|
||||||
|
|
||||||
|
# Environment — NEVER commit .env (secrets at rest: DB passwords, OIDC secrets, encryption key)
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# TypeScript
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Drizzle migrations (generated — not secrets, but keep clean)
|
||||||
|
# apps/api/src/db/migrations/
|
||||||
|
|
||||||
|
# Test coverage
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
FROM node:22-alpine AS base
|
||||||
|
WORKDIR /app
|
||||||
|
RUN corepack enable pnpm
|
||||||
|
|
||||||
|
FROM base AS deps
|
||||||
|
COPY package.json pnpm-lock.yaml* ./
|
||||||
|
RUN pnpm install --frozen-lockfile --prod
|
||||||
|
|
||||||
|
FROM base AS builder
|
||||||
|
COPY package.json pnpm-lock.yaml* ./
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
COPY tsconfig.json ./
|
||||||
|
COPY src/ ./src/
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
FROM base AS production
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY --from=builder /app/dist ./dist
|
||||||
|
# PWA static assets served from ./public (built separately)
|
||||||
|
COPY apps/pwa/dist/ ./public/ 2>/dev/null || true
|
||||||
|
CMD ["node", "dist/index.js"]
|
||||||
|
|
||||||
|
FROM base AS dev
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json pnpm-lock.yaml* ./
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
COPY tsconfig.json ./
|
||||||
|
CMD ["pnpm", "dev"]
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "@familysync/api",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "node --watch dist/index.js",
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"db:push": "drizzle-kit push",
|
||||||
|
"db:generate": "drizzle-kit generate",
|
||||||
|
"db:migrate": "drizzle-kit migrate"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hono/node-server": "2.0.4",
|
||||||
|
"@hono/oidc-auth": "1.8.3",
|
||||||
|
"@hono/zod-validator": "0.8.0",
|
||||||
|
"drizzle-orm": "0.45.2",
|
||||||
|
"hono": "4.12.23",
|
||||||
|
"ical.js": "2.2.1",
|
||||||
|
"mysql2": "3.22.4",
|
||||||
|
"node-cron": "^4.2.1",
|
||||||
|
"tsdav": "2.2.2",
|
||||||
|
"zod": "^3.25.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"drizzle-kit": "0.31.10",
|
||||||
|
"typescript": "^5.5.0",
|
||||||
|
"vitest": "^4.1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Wave 0 stub — Auth: upsertUser color round-robin + identity stability
|
||||||
|
*
|
||||||
|
* These tests are RED stubs. Implementation lives in:
|
||||||
|
* apps/api/src/auth/user.ts (Plan 02)
|
||||||
|
*
|
||||||
|
* Tests will be filled GREEN in Plan 02 when upsertUser is implemented.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it } from 'vitest'
|
||||||
|
|
||||||
|
describe('upsertUser', () => {
|
||||||
|
it.todo('assigns a color from the palette on first login (Plan 02)')
|
||||||
|
|
||||||
|
it.todo('returns the same color on subsequent logins for the same oidc identity (Plan 02)')
|
||||||
|
|
||||||
|
it.todo('assigns distinct colors to two different users (Plan 02)')
|
||||||
|
|
||||||
|
it.todo('uses oidc_iss + oidc_sub as the composite identity key, not email (Plan 02)')
|
||||||
|
|
||||||
|
it.todo('returns the full user row including id, color, displayName (Plan 02)')
|
||||||
|
})
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Wave 0 stub — Broker: AES-GCM app-password encryption
|
||||||
|
*
|
||||||
|
* These tests are RED stubs. Implementation lives in:
|
||||||
|
* apps/api/src/broker/crypto.ts (Plan 03)
|
||||||
|
*
|
||||||
|
* Tests will be filled GREEN in Plan 03 when crypto helpers are implemented.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it } from 'vitest'
|
||||||
|
|
||||||
|
describe('encryptPassword / decryptPassword', () => {
|
||||||
|
it.todo('roundtrip: decrypt(encrypt(plaintext)) === plaintext (Plan 03)')
|
||||||
|
|
||||||
|
it.todo('different IVs produce different ciphertext for the same plaintext (Plan 03)')
|
||||||
|
|
||||||
|
it.todo('decrypting with a tampered authTag throws (Plan 03)')
|
||||||
|
|
||||||
|
it.todo('decrypting with a tampered ciphertext throws (Plan 03)')
|
||||||
|
|
||||||
|
it.todo('stored payload is valid JSON with iv, authTag, ciphertext fields (Plan 03)')
|
||||||
|
})
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Wave 0 stub — Broker: ctag polling + change detection
|
||||||
|
*
|
||||||
|
* These tests are RED stubs. Implementation lives in:
|
||||||
|
* apps/api/src/broker/poller.ts (Plan 03)
|
||||||
|
*
|
||||||
|
* Tests will be filled GREEN in Plan 03 when the poller is implemented.
|
||||||
|
*
|
||||||
|
* Key behavior (D-13): ctag unchanged → no DB write (skip sync entirely)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it } from 'vitest'
|
||||||
|
|
||||||
|
describe('broker poller', () => {
|
||||||
|
it.todo('skips syncCalendar when ctag is unchanged (Plan 03)')
|
||||||
|
|
||||||
|
it.todo('calls syncCalendar when ctag changes (Plan 03)')
|
||||||
|
|
||||||
|
it.todo('calls syncCalendar when ctag was null (first sync) (Plan 03)')
|
||||||
|
|
||||||
|
it.todo('handles decryptPassword failure gracefully without crashing the poller (Plan 03)')
|
||||||
|
|
||||||
|
it.todo('processes all member credentials in a poll cycle (Plan 03)')
|
||||||
|
})
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Wave 0 stub — Broker: syncCalendar event upsert + all-day handling
|
||||||
|
*
|
||||||
|
* These tests are RED stubs. Implementation lives in:
|
||||||
|
* apps/api/src/broker/sync.ts (Plan 03)
|
||||||
|
*
|
||||||
|
* Tests will be filled GREEN in Plan 03 when syncCalendar is implemented.
|
||||||
|
*
|
||||||
|
* Key behaviors to verify (D-13):
|
||||||
|
* - All-day events: dtstart_date (DATE) set, dtstart_utc NULL, allDay=true
|
||||||
|
* - Timed events: dtstart_utc (TIMESTAMP UTC) set, dtstart_date NULL, allDay=false
|
||||||
|
* - UID used as idempotency key: second sync of same UID is an upsert, not duplicate
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it } from 'vitest'
|
||||||
|
|
||||||
|
describe('syncCalendar', () => {
|
||||||
|
it.todo('stores all-day events with dtstart_date (DATE) and dtstart_utc=NULL (Plan 03)')
|
||||||
|
|
||||||
|
it.todo('stores timed events with dtstart_utc (TIMESTAMP) and dtstart_date=NULL (Plan 03)')
|
||||||
|
|
||||||
|
it.todo('sets allDay=true for all-day events, allDay=false for timed (Plan 03)')
|
||||||
|
|
||||||
|
it.todo('upserts on duplicate UID within the same calendar (Plan 03)')
|
||||||
|
|
||||||
|
it.todo('stores the raw VEVENT blob in rawVevent column (Plan 03)')
|
||||||
|
|
||||||
|
it.todo('updates the calendar ctag/syncToken after a successful sync (Plan 03)')
|
||||||
|
})
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Wave 0 stub — GET /health: 200 + real DB round-trip
|
||||||
|
*
|
||||||
|
* This test is a RED stub. Implementation lives in:
|
||||||
|
* apps/api/src/routes/health.ts (Task 2)
|
||||||
|
*
|
||||||
|
* The test will be filled GREEN in Task 2 when the health route is implemented.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it } from 'vitest'
|
||||||
|
|
||||||
|
describe('GET /health', () => {
|
||||||
|
it.todo('returns 200 with { ok: true, db: "up" } after a real DB round-trip (Task 2)')
|
||||||
|
|
||||||
|
it.todo('returns 503 if the DB round-trip throws (Task 2)')
|
||||||
|
})
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Test DB fixture helpers.
|
||||||
|
*
|
||||||
|
* For unit tests (Tasks 1-2 scope): provides mock/stub helpers so tests can run
|
||||||
|
* without a real MariaDB connection. Plan 02/03 will fill in integration-style
|
||||||
|
* fixtures that hit the Docker MariaDB (DB_HOST=127.0.0.1 from the dev compose override).
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Re-export vi for convenience in test files
|
||||||
|
export { vi } from 'vitest'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a minimal mock for the Drizzle `db` singleton.
|
||||||
|
* Replace individual methods per test as needed.
|
||||||
|
*
|
||||||
|
* Plan 02 (auth) will extend this with a real test-schema fixture.
|
||||||
|
* Plan 03 (broker) will extend with event-fixture helpers.
|
||||||
|
*/
|
||||||
|
export function createMockDb() {
|
||||||
|
return {
|
||||||
|
select: vi.fn().mockReturnThis(),
|
||||||
|
from: vi.fn().mockReturnThis(),
|
||||||
|
where: vi.fn().mockReturnThis(),
|
||||||
|
limit: vi.fn().mockResolvedValue([]),
|
||||||
|
insert: vi.fn().mockReturnThis(),
|
||||||
|
values: vi.fn().mockReturnThis(),
|
||||||
|
onDuplicateKeyUpdate: vi.fn().mockResolvedValue([{ id: 1 }]),
|
||||||
|
$returningId: vi.fn().mockResolvedValue([{ id: 1 }]),
|
||||||
|
execute: vi.fn().mockResolvedValue([]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sample VEVENT string for broker tests — a timed event (Plan 03).
|
||||||
|
*/
|
||||||
|
export const SAMPLE_VEVENT_TIMED = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//FamilySync//Test//EN
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:test-timed-event-001@familysync
|
||||||
|
DTSTART:20260615T100000Z
|
||||||
|
DTEND:20260615T110000Z
|
||||||
|
SUMMARY:Test Timed Event
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sample VEVENT string for broker tests — an all-day event (Plan 03).
|
||||||
|
*/
|
||||||
|
export const SAMPLE_VEVENT_ALLDAY = `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//FamilySync//Test//EN
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:test-allday-event-001@familysync
|
||||||
|
DTSTART;VALUE=DATE:20260615
|
||||||
|
DTEND;VALUE=DATE:20260616
|
||||||
|
SUMMARY:Test All-Day Event
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR`
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2023",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "tests"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
globals: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="theme-color" content="#4A90D9" />
|
||||||
|
<title>FamilySync</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "@familysync/pwa",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/react-query": "5.101.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"zustand": "5.0.14"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
|
"typescript": "^5.5.0",
|
||||||
|
"vite": "8.0.16"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import App from './App.js'
|
||||||
|
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
retry: 1,
|
||||||
|
staleTime: 30_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<App />
|
||||||
|
</QueryClientProvider>
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/health': 'http://localhost:3000',
|
||||||
|
'/api': 'http://localhost:3000',
|
||||||
|
'/callback': 'http://localhost:3000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Local dev overrides — use with: docker compose -f docker-compose.yml -f docker-compose.dev.yml up
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: ./apps/api
|
||||||
|
target: dev
|
||||||
|
volumes:
|
||||||
|
- ./apps/api/src:/app/src
|
||||||
|
environment:
|
||||||
|
NODE_ENV: development
|
||||||
|
|
||||||
|
mariadb:
|
||||||
|
ports:
|
||||||
|
- "3306:3306"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
services:
|
||||||
|
api:
|
||||||
|
build: ./apps/api
|
||||||
|
environment:
|
||||||
|
DB_HOST: mariadb
|
||||||
|
DB_PORT: 3306
|
||||||
|
DB_USER: familysync
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
DB_NAME: familysync
|
||||||
|
OIDC_AUTH_SECRET: ${OIDC_AUTH_SECRET:-placeholder_change_me}
|
||||||
|
OIDC_ISSUER: ${OIDC_ISSUER:-}
|
||||||
|
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-familysync}
|
||||||
|
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-}
|
||||||
|
OIDC_REDIRECT_URI: ${OIDC_REDIRECT_URI:-}
|
||||||
|
OIDC_AUTH_EXTERNAL_URL: ${OIDC_AUTH_EXTERNAL_URL:-}
|
||||||
|
APP_PASSWORD_ENCRYPTION_KEY: ${APP_PASSWORD_ENCRYPTION_KEY:-}
|
||||||
|
depends_on:
|
||||||
|
mariadb:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
|
||||||
|
mariadb:
|
||||||
|
image: mariadb:11
|
||||||
|
environment:
|
||||||
|
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
|
||||||
|
MARIADB_DATABASE: familysync
|
||||||
|
MARIADB_USER: familysync
|
||||||
|
MARIADB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- mariadb_data:/var/lib/mysql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
# Phase 1: present but unused; Phase 4 wires pub/sub for live list sync
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mariadb_data:
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "familysync",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "pnpm@11.5.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev:api": "pnpm --filter @familysync/api dev",
|
||||||
|
"dev:pwa": "pnpm --filter @familysync/pwa dev",
|
||||||
|
"build": "pnpm --filter @familysync/api build && pnpm --filter @familysync/pwa build",
|
||||||
|
"test": "pnpm --filter @familysync/api test",
|
||||||
|
"lint": "pnpm -r lint",
|
||||||
|
"typecheck": "pnpm -r typecheck"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+2438
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
|||||||
|
packages:
|
||||||
|
- "apps/*"
|
||||||
|
|
||||||
|
allowBuilds:
|
||||||
|
esbuild: true
|
||||||
Reference in New Issue
Block a user