22 KiB
22 KiB
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)
# 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: SvelteKit → static SPA → embed in Go binary
build-player: SvelteKit → static PWA → embed in Go binary
build-display: Copy display views → embed 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: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.
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:
- Context: Claude Code can read the spec in
docs/spec/, understand the architecture from ADRs, and see the full codebase - Atomicity: A feature that touches Go API + Svelte UI + display view is one commit
- Testing:
make testfrom root runs everything - Building:
make build-leaf-fullproduces 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