feat: v2 — queue matchmaking, procedural audio, sprite archetypes, auth

- Add queue-based matchmaking with Elo-proximity and 10s timeout
- Procedural sound engine (SFX, voice announcer, 4-track music)
- Sprite system refactored into 6 archetypes (standard, lobster, sheep, cyborg, blob, tank)
- 42+ fight choreographies with themed/generic/wild card selection
- 4 KO finish styles, super-speed mode, hyperdetail close-ups
- Auth routes, JoinBout page, bot profile with stats
- 7-tier ranking system (Baby through Legend)
- Arena and challenge system expansions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-06 22:13:19 +00:00
co-authored by Claude Opus 4.6
parent 335c148866
commit 47d20fbe66
82 changed files with 14011 additions and 741 deletions
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# PreToolUse Bash guard: block dangerous shell commands.
# Denies: rm -rf, git reset --hard, git push -f, git clean -fd, chmod -R 777,
# fork bombs, block device overwrites, mkfs, building Rust on macOS for Linux.
set -euo pipefail
INPUT=$(cat)
CMD=$(python3 -c "
import json, sys
try:
data = json.loads(sys.stdin.read())
print(data.get('tool_input', {}).get('command', ''))
except: pass
" <<< "$INPUT")
BASE="${CLAUDE_PROJECT_DIR:-}"
[[ -z "$BASE" ]] && BASE=$(python3 -c "
import json, sys
try:
data = json.loads(sys.stdin.read())
print(data.get('cwd', ''))
except: pass
" <<< "$INPUT")
[[ -z "$BASE" ]] && BASE="$(pwd)"
# Normalize: collapse whitespace, strip leading/trailing
CMD_NORM=$(echo "$CMD" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
deny() {
local reason="$1"
python3 -c "
import json
print(json.dumps({
'hookSpecificOutput': {
'hookEventName': 'PreToolUse',
'permissionDecision': 'deny',
'permissionDecisionReason': '$reason'
}
}))
"
exit 0
}
# Dangerous patterns
case "$CMD_NORM" in
*"rm -rf"*|*"rm -fr"*|*"rm -f -r"*|*"rm -r -f"*) deny "Destructive rm -rf blocked by security hook" ;;
*"git reset --hard"*) deny "git reset --hard would lose uncommitted work" ;;
*"git push --force"*|*"git push -f"*|*"git push -f "*) deny "git push --force would rewrite history" ;;
*"git clean -fd"*|*"git clean -f -d"*) deny "git clean -fd deletes untracked files" ;;
*"chmod -R 777"*|*"chmod -R 0777"*) deny "chmod -R 777 is a security risk" ;;
*":(){ :"*"};:"*) deny "Fork bomb pattern blocked" ;;
*"> /dev/sd"*|*">/dev/sd"*) deny "Block device overwrite blocked" ;;
*"mkfs "*|*"mkfs."*) deny "Disk format command blocked" ;;
esac
# Block building Rust locally on macOS (should always build on dev server)
if [[ "$(uname)" == "Darwin" ]]; then
if echo "$CMD_NORM" | grep -qE '^\s*cargo\s+build'; then
# Allow if it's clearly an SSH command (building on remote)
if ! echo "$CMD_NORM" | grep -qE 'ssh|sshpass'; then
deny "NEVER build Rust on macOS — use ./scripts/deploy-to-target.sh --live or build on dev server via SSH"
fi
fi
fi
# Check for path traversal escaping project root
if [[ -n "$BASE" ]] && [[ -d "$BASE" ]]; then
if echo "$CMD_NORM" | grep -qE '\.\./|/\.\.'; then
if echo "$CMD_NORM" | grep -qE '(rm|mv|cp|cat|chmod|chown)\s+.*\.\.'; then
if echo "$CMD_NORM" | grep -qE '\brm\b.*\.\.'; then
deny "Path traversal with rm blocked"
fi
fi
fi
fi
exit 0
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# PostToolUse Bash hook: detect deploy commands and remind to test.
# Triggers after deploy-to-target.sh runs.
set -euo pipefail
INPUT=$(cat)
CMD=$(python3 -c "
import json, sys
try:
data = json.loads(sys.stdin.read())
print(data.get('tool_input', {}).get('command', ''))
except: pass
" <<< "$INPUT")
# Only trigger on deploy commands or git push
if ! echo "$CMD" | grep -qE 'deploy-to-target|git\s+push'; then
exit 0
fi
TIMESTAMP=$(date '+%Y-%m-%d %H:%M')
python3 -c "
import json
message = '''Deploy detected at $TIMESTAMP.
Post-deploy checklist:
1. Test the web UI at http://192.168.1.228
2. Verify modified apps load correctly
3. Check backend logs: sudo journalctl -u archipelago -n 20
4. Check nginx: sudo tail -f /var/log/nginx/error.log
5. If building ISO, sync system configs to image-recipe/configs/
6. Update CHANGELOG.md if this is a notable change'''
output = {
'hookSpecificOutput': {
'hookEventName': 'PostToolUse',
'deployReminder': message
}
}
print(json.dumps(output))
"
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# PostToolUse Bash hook: detect git push/commit and prompt Claude to update PROGRESS.md.
# Returns structured feedback with recent commits so Claude can write a session log entry.
# Uses python3 instead of jq for JSON (guaranteed on macOS).
set -euo pipefail
INPUT=$(cat)
# Extract command from JSON using python3
CMD=$(python3 -c "
import json, sys
try:
data = json.loads(sys.stdin.read())
print(data.get('tool_input', {}).get('command', ''))
except: pass
" <<< "$INPUT")
# Only trigger on git push or git commit commands
if ! echo "$CMD" | grep -qE '\bgit\s+(push|commit)\b'; then
exit 0
fi
# Gather context for the progress update
BASE="${CLAUDE_PROJECT_DIR:-$(pwd)}"
BRANCH=$(git -C "$BASE" branch --show-current 2>/dev/null || echo "unknown")
PROGRESS_FILE="$BASE/PROGRESS.md"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M')
# Get recent commits (branch vs main, or last 10)
if git -C "$BASE" rev-parse --verify main &>/dev/null; then
COMMITS=$(git -C "$BASE" log --oneline main..HEAD 2>/dev/null | head -15)
if [ -z "$COMMITS" ]; then
COMMITS=$(git -C "$BASE" log --oneline -10 2>/dev/null)
fi
else
COMMITS=$(git -C "$BASE" log --oneline -10 2>/dev/null)
fi
# Get changed files in recent commits
CHANGED_FILES=$(git -C "$BASE" diff --name-only main..HEAD 2>/dev/null | head -20 || \
git -C "$BASE" diff --name-only HEAD~5..HEAD 2>/dev/null | head -20 || \
echo "unknown")
# Build the feedback message and output as JSON using python3
python3 -c "
import json, sys
message = '''Progress Update Needed
A git push/commit was detected on branch \`$BRANCH\` at $TIMESTAMP.
Recent commits:
\`\`\`
$COMMITS
\`\`\`
Changed files:
\`\`\`
$CHANGED_FILES
\`\`\`
Please update PROGRESS.md:
1. Add a session log entry under '## Session Log' with format: ### $TIMESTAMP$BRANCH
2. Summarize what was accomplished (2-4 bullet points based on the commits above)
3. Update any roadmap checkboxes if tasks were completed
4. Commit the PROGRESS.md update'''
output = {
'hookSpecificOutput': {
'hookEventName': 'PostToolUse',
'progressUpdate': message
}
}
print(json.dumps(output))
"
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
# PreToolUse Edit|Write guard: block edits outside project and to protected paths.
# Denies: paths outside project, .git/, .env*, lockfiles, node_modules/, deploy-config.sh
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(python3 -c "
import json, sys
try:
data = json.loads(sys.stdin.read())
print(data.get('tool_input', {}).get('file_path', ''))
except: pass
" <<< "$INPUT")
BASE="${CLAUDE_PROJECT_DIR:-}"
[[ -z "$BASE" ]] && BASE=$(python3 -c "
import json, sys
try:
data = json.loads(sys.stdin.read())
print(data.get('cwd', ''))
except: pass
" <<< "$INPUT")
[[ -z "$BASE" ]] && BASE="$(pwd)"
# Resolve to absolute path
if [[ -z "$FILE_PATH" ]]; then
exit 0
fi
ABS_BASE=$(cd "$BASE" 2>/dev/null && pwd) || true
[[ -z "$ABS_BASE" ]] && ABS_BASE=$(python3 -c "import os,sys; print(os.path.abspath(os.path.normpath(sys.argv[1])))" "$BASE" 2>/dev/null) || true
[[ -z "$ABS_BASE" ]] && ABS_BASE="$BASE"
[[ "$ABS_BASE" != */ ]] && ABS_BASE="${ABS_BASE}/"
if [[ "$FILE_PATH" != /* ]]; then
ABS_PATH="$ABS_BASE${FILE_PATH#./}"
else
ABS_PATH="$FILE_PATH"
fi
ABS_PATH=$(python3 -c "import os,sys; print(os.path.abspath(os.path.normpath(sys.argv[1])))" "$ABS_PATH" 2>/dev/null) || true
[[ -z "$ABS_PATH" ]] && ABS_PATH="$ABS_BASE${FILE_PATH#./}"
deny() {
local reason="$1"
echo "Blocked: $ABS_PATH$reason" >&2
python3 -c "
import json
print(json.dumps({
'hookSpecificOutput': {
'hookEventName': 'PreToolUse',
'permissionDecision': 'deny',
'permissionDecisionReason': '$reason'
}
}))
"
exit 0
}
# Protected patterns
PROTECTED_PATTERNS=(
".git/"
".env"
".env.local"
"node_modules/"
"package-lock.json"
"scripts/deploy-config.sh"
)
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$ABS_PATH" == *"$pattern"* ]] || [[ "$ABS_PATH" == *"/$pattern" ]]; then
deny "Edit blocked: path matches protected pattern ($pattern)"
fi
done
# .env.*.local
if [[ "$ABS_PATH" =~ \.env\..*\.local$ ]]; then
deny "Edit blocked: .env.*.local files contain secrets"
fi
# Ensure path is under project root
if [[ "$ABS_PATH" != "$ABS_BASE"* ]] && [[ "$ABS_PATH" != "$BASE"* ]]; then
deny "Edit blocked: path is outside project directory"
fi
exit 0
+13
View File
@@ -0,0 +1,13 @@
# BOTFIGHTS Project Memory
## Architecture
- **Frontend**: Vue 3 + Kaplay game engine, Vite, TypeScript
- **Server**: Node.js + Drizzle ORM, at `/Users/dorian/projects/botfights/server`
- **Frontend path**: `/Users/dorian/Projects/botfights/frontend`
## Content Counts
- **100 archetypes** in `sprites/archetypes/` (6 original + 19 individual + 75 in 5 batch files)
- **300 choreographies** (42 hand-crafted + 258 from 9 factories)
- **20 music tracks** (4 hand-crafted + 16 via genTrack)
- **30 voice profiles** (6 original + 24 new)
- **22 challenge types** in CHALLENGE_THEMED
@@ -0,0 +1,330 @@
# Botfights: 30-Day Production Plan
## Context
Botfights is a procedural fighting game (Vue 3 + Kaplay + Hono/SQLite) where AI bots battle through challenge rounds with pixel art sprites, synthesized audio, and choreographed fight animations. Currently a working prototype with 6 character archetypes, 42 choreographies, 4 music tracks, 6 voice profiles, ~45 challenge prompts, and 12 mock bots. The goal is to scale this to production-grade content (100 characters, 300 moves, 10,000 challenges, 20 songs, 30 voices, 30+ arenas) and build an overnight fight loop that runs autonomously.
---
## Phase 1: Module Splitting (Days 1-6)
The 3 monolith files must be split before any content scaling. Without this, adding content to 3500-line files is unmaintainable.
### Day 1-2: Split `sprites.ts` (954 lines)
```
frontend/src/game/sprites/
index.ts -- re-exports generateSpriteSheet, getBotColors, etc.
palette.ts -- makePal, parseHSL, Pal interface
constants.ts -- FRAME_SIZE, ANIMATIONS, INTERNAL, SCALE
drawing.ts -- px(), box(), fill() drawing primitives
base-body.ts -- drawFrame() core body/head/arms/legs/pose logic
archetypes/
index.ts -- archetype registry + roll function
standard.ts -- base (no extra features)
lobster.ts -- claws, antennae, segmented body, tail
sheep.ts -- wool, floppy ears, hooves
cyborg.ts -- metal plating, robot eye, wiring
blob.ts -- wobbly outline, googly eyes, drool
tank.ts -- armor, rivets, treads
judge.ts -- generateJudgeSpriteSheet()
```
Each archetype exports `{ name, weight, drawFeatures(ctx, pal, params) }`. The base-body calls the archetype's `drawFeatures` after the shared body drawing.
**Test:** Sprite preview page renders all 6 archetypes at all tiers identically to before.
### Day 3-4: Split `FightScene.ts` (~3500 lines)
```
frontend/src/game/fight/
index.ts -- createFightScene() orchestrator, returns controller
types.ts -- FightContext interface (k, W, H, GROUND_Y, HOME_A, HOME_B, helpers)
constants.ts -- ARENA_THEMES, spriteAnims, positions
particles.ts -- spawnSparks, spawnBulletHoles, spawnExhaust, etc.
projectiles.ts -- spawnProjectile, spawnBullet, spawnProp, spawnBarrage
effects.ts -- glitchRGB, scanlineGlitch, vhsTracking, dimensionalShift, etc.
grotesque.ts -- spawnGrotesqueDetails, destroyGrotesqueDetails
arena-decor.ts -- drawArenaDecor() per-arena backgrounds
choreography/
index.ts -- choreographyMap registry, pickChoreography
challenge-map.ts -- CHALLENGE_THEMED + critMoves
basic.ts -- dashPunch, aerialSlam, flyingKick, uppercut
ranged.ts -- projectile, gunBurst, sniperShot, minigunSpray
movement.ts -- dashThrough, fullScreenDash, teleportStrike
heavy.ts -- groundPound, jetpackDive, bodySlam, suplex
weapons.ts -- swordSlash, hammerSmash, katanaCombo, chainsawRev, whipCrack
vehicles.ts -- motorbikeCharge, carSmash, boatCannon
themed.ts -- golfClubSmash, fireBreath, riddleBarrage, coinShower, etc.
finishers.ts -- playKO (4 styles), playPerfect
round.ts -- playRound, playTaunt, playDodge
```
**Critical pattern:** Choreography functions close over Kaplay context. Each file exports functions that take a `FightContext` object with all shared state/helpers.
**Test:** Replay several fights, verify all 42 choreographies work.
### Day 5-6: Split `sounds.ts` (1381 lines) + server modules
```
frontend/src/game/sounds/
index.ts -- re-exports
context.ts -- AudioContext setup, gain nodes
primitives.ts -- tone(), noise(), sweep() helpers
sfx.ts -- all sfx functions (punch, kick, special, etc.)
voice.ts -- voice profiles, speak(), announce*(), voice lines
music/
index.ts -- startMusic(), stopMusic(), setMusicIntensity()
types.ts -- MusicTrack interface
player.ts -- playMusicBar() scheduler
tracks/
neon-fury.ts
dark-circuit.ts
pixel-blitz.ts
skull-crusher.ts
server/src/engine/
challenges/
index.ts -- pickChallenge(), registry
types.ts -- Challenge interfaces
speed-blitz.ts -- prompts array
riddle.ts
... (one per type)
mock/
index.ts -- runMockFight(), seedMockFights()
bots.ts -- MOCK_BOTS array
answers.ts -- MOCK_ANSWERS, BAD_ANSWERS
trash-talk.ts -- TRASH_TALK lines
```
**Test:** Full end-to-end fight replay with music, SFX, voice.
**Deliverable:** Identical functionality, ~40 small focused modules instead of 3 monoliths.
---
## Phase 2: Characters (Days 7-12)
### Day 7-8: New archetypes batch 1 (8 new)
- `dog.ts` -- floppy ears, wagging tail, snout with tongue, collar
- `cat.ts` -- pointed ears, whiskers, curled tail, slit eyes
- `cactus.ts` -- spikes everywhere, small flower on head, no real arms
- `pizza.ts` -- triangular body, cheese drip, pepperoni spots
- `mushroom.ts` -- dome cap head, spotted cap, stubby legs, spore particles
- `shark.ts` -- dorsal fin, teeth row, tail fin
- `penguin.ts` -- tuxedo coloring, beak, flippers
- `octopus.ts` -- 4 visible tentacles, large head, suction cups
### Day 9-10: New archetypes batch 2 (8 more)
- `skeleton.ts` -- visible ribs, skull head, bony limbs
- `ghost.ts` -- semi-transparent, wavy bottom (no legs), glowing eyes
- `alien.ts` -- big head, huge eyes, antenna, green tint
- `dinosaur.ts` -- tail, tiny arms, spiky back, big jaw
- `pirate.ts` -- eye patch, hat, peg leg, hook hand
- `ninja.ts` -- mask, headband, throwing star
- `cowboy.ts` -- hat, bandana, boots with spurs
- `wizard.ts` -- pointed hat, robe, staff replaces one arm, beard
### Day 11: Final archetypes + hybrid system
- `bee.ts` -- stripes, wings, stinger, antennae
- `frog.ts` -- wide mouth, bulging eyes, webbed feet, long tongue
- `snail.ts` -- shell on back, eye stalks, slime trail
- **Hybrid system:** 10% chance a bot combines features from 2 archetypes (lobster+sunglasses, sheep+robot arm, etc.)
- **Total:** 25 archetypes
### Day 12: 100 Named Bots + Seed Data
Expand `MOCK_BOTS` from 12 to 100 with distribution:
- ~5 tier-5, ~10 tier-4, ~20 tier-3, ~25 tier-2, ~25 tier-1, ~15 tier-0
Funny names by theme:
- **Tech:** `segfault_sarah`, `stack_overflow_sam`, `infinite_loop_larry`, `null_reference_nancy`
- **Food:** `pizza_pete`, `sushi_sensei`, `taco_tornado`, `burrito_bomber`
- **Animals:** `mega_lobster_xl`, `tactical_penguin`, `stealth_shark_3000`
- **Pop culture:** `keyboard_warrior`, `meme_lord_420`, `reply_guy_9000`
- **Absurd:** `sentient_toaster`, `angry_calculator`, `philosophical_fork`
Pre-seed 200 fight history records.
**Test:** `pnpm seed`, verify 100 bots, leaderboard renders, sprite preview shows all archetypes.
---
## Phase 3: Choreographies (Days 13-19)
Scale from 42 to 300 choreographies. Each follows the established function signature.
### Day 13-14: Food choreographies (~30 moves)
`choreography/food.ts`:
- `pizzaThrow`, `bananaSlip`, `pieFace`, `hotDogWhip`, `sushiRoll`
- `watermelonSmash`, `popcornBarrage`, `burritoWrap`, `cakeExplosion`, `iceCreamFreeze`
- `tacoSlam`, `donutSpin`, `breadSlap`, `eggCrack`, `soupSplash`
- `cheeseWheel`, `grapeShot`, `pineappleGrenade`, `cornCobGatling`, `steakSlap`
- Plus ~10 more food-themed variations
### Day 15-16: Spins/flips (~25) + vehicles (~15)
`choreography/spins.ts`:
- `spin720`, `backflipKick`, `tornadoSpin`, `corkscrewDive`, `cartwheel`
- `helicopterSpin`, `barrelRoll`, `frontflipSlam`, `spinningPiledriver`
- `doubleBackflip`, `wallflip`, `moonwalk`, `breakdanceSpin`, `drillKick`
- Plus more rotation/flip-based moves
`choreography/vehicles.ts` expansion:
- `skateboard`, `helicopter`, `shoppingCart`, `zamboni`, `rocketSled`
- `segway`, `unicycle`, `bulldozer`, `rollercoaster`, `cannonball`
- Plus existing (motorbikeCharge, carSmash, boatCannon)
### Day 17-18: Animal (~20) + energy (~20) + silly (~25)
`choreography/animal.ts`:
- `sharkBite`, `beeSwarm`, `frogTongue`, `stampede`, `spiderWeb`
- `snakeStrike`, `bearHug`, `eagleDive`, `ramCharge`, `crocodileRoll`
`choreography/energy.ts`:
- `kamehameha`, `hadouken`, `spiritBomb`, `thunderStrike`, `dragonPunch`
- `novaBlast`, `blackHole`, `sonicBoom`, `plasmaWhip`, `quantumTunnel`
`choreography/silly.ts`:
- `pillowFight`, `tickleAttack`, `selfieStun`, `vibeCheck`, `dabOnEm`
- `yeetThrow`, `airHorn`, `rubberChicken`, `bananaPeelChain`, `confettiCannon`
- `invisibleWall`, `slipNSlide`, `trampolineBounce`, `bubbleWrap`, `kazooBlast`
### Day 19: Wire all 300 choreographies
- Register all in `choreographyMap`
- Update `CHALLENGE_THEMED` with new move pools per challenge type
- Balance `pickChoreography` weights for maximum variety
- Add new moves to `critMoves` and `wild` arrays
**Test:** Run 50 fight replays, verify no crashes, observe variety.
---
## Phase 4: Challenges (Days 20-22)
Scale from ~45 prompts to ~10,000 challenges. Hilarious, modern, pop culture, stupid/silly.
### Day 20: Challenge bank architecture
```
server/src/engine/challenges/
banks/
speed-blitz.ts -- 1,200+ prompts
riddle.ts -- 500+ riddles
code-golf.ts -- 400+ challenges
roast-battle.ts -- 600+ roast prompts
hallucination.ts -- 800+ true/false
token-economy.ts -- 500+ explain prompts
creative-writing.ts -- 400+ creative prompts
math-blitz.ts -- 1,500+ math problems (many templated)
trap-card.ts -- 600+ prompt injections
meme-knowledge.ts -- 500+ meme identification (NEW TYPE)
emoji-translate.ts -- 400+ emoji challenges (NEW TYPE)
debate.ts -- 300+ debate topics (NEW TYPE)
fill-the-blank.ts -- 400+ completions (NEW TYPE)
reverse-engineer.ts -- 300+ output puzzles (NEW TYPE)
survival.ts -- 500+ escalating difficulty (NEW TYPE)
```
### Day 21: Build prompt banks (~10,000 total)
- Mix of hand-written and procedural templates
- Pop culture: "Is it true Elon Musk once fought a kangaroo?", "What would happen if the Minecraft creeper attended a job interview?"
- Modern/silly: "Explain blockchain to a medieval peasant", "Write a breakup text from a printer to its owner"
- Procedural templates for math/trivia: fill in numbers, countries, units
### Day 22: New challenge types + mock answers
- 6 new types: meme_knowledge, emoji_translate, debate, fill_the_blank, reverse_engineer, survival
- Scoring for each, mock answers for all 100 bots
- Wire into frontend `CHALLENGE_THEMED`
---
## Phase 5: Audio (Days 23-25)
### Day 23: 16 new music tracks (20 total)
Each track ~40 lines. Genre variety:
- 2x Synthwave (180-200 BPM), 2x Metal (250-280 BPM)
- 2x Chiptune (200-220 BPM), 2x Jazz Fusion (160-180 BPM)
- 2x Drum & Bass (170-175 BPM), 2x Lo-fi (140-160 BPM)
- 2x Ska Punk (210-230 BPM), 2x Boss Fight (280-300 BPM)
Intensity-based genre grouping:
- Low: lo-fi, jazz, chiptune | Medium: synthwave, ska punk
- High: metal, drum & bass | Max: boss fight
### Day 24: 24 new voice profiles (30 total)
New: whisper, manic, gravelly, chipper, deadpan, sports_caster, old_timey, drill_sergeant, surfer_dude, valley_girl, pirate_voice, robot_glitch, auctioneer, news_anchor, horror_narrator, lullaby, rage_quit, stoner, brit_posh, aussie, game_show, wrestling_announcer, noir_detective, anime_narrator
Expand voice line libraries to 300+ unique lines.
### Day 25: New SFX + arena-specific ambience
- 20 new SFX for new choreography types
- Arena-specific ambient background layers (beach waves, space hum, forest chirps)
---
## Phase 6: Scenes & Polish (Days 26-27)
### Day 26: 15+ new arenas (30+ total)
volcano, underwater, rooftop, train, kitchen, arcade, library, parking_lot, school, hospital, factory, castle, moon_base, ice_rink, stadium
New modifiers: fire_boost, ice_slow, gravity_low, double_damage, chaos_mode
### Day 27: Scene transitions + animated elements
- 4 transition styles: wipe, circle close/open, pixel dissolve, glitch cut
- Animated arena backgrounds: floating particles, dynamic lighting
- Enhanced spectator crowd reactions tied to round outcomes
---
## Phase 7: Production Loop (Days 28-30)
### Day 28: Fight scheduler + matchmaking
`server/src/engine/scheduler.ts`:
- `startScheduler(intervalMs)` -- Elo-based matchmaking loop
- Routes: `POST /api/scheduler/start|stop`, `GET /api/scheduler/status`
- Queue with rate limiting + bot cooldown
### Day 29: Headless mode + overnight operation
- `pnpm run fight-loop` for headless scheduler
- Env vars: `FIGHT_INTERVAL_MS`, `FIGHTS_PER_BATCH`, `MAX_CONCURRENT`
- Structured JSON logging, graceful SIGTERM shutdown
- `GET /api/stats` endpoint, health monitoring
### Day 30: Final seed data + integration test
- Generate: 100 bots, 500+ fight history
- Verify: Elo bell curve, tier distribution
- Full E2E: seed -> dev -> replay 10 fights -> fight-loop -> 100 fights -> typecheck + lint
---
## Dependency Graph
```
Phase 1 (Split) --- BLOCKS EVERYTHING
|
+-- Phase 2 (Characters) -- independent after Phase 1
+-- Phase 3 (Choreographies) -- independent after Phase 1
+-- Phase 4 (Challenges) -- independent after Phase 1
+-- Phase 5 (Audio) -- independent after Phase 1
|
+-- Phase 6 (Scenes) -- needs Phase 1 + Phase 3
+-- Phase 7 (Production) -- needs Phase 2 + Phase 4
```
## Testing Strategy
After every day:
1. `pnpm typecheck` -- catches import/export errors
2. Sprite preview page -- verify all archetypes render
3. Watch 5 random fight replays -- no crashes, visual variety
4. Audio check -- music, voices, SFX all play
5. `pnpm seed` -- verify DB has expected data
## Critical Files
- `frontend/src/game/sprites.ts` -> split into `sprites/` directory
- `frontend/src/game/FightScene.ts` -> split into `fight/` directory
- `frontend/src/game/sounds.ts` -> split into `sounds/` directory
- `frontend/src/components/FightViewer.vue` -> update imports
- `server/src/engine/challenges.ts` -> split into `challenges/` directory
- `server/src/engine/mock.ts` -> split into `mock/` + expand to 100 bots
- `server/src/engine/arenas.ts` -> expand to 30+ arenas
- `server/src/seed.ts` -> update imports
- `server/src/routes/fights.ts` -> add scheduler routes
+25
View File
@@ -0,0 +1,25 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-risky-bash.sh"
}
]
},
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
],
"PostToolUse": []
}
}
+49
View File
@@ -0,0 +1,49 @@
---
name: add-app
description: Step-by-step guide for adding a new containerized app to Archipelago
disable-model-invocation: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
argument-hint: "[app-name]"
---
Add a new containerized app ($ARGUMENTS) to Archipelago.
## Steps
### 1. Create the manifest
Create `apps/{app-id}/manifest.yml` following the spec in `docs/app-manifest-spec.md`:
- `app.id` (kebab-case), `app.name`, `app.version` (SemVer)
- `container.image` (pinned version, **NEVER** `latest`)
- `security`: readonly_root, dropped capabilities, non-root UID > 1000
- `health_check`, `dependencies`
### 2. Add app icon
Place icon at `neode-ui/public/assets/img/app-icons/{app-id}.{png|webp|svg}`
### 3. Create status UI (if no native web UI)
For apps without their own web interface, create a UI container in `docker/{app-id}-ui/` following the patterns in `.cursor/rules/APP-UI-STANDARDS.md`.
Reference implementations:
- Bitcoin UI: `docker/bitcoin-ui/`
- LND UI: `docker/lnd-ui/`
### 4. Update backend
- Add port mapping in `core/archipelago/src/container/docker_packages.rs`
- Add env vars in `get_app_config()` in `core/archipelago/src/api/rpc.rs`
### 5. Deploy and test
- Deploy: `./scripts/deploy-to-target.sh --live`
- Install from marketplace UI at http://192.168.1.228
- Verify it launches and auto-connects to dependencies
- Check logs: `sudo podman logs {container-name}`
### 6. Security review
- Verify readonly root, dropped caps, non-root user
- Check network isolation
- No hardcoded secrets
@@ -0,0 +1,147 @@
---
description: E2E encrypted records and group encryption for the Syntropy Institute portal
match:
- encrypt
- decrypt
- record
- NIP-44
- group key
- channel
- community
---
# Encrypted Records Skill
## When to Use
When working with E2E encrypted client records, community posts, or group key management.
## NIP-44 Encryption (Practitioner <-> Client)
```typescript
import { nip44 } from 'nostr-tools';
// Encrypt record for a specific client
function encryptRecord(
practitionerSecretKey: Uint8Array,
clientPubKey: string,
recordData: object
): string {
const plaintext = JSON.stringify(recordData);
const conversationKey = nip44.v2.utils.getConversationKey(practitionerSecretKey, clientPubKey);
return nip44.v2.encrypt(plaintext, conversationKey);
}
// Decrypt record (client side)
function decryptRecord(
clientSecretKey: Uint8Array,
practitionerPubKey: string,
encryptedData: string
): object {
const conversationKey = nip44.v2.utils.getConversationKey(clientSecretKey, practitionerPubKey);
const plaintext = nip44.v2.decrypt(encryptedData, conversationKey);
return JSON.parse(plaintext);
}
```
## Group Encryption (Community Channels)
### Creating a Channel with Group Key
```typescript
// Generate a symmetric group key for the channel
function generateGroupKey(): Uint8Array {
return crypto.getRandomValues(new Uint8Array(32));
}
// Encrypt group key for a specific member using NIP-44
function wrapGroupKeyForMember(
adminSecretKey: Uint8Array,
memberPubKey: string,
groupKey: Uint8Array
): string {
const conversationKey = nip44.v2.utils.getConversationKey(adminSecretKey, memberPubKey);
return nip44.v2.encrypt(
btoa(String.fromCharCode(...groupKey)),
conversationKey
);
}
// Member unwraps their group key
function unwrapGroupKey(
memberSecretKey: Uint8Array,
adminPubKey: string,
wrappedKey: string
): Uint8Array {
const conversationKey = nip44.v2.utils.getConversationKey(memberSecretKey, adminPubKey);
const decoded = nip44.v2.decrypt(wrappedKey, conversationKey);
return Uint8Array.from(atob(decoded), c => c.charCodeAt(0));
}
```
### Encrypting Community Posts with Group Key
```typescript
// Encrypt post content with the channel's symmetric group key
async function encryptPostWithGroupKey(
groupKey: Uint8Array,
content: string
): Promise<string> {
const key = await crypto.subtle.importKey(
'raw', groupKey, 'AES-GCM', false, ['encrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(content);
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv }, key, encoded
);
const combined = new Uint8Array([...iv, ...new Uint8Array(ciphertext)]);
return btoa(String.fromCharCode(...combined));
}
// Decrypt post content
async function decryptPostWithGroupKey(
groupKey: Uint8Array,
encrypted: string
): Promise<string> {
const combined = Uint8Array.from(atob(encrypted), c => c.charCodeAt(0));
const iv = combined.slice(0, 12);
const ciphertext = combined.slice(12);
const key = await crypto.subtle.importKey(
'raw', groupKey, 'AES-GCM', false, ['decrypt']
);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv }, key, ciphertext
);
return new TextDecoder().decode(decrypted);
}
```
### Key Rotation (when member removed)
```typescript
// When a member is removed from a channel:
// 1. Generate new group key
// 2. Re-wrap for all remaining members
// 3. Update ChannelMember.encryptedGroupKey for each
// 4. New posts use new key; old posts remain readable with old key
// (store key version/epoch on each post)
```
## Record Types
```typescript
interface ClientRecord {
type: 'session_notes' | 'assessment' | 'treatment_plan';
title: string;
content: string; // Rich text or structured data
attachments?: {
name: string;
mimeType: string;
encryptedData: string; // Each file encrypted separately
}[];
createdAt: string; // ISO timestamp
}
```
## Rules
- ALL encryption happens client-side — server stores only encrypted blobs
- Use NIP-44 v2 (NOT NIP-04 — it's deprecated and has known weaknesses)
- Group keys: AES-256-GCM with random IVs
- Include key version/epoch on group-encrypted content for rotation support
- Never log or expose plaintext content on the server
- Media files are encrypted individually before upload
+49
View File
@@ -0,0 +1,49 @@
---
name: harden
description: Security hardening review and fixes for Archipelago code and infrastructure
disable-model-invocation: true
allowed-tools: Read, Edit, Write, Glob, Grep, Bash
argument-hint: "[area: backend|frontend|containers|scripts|all]"
---
Perform a security hardening pass on $ARGUMENTS (default: all).
## Backend Hardening (Rust)
- [ ] No hardcoded credentials — check for Base64-encoded auth strings, passwords in source
- [ ] Secrets use `core/security/secrets_manager.rs` — verify encryption is implemented (not plaintext)
- [ ] All RPC endpoints validate inputs before processing
- [ ] No `unwrap()` on user-supplied data — handle errors gracefully
- [ ] Rate limiting on auth endpoints (login, password change)
- [ ] Session tokens have proper expiry and rotation
- [ ] File permissions: keys at 0o600, dirs at 0o700
- [ ] Tracing never logs secrets, passwords, keys, or tokens
## Frontend Hardening (Vue/TypeScript)
- [ ] No secrets in source (API keys, passwords, tokens)
- [ ] No `eval()` or `innerHTML` with untrusted content
- [ ] XSS prevention — sanitize all user inputs
- [ ] CSRF protection on state-changing requests
- [ ] Credentials use `credentials: 'include'` not localStorage tokens
- [ ] No sensitive data in console.log statements
## Container Hardening
- [ ] All manifests: `readonly_root: true` (unless documented exception)
- [ ] All manifests: capabilities dropped, only required ones added
- [ ] All manifests: non-root user (UID > 1000)
- [ ] All manifests: `no-new-privileges: true`
- [ ] All images pinned to specific versions (no `:latest`)
- [ ] Network isolation — no `host` network unless required and documented
- [ ] AppArmor profiles defined and enforced
## Script Hardening
- [ ] All scripts use `set -euo pipefail`
- [ ] No hardcoded passwords (use deploy-config.sh or env vars)
- [ ] SSH uses proper key-based auth where possible
- [ ] No `chmod 777` or overly permissive permissions
- [ ] Temp files use `mktemp` not predictable paths
Report all findings with file paths and line numbers. Fix issues directly where safe to do so. Flag anything that needs discussion.
+52
View File
@@ -0,0 +1,52 @@
---
name: lint
description: Run all linters and type checks for the Archipelago project
allowed-tools: Bash, Read, Grep
argument-hint: "[backend|frontend|all]"
---
Run linters and type-checks for $ARGUMENTS (default: all).
## Frontend Linting
```bash
cd neode-ui
# Type check
npm run type-check 2>&1
# Check for any `any` types (should be zero)
grep -rn ': any' src/ --include='*.ts' --include='*.vue' | grep -v node_modules | grep -v '.d.ts'
# Check for inline Tailwind violations (long class strings)
grep -rn 'class="[^"]\{100,\}"' src/ --include='*.vue'
# Check for TODO/FIXME
grep -rn 'TODO\|FIXME' src/ --include='*.ts' --include='*.vue'
# Check for console.log (should be cleaned before production)
grep -rn 'console\.\(log\|warn\|error\)' src/ --include='*.ts' --include='*.vue' | wc -l
```
## Backend Linting (on dev server)
```bash
sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228 \
'source ~/.cargo/env && cd ~/archy/core && cargo clippy --all-targets --all-features 2>&1 && cargo fmt --all -- --check 2>&1'
```
## Script Linting
```bash
# Check for scripts missing set -e
for f in scripts/*.sh; do
if ! head -5 "$f" | grep -q 'set -e'; then
echo "MISSING set -e: $f"
fi
done
# Check for hardcoded IPs (should use variables)
grep -rn '192\.168\.1\.' scripts/ --include='*.sh' | grep -v deploy-config
```
Report all issues found with severity (critical/warning/info).
+116
View File
@@ -0,0 +1,116 @@
---
description: Nostr keypair authentication for the Syntropy Institute portal (auth only, no relays)
match:
- nostr
- auth
- login
- keypair
- sign
- challenge
---
# Nostr Auth Skill
## When to Use
When working with authentication flows, keypair management, or NIP-98 API auth in the portal.
## Key Principle
Nostr is used ONLY for cryptographic authentication. No relay connections. No event publishing. Just keypairs and signatures.
## Keypair Generation (Easy Mode)
```typescript
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
// Generate new identity
const secretKey = generateSecretKey(); // Uint8Array
const publicKey = getPublicKey(secretKey); // hex string
// Encode for display (only when user requests it)
const npub = nip19.npubEncode(publicKey);
const nsec = nip19.nsecEncode(secretKey);
```
## Private Key Encryption (for localStorage)
```typescript
// Encrypt private key with user's passphrase before storing
async function encryptPrivateKey(secretKey: Uint8Array, passphrase: string): Promise<string> {
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw', encoder.encode(passphrase), 'PBKDF2', false, ['deriveKey']
);
const salt = crypto.getRandomValues(new Uint8Array(16));
const derivedKey = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 600000, hash: 'SHA-256' },
keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['encrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv }, derivedKey, secretKey
);
// Return salt + iv + ciphertext as base64
const combined = new Uint8Array([...salt, ...iv, ...new Uint8Array(encrypted)]);
return btoa(String.fromCharCode(...combined));
}
```
## NIP-07 Detection (Nostr Native)
```typescript
// Check for browser extension
function hasNostrExtension(): boolean {
return typeof window !== 'undefined' && 'nostr' in window;
}
// Sign with extension
async function signWithExtension(event: object): Promise<object> {
return await (window as any).nostr.signEvent(event);
}
// Get public key from extension
async function getExtensionPubkey(): Promise<string> {
return await (window as any).nostr.getPublicKey();
}
```
## Challenge-Response Auth (NIP-98 style)
```typescript
import { finalizeEvent, verifyEvent } from 'nostr-tools';
// Client: Sign auth challenge
function createAuthEvent(secretKey: Uint8Array, url: string, method: string) {
const event = finalizeEvent({
kind: 27235, // NIP-98 HTTP Auth
created_at: Math.floor(Date.now() / 1000),
tags: [
['u', url],
['method', method],
],
content: '',
}, secretKey);
return event;
}
// Send as Authorization header:
// Authorization: Nostr <base64-encoded-event-json>
// Server: Verify auth event
function verifyAuthEvent(event: any, expectedUrl: string, expectedMethod: string): boolean {
if (!verifyEvent(event)) return false;
if (event.kind !== 27235) return false;
const urlTag = event.tags.find((t: string[]) => t[0] === 'u');
const methodTag = event.tags.find((t: string[]) => t[0] === 'method');
if (urlTag?.[1] !== expectedUrl) return false;
if (methodTag?.[1] !== expectedMethod) return false;
// Check timestamp is within 60 seconds
if (Math.abs(Date.now() / 1000 - event.created_at) > 60) return false;
return true;
}
```
## Rules
- NEVER store private keys on the server
- NEVER connect to any Nostr relay
- NEVER transmit private keys over the network
- ALWAYS encrypt private keys before storing in localStorage
- Use PBKDF2 with at least 600,000 iterations for key derivation
- Auth events expire after 60 seconds
- Server only stores npub (public key)
+19
View File
@@ -0,0 +1,19 @@
---
name: overnight
description: Commit, branch, and start the overnight automation loop
disable-model-invocation: true
allowed-tools: Bash(*), Read, Write, Edit, Glob, Grep
---
Prepare and launch the overnight automation loop. Do ALL steps in order, stopping on any failure:
1. Stage and commit all uncommitted changes: `git add -A && git commit -m "chore: pre-overnight snapshot"` (skip if working tree is clean)
2. Push current branch to origin
3. Get today's date as YYYY-MM-DD. Check if `overnight/$DATE` branch exists:
- If yes: `git checkout overnight/$DATE`
- If no: run `./loop/prepare.sh`
4. Verify `loop/plan.md` has unchecked tasks (`grep -c '^\- \[ \]' loop/plan.md`)
5. Commit plan files if modified: `git add loop/plan.md loop/prompt.md && git commit -m "chore: overnight plan $DATE"` (skip if clean)
6. Push: `git push -u origin overnight/$DATE`
7. Start the loop: run `caffeinate -i ./loop/loop.sh` with `run_in_background: true`
8. Report: branch name, number of tasks, and confirm the loop is running in background
+102
View File
@@ -0,0 +1,102 @@
---
name: pwa-icon-cache-fix
description: Use when the user reports a PWA icon not updating, stale PWA icon, wrong icon after install, or any PWA caching issue. Also applies when changing PWA icons in a Vite + vite-plugin-pwa project.
version: 2.0.0
---
# PWA Icon Cache Fix
## Problem
PWA icons are cached at FOUR independent layers:
1. **Service worker cache** (Workbox precache)
2. **Browser HTTP cache**
3. **Browser manifest resources** (Chromium stores resized icons in its profile data, keyed by a permanent extension ID tied to the origin — NEVER re-fetched even after uninstall/reinstall)
4. **macOS .app bundle** (`.icns` file baked into the `.app` in `~/Applications/`)
Query string cache busting (`?v=2`) and uninstall/reinstall do NOT fix this. Chromium reuses the same extension ID for the same origin, so it keeps the old cached icons.
## Fix Steps
### 1. Verify icon files on disk and server are correct
```bash
# Visual check
Read packages/app/public/pwa-192x192.png
Read packages/app/public/pwa-512x512.png
# Hash match check
curl -s http://localhost:5173/pwa-192x192.png | md5
md5 -q packages/app/public/pwa-192x192.png
```
### 2. Find the PWA's Chromium extension ID
Read the installed `.app` bundle's `Info.plist` to get the `CrAppModeShortcutID`:
```bash
plutil -p "~/Applications/Brave Browser Apps.localized/AIUI.app/Contents/Info.plist" | grep CrAppModeShortcutID
```
This returns an ID like `idemibpphagihbobmgmaojhjfidlfpdl`.
### 3. Overwrite the cached icons in browser profile
Chromium stores resized icons at:
`~/Library/Application Support/BraveSoftware/Brave-Browser/Default/Web Applications/Manifest Resources/{ID}/Icons/`
Overwrite every size using `sips`:
```bash
ICON_DIR="~/Library/Application Support/BraveSoftware/Brave-Browser/Default/Web Applications/Manifest Resources/{ID}/Icons"
SRC="packages/app/public/pwa-512x512.png"
for size in 32 48 64 96 128 192 256 512; do
sips -z $size $size "$SRC" --out "${ICON_DIR}/${size}.png"
done
```
### 4. Rebuild the macOS .icns in the .app bundle
```bash
ICONSET="/tmp/aiui.iconset"
mkdir -p "$ICONSET"
SRC="packages/app/public/pwa-512x512.png"
sips -z 16 16 "$SRC" --out "$ICONSET/icon_16x16.png"
sips -z 32 32 "$SRC" --out "$ICONSET/icon_16x16@2x.png"
sips -z 32 32 "$SRC" --out "$ICONSET/icon_32x32.png"
sips -z 64 64 "$SRC" --out "$ICONSET/icon_32x32@2x.png"
sips -z 128 128 "$SRC" --out "$ICONSET/icon_128x128.png"
sips -z 256 256 "$SRC" --out "$ICONSET/icon_128x128@2x.png"
sips -z 256 256 "$SRC" --out "$ICONSET/icon_256x256.png"
sips -z 512 512 "$SRC" --out "$ICONSET/icon_256x256@2x.png"
sips -z 512 512 "$SRC" --out "$ICONSET/icon_512x512.png"
cp "$SRC" "$ICONSET/icon_512x512@2x.png"
iconutil -c icns "$ICONSET" -o "~/Applications/Brave Browser Apps.localized/AIUI.app/Contents/Resources/app.icns"
```
### 5. Flush macOS icon cache
```bash
touch "~/Applications/Brave Browser Apps.localized/AIUI.app"
killall Finder
killall Dock
```
### 6. Bump PWA_CACHE_VERSION in main.ts
Increment the `PWA_CACHE_VERSION` constant — this nukes all SW caches on next page load for web-layer caching.
### 7. Delete stale build artifacts
Remove old `dist/` and `dev-dist/` SW/manifest files.
## Browser-Specific Paths
- **Brave**: `~/Library/Application Support/BraveSoftware/Brave-Browser/Default/Web Applications/`
- **Chrome**: `~/Library/Application Support/Google/Chrome/Default/Web Applications/`
- **PWA apps (Brave)**: `~/Applications/Brave Browser Apps.localized/`
- **PWA apps (Chrome)**: `~/Applications/Chrome Apps.localized/`
## Key Insight
Chromium assigns a permanent extension ID per origin (e.g., `localhost:5173`). This ID persists across uninstall/reinstall. The icon cache in `Manifest Resources/{ID}/Icons/` is populated ONCE and never refreshed from the manifest. The only fix is to overwrite the files directly on disk.
+41
View File
@@ -0,0 +1,41 @@
---
name: refactor
description: Refactor code for quality, maintainability, and adherence to project standards
disable-model-invocation: true
allowed-tools: Read, Edit, Write, Glob, Grep, Bash
argument-hint: "[file-or-area]"
---
Refactor the specified code ($ARGUMENTS) following Archipelago coding standards.
## Checklist
### Rust Backend
- [ ] No `unwrap()` or `expect()` — use `?` operator with context
- [ ] Replace `#[allow(dead_code)]` — either use it or remove it
- [ ] Functions under 50 lines, single responsibility
- [ ] Custom error types per module with `thiserror`
- [ ] `tracing` for logging — no `println!` or secrets in logs
- [ ] Split files over 500 lines into focused modules
- [ ] Run `cargo clippy --all-targets --all-features` mentally and fix issues
### Vue Frontend
- [ ] Extract ALL inline Tailwind to global classes in `neode-ui/src/style.css`
- [ ] Use semantic class names: `.glass-card`, `.info-card`, `.glass-button`, `.path-option-card`
- [ ] Replace ALL `.gradient-button` with `.glass-button` (gradient buttons are BANNED)
- [ ] Replace ALL `.gradient-card` / `.gradient-card-dark` with `.glass-card` or `.path-option-card`
- [ ] Settings.vue is the gold standard — all screens should match its patterns
- [ ] Replace `any` types with proper interfaces or `unknown`
- [ ] Ensure `<script setup lang="ts">` on all components
- [ ] Remove dead code (unused imports, components like HelloWorld.vue)
- [ ] Remove all `TODO`/`FIXME` — fix now or create GitHub issues
- [ ] Consolidate `console.log` calls to use a logging utility
- [ ] Split views over 800 LOC into sub-components
### General
- [ ] No hardcoded paths (`/Users/dorian/...`)
- [ ] No hardcoded credentials — use env vars or secrets manager
- [ ] Comment WHY not WHAT
- [ ] Remove commented-out code entirely
After refactoring, verify the code still compiles/type-checks. For frontend: `cd neode-ui && npm run type-check`. Do NOT deploy — leave that to `/deploy`.
+59
View File
@@ -0,0 +1,59 @@
---
name: test
description: Run tests or create test coverage for Archipelago
disable-model-invocation: true
allowed-tools: Read, Edit, Write, Glob, Grep, Bash
argument-hint: "[area: backend|frontend|all] or [specific-file]"
---
Run or create tests for $ARGUMENTS.
## Backend Testing (Rust)
### Run existing tests
```bash
# On dev server (never build Rust on macOS)
sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228 \
'source ~/.cargo/env && cd ~/archy/core && cargo test --all-features 2>&1'
```
### Creating new tests
- Place unit tests in the same file with `#[cfg(test)]` module
- Place integration tests in `core/{crate}/tests/`
- Use `#[tokio::test]` for async tests
- Mock external dependencies (filesystem, network, Podman)
- Test error cases, not just happy paths
- Aim for >80% coverage on core logic
### Priority areas needing tests
1. RPC endpoint handlers (core/archipelago/src/api/)
2. Manifest parsing (core/container/src/manifest.rs)
3. Dependency resolver (core/container/src/dependency_resolver.rs)
4. Auth flows (core/archipelago/src/auth.rs)
5. Secrets manager (core/security/src/secrets_manager.rs)
6. Port allocation (core/container/src/port_manager.rs)
## Frontend Testing (Vue/TypeScript)
### Setup (if not already configured)
Ensure vitest is configured in `neode-ui/`:
```bash
cd neode-ui && npm run test 2>&1 || echo "No test script configured"
```
### Creating new tests
- Use Vitest + @vue/test-utils
- Place tests in `neode-ui/src/__tests__/` or co-located `*.test.ts`
- Test stores (Pinia) with `createTestingPinia()`
- Test API clients with mocked fetch
- Test component rendering and interactions
- Test routing guards
### Priority areas needing tests
1. Pinia stores (app.ts, container.ts, appLauncher.ts)
2. RPC client (api/rpc-client.ts) — error handling, retry logic
3. WebSocket client (api/websocket.ts) — reconnection
4. Router guards — auth flow, session timeout
5. Key components — ContainerStatus, SpotlightSearch
Report test results and any new tests created.
+90
View File
@@ -0,0 +1,90 @@
---
name: ux-review
description: Review UI components against Archipelago glassmorphism design standards and UX conventions
disable-model-invocation: true
allowed-tools: Read, Glob, Grep, Edit, Write
argument-hint: "[component-or-view-name]"
---
Review the UI of $ARGUMENTS against Archipelago's glassmorphism design system and UX standards.
## Design System Compliance
### Glass Classes (must use global classes from style.css)
- [ ] Section containers use `.path-option-card cursor-default px-6 py-6` (Settings-style sections)
- [ ] Content containers/modals use `.glass-card`
- [ ] Interactive selectable cards use `.path-option-card` (with hover)
- [ ] Status displays use `.info-card` (no hover effects)
- [ ] ALL buttons use `.glass-button` — NEVER `.gradient-button` (BANNED)
- [ ] Large primary actions use `.path-action-button`
- [ ] Info sub-cards use `bg-black/20 rounded-xl border border-white/10`
- [ ] Info rows use `bg-white/5 rounded-lg` pattern
- [ ] Action buttons in info sections use `.info-card-button`
### BANNED — Flag These as Violations
- [ ] No `.gradient-button` anywhere (replace with `.glass-button`)
- [ ] No `.gradient-card` / `.gradient-card-dark` (replace with `.glass-card` or `.path-option-card`)
### NO Inline Tailwind
- [ ] Check for long `class="..."` strings with layout/color utilities
- [ ] Extract to semantic classes in `neode-ui/src/style.css`
- [ ] Name classes semantically: `.app-card`, `.status-badge`, `.nav-item`
### Color Compliance
- [ ] Primary text: `text-white/90` (not `text-white` or arbitrary opacity)
- [ ] Muted text: `text-white/60` to `text-white/70`
- [ ] Backgrounds: `rgba(0,0,0,0.60)` with `backdrop-filter: blur(24px)`
- [ ] Borders: `rgba(255,255,255,0.18)` standard
- [ ] Status colors: green=#4ade80, red=#ef4444, yellow=#facc15, blue=#3b82f6, orange=#fb923c
### Typography
- [ ] Font: Avenir Next (body), Montserrat (headings via `font-archipelago`)
- [ ] H1: text-3xl font-bold, H2: text-2xl font-semibold, H3: text-xl font-semibold
- [ ] Body: text-base, Small: text-sm, Labels: text-xs
### Interaction States
- [ ] Hover: `translateY(-2px)` lift + background brighten + enhanced shadow
- [ ] Active: `translateY(1px)` press
- [ ] Selected: brighter background + glow shadow + enhanced gradient border
- [ ] Disabled: reduced opacity (~50%), no pointer events
- [ ] Loading: spinner SVG + descriptive text, button disabled
- [ ] Focus-visible: soft blue glow `rgba(120, 180, 255, 0.2)`
### Transitions
- [ ] Standard: `all 0.3s ease`
- [ ] All interactive elements have transitions (no jarring state changes)
- [ ] Respect `prefers-reduced-motion`
### Spacing
- [ ] 4px grid system (p-1=4px, p-2=8px, p-3=12px, p-4=16px)
- [ ] 16px default padding on cards
- [ ] Consistent gap values between grid items
### Responsive
- [ ] Mobile: single column, reduced padding, touch targets >= 44x44px
- [ ] Tablet (md:): two columns
- [ ] Desktop (lg:): three columns, full effects
### Accessibility
- [ ] Semantic HTML (`<button>`, `<nav>`, `<main>`, not div soup)
- [ ] ARIA labels on icon-only buttons
- [ ] Keyboard navigable (Tab order, Enter to activate, Esc to close)
- [ ] Color contrast WCAG AA (4.5:1 normal text, 3:1 large)
- [ ] Images have alt text (decorative: `alt=""`)
### Icons
- [ ] Stroke-based SVGs, stroke-width 2.5 default
- [ ] Color: `text-white/85` default, `text-white` on hover
- [ ] Drop-shadow filter applied on interactive icons
- [ ] Size: w-5 h-5 standard, w-4 h-4 small
## Service UI Review (if reviewing docker/*-ui/)
- [ ] Uses `.glass-card` for main sections
- [ ] Uses `.info-card` for status (no hover)
- [ ] Uses `.info-card-button` for actions (with hover)
- [ ] Uses `bg-white/5` for info rows
- [ ] Header: logo + title + description + status
- [ ] Background image loads correctly
- [ ] Mobile responsive
Report violations with file paths and specific fixes.