felt/docs/felt_repo_structure_v02.md

485 lines
22 KiB
Markdown

# Felt — Repository Structure
## Monorepo Layout
```
felt/
├── .forgejo/
│ └── workflows/ # CI/CD pipelines
│ ├── lint.yml
│ ├── test.yml
│ ├── build.yml
│ └── release.yml
├── cmd/ # Go entrypoints (main packages)
│ ├── felt-core/ # Core cloud service
│ │ └── main.go
│ ├── felt-leaf/ # Leaf node service
│ │ └── main.go
│ └── felt-cli/ # Management CLI tool
│ └── main.go
├── internal/ # Private Go packages (not importable externally)
│ ├── auth/ # Authentication (PIN, JWT, OIDC)
│ │ ├── jwt.go
│ │ ├── pin.go
│ │ ├── oidc.go
│ │ ├── middleware.go
│ │ └── roles.go
│ │
│ ├── tournament/ # Tournament engine (core domain)
│ │ ├── engine.go # Clock, state machine, level progression
│ │ ├── clock.go # Millisecond-precision countdown
│ │ ├── blinds.go # Blind structure, wizard, templates
│ │ ├── financial.go # Buy-in, rebuy, add-on, bounty, prize pool
│ │ ├── payout.go # Prize calculation, ICM, chop
│ │ ├── receipt.go # Transaction receipts
│ │ └── engine_test.go
│ │
│ ├── player/ # Player management
│ │ ├── database.go # CRUD, search, import
│ │ ├── tournament.go # In-tournament player state
│ │ ├── bust.go # Bust-out, undo, ranking
│ │ └── player_test.go
│ │
│ ├── table/ # Table & seating
│ │ ├── seating.go # Random seat, manual moves
│ │ ├── balance.go # Auto-balance algorithm
│ │ ├── layout.go # Table definitions, blueprints
│ │ └── balance_test.go
│ │
│ ├── league/ # League & points system
│ │ ├── league.go # League, season, standings
│ │ ├── formula.go # Formula parser & evaluator (sandboxed)
│ │ ├── formula_test.go
│ │ └── achievements.go
│ │
│ ├── events/ # Events & automation engine
│ │ ├── engine.go # Trigger → condition → action pipeline
│ │ ├── triggers.go # Trigger definitions
│ │ ├── actions.go # Action executors (sound, message, view change)
│ │ └── rules.go # Rule storage & builder
│ │
│ ├── display/ # Display node management
│ │ ├── registry.go # Node discovery, heartbeat, status
│ │ ├── routing.go # View assignment, cycling, multi-tournament
│ │ └── protocol.go # WebSocket protocol (state push, assign, heartbeat)
│ │
│ ├── content/ # Digital signage & info screen system
│ │ ├── editor.go # WYSIWYG content CRUD (templates, custom content)
│ │ ├── playlist.go # Playlist management, scheduling, priority
│ │ ├── renderer.go # Template → HTML/CSS bundle generation
│ │ ├── ai.go # AI-assisted content generation (prompt → layout)
│ │ └── templates/ # Built-in content templates (events, menus, promos)
│ │
│ ├── import/ # Data import from external systems
│ │ ├── tdd.go # TDD XML parser + converter (templates, players, history)
│ │ ├── csv.go # Generic CSV import (players, results)
│ │ └── wizard.go # Import wizard logic (preview, mapping, confirmation)
│ │
│ ├── sync/ # Leaf ↔ Core sync engine
│ │ ├── publisher.go # Leaf: publish events to NATS JetStream
│ │ ├── consumer.go # Core: consume events, upsert to PostgreSQL
│ │ ├── reconcile.go # Conflict resolution, reverse sync
│ │ └── messages.go # Message type definitions
│ │ # NOTE: No relay needed — Netbird reverse proxy tunnels
│ │ # player traffic directly to Leaf via WireGuard
│ │
│ ├── websocket/ # WebSocket hub
│ │ ├── hub.go # Connection manager, broadcast, rooms
│ │ ├── operator.go # Operator channel (full control)
│ │ ├── display.go # Display node channel (view data)
│ │ ├── player.go # Player channel (read-only)
│ │ └── hub_test.go
│ │
│ ├── api/ # HTTP API layer
│ │ ├── router.go # chi router setup, middleware chain
│ │ ├── tournaments.go # Tournament endpoints
│ │ ├── players.go # Player endpoints (in-tournament + database)
│ │ ├── tables.go # Table/seating endpoints
│ │ ├── displays.go # Display node endpoints
│ │ ├── leagues.go # League/standings endpoints
│ │ ├── export.go # Export endpoints (CSV, JSON, HTML)
│ │ └── health.go # Health check, version, status
│ │
│ ├── store/ # Database access layer
│ │ ├── libsql.go # LibSQL connection, migrations (Leaf)
│ │ ├── postgres.go # PostgreSQL connection, migrations (Core)
│ │ ├── migrations/ # SQL migration files
│ │ │ ├── leaf/
│ │ │ │ ├── 001_initial.sql
│ │ │ │ └── ...
│ │ │ └── core/
│ │ │ ├── 001_initial.sql
│ │ │ └── ...
│ │ ├── queries/ # SQL queries (sqlc or hand-written)
│ │ │ ├── tournaments.sql
│ │ │ ├── players.sql
│ │ │ ├── tables.sql
│ │ │ ├── transactions.sql
│ │ │ └── leagues.sql
│ │ └── store.go # Store interface (implemented by libsql + postgres)
│ │
│ ├── audit/ # Audit trail
│ │ ├── logger.go # Append-only audit record writer
│ │ └── types.go # Audit event types
│ │
│ ├── audio/ # Sound playback (Leaf only)
│ │ └── player.go # mpv subprocess, sound queue
│ │
│ └── config/ # Configuration
│ ├── config.go # Typed config struct, env var loading
│ ├── defaults.go # Default values
│ └── validate.go # Config validation
├── pkg/ # Public Go packages (shared types, importable)
│ ├── models/ # Shared domain models
│ │ ├── tournament.go
│ │ ├── player.go
│ │ ├── table.go
│ │ ├── transaction.go
│ │ ├── league.go
│ │ ├── display.go
│ │ └── event.go
│ │
│ └── protocol/ # WebSocket message types (shared by Go + JS)
│ ├── messages.go
│ └── messages.json # JSON Schema (for JS client codegen)
├── web/ # Frontend (SvelteKit)
│ ├── operator/ # Operator UI (mobile-first, served by Leaf + Core)
│ │ ├── src/
│ │ │ ├── lib/
│ │ │ │ ├── components/ # Reusable UI components
│ │ │ │ │ ├── Clock.svelte
│ │ │ │ │ ├── PlayerList.svelte
│ │ │ │ │ ├── SeatingChart.svelte
│ │ │ │ │ ├── BlindsSchedule.svelte
│ │ │ │ │ ├── QuickActions.svelte
│ │ │ │ │ ├── Toast.svelte
│ │ │ │ │ ├── Badge.svelte
│ │ │ │ │ ├── Card.svelte
│ │ │ │ │ ├── DataTable.svelte
│ │ │ │ │ └── ...
│ │ │ │ ├── stores/ # Svelte stores (WebSocket state)
│ │ │ │ │ ├── ws.ts # WebSocket connection manager
│ │ │ │ │ ├── tournament.ts # Tournament state store
│ │ │ │ │ ├── players.ts
│ │ │ │ │ ├── tables.ts
│ │ │ │ │ └── displays.ts
│ │ │ │ ├── api/ # REST API client
│ │ │ │ │ └── client.ts
│ │ │ │ ├── theme/ # Design system
│ │ │ │ │ ├── tokens.css # CSS custom properties (Catppuccin Mocha)
│ │ │ │ │ ├── typography.css
│ │ │ │ │ ├── components.css
│ │ │ │ │ └── themes.ts # Theme definitions (dark/light/custom)
│ │ │ │ └── utils/
│ │ │ │ ├── format.ts # Money, chip count, time formatting
│ │ │ │ └── localFirst.ts # Optional: try felt.local before proxy URL
│ │ │ ├── routes/
│ │ │ │ ├── +layout.svelte # Root layout (persistent header, nav)
│ │ │ │ ├── +page.svelte # Dashboard / tournament selector
│ │ │ │ ├── login/
│ │ │ │ │ └── +page.svelte # PIN / SSO login
│ │ │ │ ├── tournament/[id]/
│ │ │ │ │ ├── +layout.svelte # Tournament layout (header + tabs)
│ │ │ │ │ ├── +page.svelte # Overview dashboard
│ │ │ │ │ ├── players/
│ │ │ │ │ │ └── +page.svelte # Player list + actions
│ │ │ │ │ ├── tables/
│ │ │ │ │ │ └── +page.svelte # Seating chart
│ │ │ │ │ ├── clock/
│ │ │ │ │ │ └── +page.svelte # Clock control
│ │ │ │ │ ├── financials/
│ │ │ │ │ │ └── +page.svelte # Prize pool, transactions
│ │ │ │ │ └── settings/
│ │ │ │ │ └── +page.svelte # Tournament config
│ │ │ │ ├── displays/
│ │ │ │ │ └── +page.svelte # Display node management
│ │ │ │ ├── players/
│ │ │ │ │ └── +page.svelte # Player database
│ │ │ │ ├── leagues/
│ │ │ │ │ └── +page.svelte # League management
│ │ │ │ └── settings/
│ │ │ │ └── +page.svelte # Venue settings
│ │ │ └── app.html
│ │ ├── static/
│ │ │ ├── sounds/ # Default sound files
│ │ │ └── fonts/ # Inter, JetBrains Mono (self-hosted)
│ │ ├── svelte.config.js
│ │ ├── vite.config.js
│ │ ├── tailwind.config.js # Catppuccin color tokens
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ ├── player/ # Player mobile PWA (shared components with operator)
│ │ ├── src/
│ │ │ ├── lib/
│ │ │ │ ├── components/ # Subset of operator components
│ │ │ │ ├── stores/
│ │ │ │ │ └── ws.ts # WebSocket with smart routing
│ │ │ │ └── theme/ # Shared theme tokens
│ │ │ ├── routes/
│ │ │ │ ├── +layout.svelte
│ │ │ │ ├── +page.svelte # Clock + blinds (default view)
│ │ │ │ ├── schedule/
│ │ │ │ │ └── +page.svelte # Blind schedule
│ │ │ │ ├── rankings/
│ │ │ │ │ └── +page.svelte # Live rankings
│ │ │ │ ├── payouts/
│ │ │ │ │ └── +page.svelte # Prize structure
│ │ │ │ ├── league/
│ │ │ │ │ └── +page.svelte # League standings
│ │ │ │ └── me/
│ │ │ │ └── +page.svelte # Personal status (PIN-gated)
│ │ │ └── app.html
│ │ ├── static/
│ │ │ └── manifest.json # PWA manifest
│ │ ├── svelte.config.js
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ └── display/ # Display node views (vanilla HTML/CSS/JS)
│ ├── views/
│ │ ├── clock/
│ │ │ ├── index.html
│ │ │ ├── style.css
│ │ │ └── clock.js
│ │ ├── rankings/
│ │ │ ├── index.html
│ │ │ ├── style.css
│ │ │ └── rankings.js
│ │ ├── seating/
│ │ │ ├── index.html
│ │ │ ├── style.css
│ │ │ └── seating.js
│ │ ├── schedule/
│ │ │ ├── index.html
│ │ │ ├── style.css
│ │ │ └── schedule.js
│ │ ├── lobby/
│ │ │ ├── index.html
│ │ │ ├── style.css
│ │ │ └── lobby.js
│ │ ├── prizepool/
│ │ │ ├── index.html
│ │ │ ├── style.css
│ │ │ └── prizepool.js
│ │ ├── movement/
│ │ │ ├── index.html
│ │ │ ├── style.css
│ │ │ └── movement.js
│ │ ├── league/
│ │ │ ├── index.html
│ │ │ ├── style.css
│ │ │ └── league.js
│ │ └── welcome/
│ │ ├── index.html
│ │ ├── style.css
│ │ └── welcome.js
│ ├── shared/
│ │ ├── ws-client.js # WebSocket client (reconnect, interpolation)
│ │ ├── theme.css # Shared display theme (Catppuccin)
│ │ ├── typography.css
│ │ └── animations.css # Level transitions, bust-out effects
│ └── README.md
├── deploy/ # Deployment & infrastructure
│ ├── leaf/ # Leaf node OS image & config
│ │ ├── Makefile # Build Felt OS image (Armbian-based)
│ │ ├── overlay/ # Files overlaid on base OS
│ │ │ ├── etc/
│ │ │ │ ├── systemd/system/felt-leaf.service
│ │ │ │ ├── systemd/system/felt-nats.service
│ │ │ │ ├── nftables.conf
│ │ │ │ └── netbird/
│ │ │ └── opt/felt/
│ │ │ └── config.toml.example
│ │ └── scripts/
│ │ ├── setup-wizard.sh
│ │ └── update.sh
│ │
│ ├── display/ # Display node OS image & config
│ │ ├── Makefile
│ │ ├── overlay/
│ │ │ ├── etc/
│ │ │ │ ├── systemd/system/felt-kiosk.service
│ │ │ │ ├── chromium-flags.conf
│ │ │ │ └── netbird/
│ │ │ └── opt/felt-display/
│ │ │ └── boot.sh
│ │ └── scripts/
│ │ └── provision.sh
│ │
│ ├── core/ # Core infrastructure (PVE LXC/VM configs)
│ │ ├── ansible/ # Ansible playbooks for PVE provisioning
│ │ │ ├── inventory/
│ │ │ │ ├── dev.yml
│ │ │ │ └── production.yml
│ │ │ ├── playbooks/
│ │ │ │ ├── setup-pve.yml
│ │ │ │ ├── deploy-core-api.yml
│ │ │ │ ├── deploy-nats.yml
│ │ │ │ ├── deploy-authentik.yml
│ │ │ │ ├── deploy-netbird.yml # Unified server + reverse proxy + Traefik
│ │ │ │ ├── deploy-postgres.yml
│ │ │ │ └── deploy-pbs.yml
│ │ │ └── roles/
│ │ │ └── ...
│ │ ├── pve-templates/ # LXC/VM config templates
│ │ │ ├── felt-core-api.conf
│ │ │ ├── felt-nats.conf
│ │ │ ├── felt-postgres.conf
│ │ │ └── ...
│ │ └── docker-compose/ # For services that run in Docker (Authentik)
│ │ └── authentik/
│ │ └── docker-compose.yml
│ │
│ └── backup/ # Backup configuration
│ ├── pbs-config.md # PBS setup documentation
│ ├── wal-archive.sh # PostgreSQL WAL archiving script
│ └── restore-runbook.md
├── docs/ # Documentation
│ ├── spec/
│ │ └── felt_phase1_spec.md # This product spec (living document)
│ ├── architecture/
│ │ ├── decisions/ # Architecture Decision Records (ADR)
│ │ │ ├── 001-monorepo.md
│ │ │ ├── 002-go-backend.md
│ │ │ ├── 003-libsql-leaf.md
│ │ │ ├── 004-nats-sync.md
│ │ │ ├── 005-authentik-idp.md
│ │ │ ├── 006-pve-core.md
│ │ │ └── 007-sveltekit-ui.md
│ │ └── diagrams/
│ ├── api/ # API documentation (auto-generated from Go)
│ ├── operator-guide/ # End-user documentation
│ └── security/
│ ├── threat-model.md
│ └── incident-response.md
├── templates/ # Tournament templates & presets
│ ├── blind-structures/
│ │ ├── turbo.json
│ │ ├── standard.json
│ │ ├── deepstack.json
│ │ └── wsop-style.json
│ └── event-rules/
│ └── defaults.json
├── testdata/ # Test fixtures
│ ├── players.csv
│ ├── tournaments/
│ └── blind-structures/
├── go.mod
├── go.sum
├── Makefile # Top-level build orchestration
├── .env.example
├── .gitignore
├── LICENSE
└── README.md
```
## Build Targets (Makefile)
```makefile
# Go backends
build-leaf: Cross-compile felt-leaf for linux/arm64
build-core: Build felt-core for linux/amd64
build-cli: Build felt-cli for current platform
# Frontends
build-operator: SvelteKitstatic SPAembed in Go binary
build-player: SvelteKitstatic PWAembed in Go binary
build-display: Copy display viewsembed in Go binary
# Combined
build-all: All of the above
build-leaf-full: build-operator + build-player + build-display + build-leaf
→ single felt-leaf binary with all web assets embedded
# Testing
test: Go tests + Svelte tests
test-go: go test ./...
test-web: npm test in each web/ subdirectory
lint: golangci-lint + eslint + prettier
# Database
migrate-leaf: Run LibSQL migrations
migrate-core: Run PostgreSQL migrations
# Deployment
deploy-core: Ansible playbook for Core services
deploy-leaf-image: Build Felt OS image for Leaf SBC
deploy-display-image: Build display node image
# Development
dev-leaf: Run felt-leaf locally (with hot reload via air)
dev-operator: Run SvelteKit operator UI in dev mode
dev-player: Run SvelteKit player PWA in dev mode
dev-core: Run felt-core locally
```
## Key Design Decisions
### Single Binary Deployment (Leaf)
The Leaf node ships as a single Go binary with all web assets embedded via `go:embed`:
```go
//go:embed web/operator/build web/player/build web/display
var webAssets embed.FS
```
This means:
- Flash SSD → boot → one binary serves everything
- No npm, no node_modules, no webpack on the Pi
- Atomic updates: replace one binary, restart service
- Rollback: keep previous binary, switch systemd symlink
### Shared Types Between Go and JavaScript
The `pkg/protocol/messages.json` file is a JSON Schema that defines all WebSocket message types. This is the single source of truth consumed by:
- Go: generated types via tooling
- TypeScript: generated types for Svelte stores
- Display views: lightweight type checking
### Database Abstraction
The `internal/store/store.go` defines a `Store` interface. Both LibSQL (Leaf) and PostgreSQL (Core) implement this interface. The tournament engine, player management, etc. all depend on the interface, not on a specific database.
```go
type Store interface {
// Tournaments
CreateTournament(ctx context.Context, t *models.Tournament) error
GetTournament(ctx context.Context, id string) (*models.Tournament, error)
// ...
// Players
GetPlayer(ctx context.Context, id string) (*models.Player, error)
SearchPlayers(ctx context.Context, query string) ([]*models.Player, error)
// ...
}
```
This allows the same domain logic to run on Leaf (LibSQL) and Core (PostgreSQL) without code duplication.
### Development Workflow with Claude Code
The monorepo is designed for Claude Code's agentic workflow:
1. **Context**: Claude Code can read the spec in `docs/spec/`, understand the architecture from ADRs, and see the full codebase
2. **Atomicity**: A feature that touches Go API + Svelte UI + display view is one commit
3. **Testing**: `make test` from root runs everything
4. **Building**: `make build-leaf-full` produces a deployable binary
### Forgejo CI
`.forgejo/workflows/` contains pipelines that:
- Lint Go + JS on every push
- Run tests on every PR
- Build binaries on merge to main
- Build OS images on release tag