From f3174bb55d043f7bf5e633ccdcec1bf082363d8e Mon Sep 17 00:00:00 2001 From: Joey Spiker Date: Mon, 27 Apr 2026 10:58:52 -0400 Subject: [PATCH] added readme, for information about the projects --- README.md | 1913 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1913 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..cbd2526 --- /dev/null +++ b/README.md @@ -0,0 +1,1913 @@ +# SpikerSoft Platform + +A travel-based assisted learning platform that fundraises for children's global exploration. SpikerSoft bridges outdoor adventure with modern technology -- structured education meets hands-on STEM, where families explore the world together and sponsors fund the journey. + +Think Boy Scouts meets Geek Squad in the jungle. + +## Platform Overview + +SpikerSoft is a mono-repo whose **product** stack is these two repositories: + +``` +SpikerSoft Solution/ +├── spikersoft-angular/ # Frontend - Angular 21 Nx workspace +└── spikersoft-backend/ # Backend - .NET 10 C# solution +``` + +Other sibling directories in this workspace (if present), such as experimental games or tooling, are **not** part of that core pair unless noted in their own README. + +### Frontend (spikersoft-angular) + +An Nx monorepo Angular application providing: + +- **Integrated Game Ecosystem** - Three interconnected web-based games (Space, Voxel, Dungeon Crawler) forming a single MMO experience, plus standalone learning games (Chess, Fishing, puzzle games) +- **Visual Programming Tools** - Blockly and Rete.js node-graph editors for programming in-game robots and spacecraft, doubling as real STEM learning tools +- **3D Visualization** - Three.js and WebGL-powered immersive experiences +- **Developer Tools** - JSON editors, encoding utilities, visual flowcharts, SQL trainers, and a 95-lesson C# Coding Curriculum that compiles and grades student code either server-side or fully in-browser via a WebAssembly Roslyn runtime (works offline) +- **Geography & Travel** - Interactive world maps, country exploration, travel planning tied to real-world adventures +- **Sponsorship & Fundraising** - Stripe-powered donation system connecting donors with children's travel experiences +- **Real-time Communication** - PeerJS/WebRTC video calling, OvenPlayer live streaming, SignalR chat and notifications +- **Team & Hiring** - Dynamic staff profiles and data-driven open positions (ambassadors auto-generated per location) + +**Tech Stack:** +- Angular 21 with standalone components +- Three.js for 3D graphics +- RxJS for reactive state management +- SignalR for real-time updates +- Keycloak authentication integration + +### Backend (spikersoft-backend) + +A multi-project .NET 10 solution providing: + +- **REST API** - Main platform API with CQRS pattern (MediatR) +- **Game Server** - Purpose-built real-time multiplayer server +- **Event Handlers** - Distributed microservices for async processing +- **AI Services** - LLamaSharp-powered ML capabilities + +**Tech Stack:** +- .NET 10 / C# 14 +- MongoDB (sharded cluster) - Primary database +- Redis (cluster) - Caching and pub/sub +- RabbitMQ - Message queue +- SignalR - Real-time communication +- Keycloak - Identity management +- Docker Swarm - Container orchestration + +--- + +## Architecture Diagram + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ CLIENTS │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ Angular Web App │ │ Native iOS / │ │ Mobile PWA │ │ +│ │ (Browser) │ │ Android (Future)│ │ (Future) │ │ +│ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ │ +└───────────┼─────────────────────┼─────────────────────┼─────────────────────┘ + │ HTTPS/WSS │ WebSocket │ + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ TRAEFIK (Load Balancer) │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ SERVICES │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ SpikerSoft.API │ │ SpikerSoft.Game │ │ Event Handlers │ │ +│ │ (REST + SignalR)│ │ Server (30Hz) │ │ (RabbitMQ) │ │ +│ │ │ │ │ │ │ │ +│ │ • Auth │ │ • Zone Manager │ │ • Embeddings │ │ +│ │ • CRUD APIs │ │ • Space Zones │ │ • File Movement │ │ +│ │ • Chat Hubs │ │ • Voxel Zones │ │ • Book Mgmt │ │ +│ │ • Sponsor API │ │ • Camp Zones │ │ • Code Execution│ │ +│ │ • Lessons API │ │ • Robot Engine │ │ (Roslyn) │ │ +│ │ • Geography API │ │ • Combat System │ │ • Image Desc. │ │ +│ │ • Notifications │ │ │ │ │ │ +│ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ │ +└───────────┼─────────────────────┼─────────────────────┼─────────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ DATA LAYER │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ MongoDB Cluster │ │ Redis Cluster │ │ RabbitMQ │ │ +│ │ (Sharded) │ │ (6 nodes) │ │ (Message Queue) │ │ +│ │ │ │ │ │ │ │ +│ │ • Users │ │ • Session Cache │ │ • Task Queues │ │ +│ │ • Game State │ │ • Rate Limiting │ │ • Event Bus │ │ +│ │ • Content │ │ • SignalR │ │ • DLQ Support │ │ +│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ IDENTITY │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ ┌──────────────────────────────────────────────────────────────┐ │ +│ │ Keycloak v26 (OAuth2 / OIDC) │ │ +│ │ • JWT Token Issuance │ │ +│ │ • Multi-tenant Organization Support │ │ +│ │ • SSO Integration │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Game Server Architecture + +The `SpikerSoft.GameServer` is a purpose-built, high-performance real-time game server powering all three integrated game experiences through a unified backend. + +### Key Features + +- **30 Hz Fixed Timestep** - Deterministic game loop for consistent simulation +- **Hybrid Transport** - WebSocket for browsers, LiteNetLib (UDP) for native clients +- **Server-Authoritative** - Anti-cheat by design, server owns game state +- **Command-Based Sync** - Client prediction with server reconciliation +- **Persistent Zones** - Shared world state like RuneScape/EverQuest +- **Full Persistence** - Character progress saved permanently +- **Robot Execution Engine** - Server-side programmable robot instruction processing + +### Capacity Targets + +- 200-500 concurrent players per zone +- ~5,000 robot entities per zone (250 clients x ~20 robots each) +- Multiple zones per server instance +- Spatial hashing for O(1) proximity queries + +### Integration + +The game server integrates with the existing SpikerSoft ecosystem: + +``` +SpikerSoft.GameServer/ # Game server executable + ├── Zones/ + │ ├── SpaceZone # Orbital mechanics, spacecraft, docking + │ ├── VoxelZone # Block world, robot management, terraforming + │ └── CampZone # Dungeon crawler RPG, stronghold PvE + ├── Services/ + │ ├── RobotExecutionEngine # Server-side robot program interpreter + │ └── MongoRobotProgramRepository # Robot program persistence + ├── References: + │ ├── SpikerSoft.Common # Shared models (Commands, Events, Entities) + │ ├── SpikerSoft.Data # MongoDB persistence + │ └── SpikerSoft.Business # Game services, maze generation +``` + +--- + +## Integrated Game Ecosystem + +Three Angular game components share the same `SpikerSoft.GameServer` backend and form a single interconnected web-based MMO. Players transition seamlessly between experiences -- orbiting in space, terraforming planets with voxel blocks, and defending strongholds in dungeon combat -- all tied together through shared characters, resources, and progression. + +``` +┌────────────────┐ ┌──────────────────┐ ┌────────────────────┐ +│ Space Game │────▶│ Voxel World │────▶│ Dungeon Crawler │ +│ (ship/orbit) │ │ (terraform/build)│ │ (stronghold PvE) │ +└────────────────┘ └──────────────────┘ └────────────────────┘ + │ │ │ + └──────────────┬───────┘─────────────────────────┘ + ▼ + SpikerSoft GameServer + (WebSocket / MessagePack / MongoDB) +``` + +### Space Game + +A real-time orbital mechanics simulation where players pilot spacecraft, manage fleets of drones, mine asteroids, and navigate between celestial bodies. The space game serves as the entry point to planetary exploration -- when you land on a terraformed planet or asteroid, you transition into the voxel world. + +- Server-authoritative physics with thruster simulation, docking, and autopilot +- Spacecraft management with ownership enforcement +- Beam weapons, mines, projectile combat, and radar/stealth systems +- All UI rendered as native WebGL overlays for performance +- Procedural planet and asteroid generation + +### Voxel World (Minecraft Port) + +A multiplayer voxel sandbox built on a TypeScript Minecraft port, connected to SpikerSoft's backend for server-authoritative multiplayer. Players terraform planets, build strongholds, and deploy programmable robots to automate resource gathering and construction. + +- Full block world with chunk-based terrain, mining, and building +- Multiplayer via SpikerSoft GameServer (character sync, block changes) +- Programmable robot units (gatherer, builder, scout, combat) with visual programming +- In-world real-time debugging with 3D path visualization and step-through execution + +### Dungeon Crawler RPG + +An EverQuest-inspired multiplayer RPG with Ultima Online-style ruleset. Players explore procedurally generated dungeons, fight bosses, and ultimately assault other players' strongholds built in the voxel world. The pilot of a spacecraft can serve as the player character. + +- Server-authoritative combat with spells, potions, and equipment +- Procedural maze/dungeon generation with boss encounters +- Zone-based world with camp, dungeon, and stronghold areas +- Character persistence with level progression and inventory + +### Cross-Game Integration + +| Feature | Space | Voxel | Dungeon | +|---------|-------|-------|---------| +| Characters | Ships/spacecraft | Avatars + robots | RPG characters | +| Entry point | Launch from orbit | Land on planet | Enter stronghold | +| Resources | Asteroid mining | Block harvesting | Loot drops | +| Ownership | Spacecraft per player | Robots per player | Characters per player | +| Entity filtering | `ListShipsCommand` | `ListUnitsCommand` (voxel zones) | `ListCharactersCommand` (camp/dungeon zones) | + +--- + +## Robot & Visual Programming + +The robot system brings SpikerSoft's educational mission into the game world. Players visually program robots using the same Blockly and Rete.js tools available in SpikerSoft's developer toolbox, creating a bridge between gameplay and real STEM learning. + +### How It Works + +1. **Deploy** a robot from your spacecraft onto a planet surface +2. **Program** it using Blockly (drag-and-drop blocks) or Rete (node graph editor) -- directly in the game world +3. **Compile** the visual program into a typed instruction list (client-side) +4. **Upload** the instructions to the server for secure execution +5. **Debug** with real-time 3D visualization -- glowing pathfinding lines, floating instruction labels, energy bars, and ghost block previews + +### Instruction Set + +Programs compile to a typed instruction set (not arbitrary code) for security and scalability: + +| Category | Instructions | +|----------|-------------| +| Movement | `MoveTo`, `MoveForward`, `Turn` | +| World | `Mine`, `Place`, `Scan` | +| Control | `Branch`, `Loop`, `Wait`, `CallSubroutine` | +| Variables | `SetVariable`, `GetVariable`, `Compare`, math ops | +| Communication | `Broadcast`, `OnMessage`, `TransferItem` | + +### RAM as a Game Mechanic + +Each robot has a RAM capacity (a fun, thematic resource constraint) that limits how complex its program can be. Simple gather-and-return loops fit in a small bot; complex multi-step build routines require upgraded hardware. This teaches resource-awareness and code optimization. + +### Encapsulation + +Once a player writes a working routine (e.g., "gather moon rocks and return to base"), they can save it and reuse it as a single block/node in future programs. This mirrors real software engineering -- building abstractions from tested components. + +### Multi-Robot Coordination + +Robots communicate via broadcast channels, enabling fleet-level behaviors: + +- A scout broadcasts "ore found at (x,y,z)" on the `resources` channel +- Gatherer robots listen on `resources` and navigate to the coordinates +- Builders listen for "materials delivered" and begin construction +- Robots can transfer items to each other when within 2 blocks + +### Visual Programming Tools + +The same Blockly and Rete editors available at `/tools/(tools:blockly)` and `/tools/(tools:diagram)` power robot programming. When opened in "robot mode" within the voxel game, they load robot-specific blocks/nodes instead of general-purpose ones. This dual use means skills learned programming robots transfer directly to the developer tools, and vice versa. + +### Mobile Readiness + +- Touch-friendly UI with 44px minimum touch targets +- Protocol version negotiation for native iOS/Android clients +- Offline program editing with localStorage cache and dirty-tracking sync + +--- + +## Coding Curriculum & Playground (C#, Python, and JavaScript) + +SpikerSoft's flagship coding-education tools run three parallel tracks: + +- **C# Playground** — 95-lesson curriculum graded by **Roslyn** server-side and **.NET WebAssembly** in the browser. +- **Python Playground** — ~75-lesson curriculum graded by **CPython subprocess** server-side and **Pyodide** in the browser. +- **JavaScript Playground** — **134-lesson** curriculum (8 orientation tutorials + 126 graded challenges across 17 tiers) graded by **Node** server-side and **QuickJS-WASM** in the browser. Same harness contract as Python (`runTests()` returning `PASS:`/`FAIL:`/`ERROR:` strings); offline + Run-locally + batch re-verification all work the same way. Every challenge ships with a reference solution that's executed end-to-end through Node by `JavaScriptChallenge_ReferenceSolutionPasses`. + +All three tracks share the same lesson UI (the language-agnostic `LanguageRunner` Angular component), the same attempt/progress APIs, the same PASS/FAIL line protocol, and the same offline-first PWA model. A student can start a lesson at home, finish it on a campus bus with no signal, and see their unlocks update the moment connectivity returns. The JavaScript Playground is reachable at `/tools/(tools:javascript-playground)`, Python at `/tools/(tools:python-playground)`, and C# at `/tools/(tools:csharp-playground)`. + +Backend dispatch is strategy-based: `ILessonGradingExecutorFactory` picks Roslyn / CPython / Node per `LessonGradingRuntime`, `IFreePlayCodeExecutorFactory` picks the per-language free-play executor from request metadata, and `IStudentCodeHintAnalyzerFactory` returns the language-appropriate hint analyzer (`StudentCodeHintAnalyzer` for C#, `PythonStudentCodeHintAnalyzer` for Python, `JavaScriptStudentCodeHintAnalyzer` for JavaScript). A future TypeScript curriculum plugs into the same JS runtime via the new `LessonMetadata.Preprocessor = LessonPreprocessor.TypeScript` flag — no second runtime needed. + +### Python curriculum tiers + +Python lessons live under [`SpikerSoft.Business/Domain/Lessons/Curriculum/Python/`](spikersoft-backend/SpikerSoft.Business/Domain/Lessons/Curriculum/Python/) and mirror the C# tier structure plus a Python-only `TierP1_Pythonic` for idioms that have no C# parallel: + +| Tier | Numbers | Topic | +|------|---------|-------| +| `Tier00_Welcome` | 20001-20006 | Welcome tutorials (intro, modules, functions, print, first submit) | +| `Tier01_Foundations` | 20100-20107 | Hello World, variables (int, float, bool, str, None) | +| `Tier02_Operators` | 20200-20206 | Arithmetic, comparison, logical, conditional expression, f-strings | +| `Tier03_ControlFlow` | 20300-20307 | if / elif / else, `match`/`case`, dict dispatch, for / while / iteration | +| `Tier04_Functions` | 20400-20405 | return, multi-param, keyword args, defaults, lambda | +| `Tier05_Collections` | 20500-20506 | list, append, iteration, dict, set, nested lists, tuple | +| `Tier06_Strings` | 20600-20604 | slicing, split/join, replace/case, `''.join` accumulation, try/except `int()` | +| `Tier07_Classes` | 20700-20707 | class, `__init__`, methods, `@property`, `@staticmethod`/`@classmethod`, `__repr__`, `_private`, `self` | +| `Tier08_Inheritance` | 20800-20805 | base/derived, override, `abc.ABC`, duck typing, multiple inheritance, `super()` | +| `Tier09_Generics` | 20900-20902 | `TypeVar`, generic class (`Stack[T]`), bounded generics | +| `Tier10_Exceptions` | 21000-21003 | try/except, finally, custom exception, context manager (`with`) | +| `Tier11_FunctionalAndComprehensions` | 21100-21105 | filter / map list comps, `sorted(key=)`, group-by, `any`/`all`/`next`, generator expressions | +| `Tier12_Callables` | 21200-21203 | callable, lambda + `filter`, closures, decorators | +| `Tier13_Patterns` | 21300-21302 | tuple patterns, class patterns, `@dataclass(frozen=True)` | +| `Tier14_OptionalAndNone` | 21400-21402 | None as sentinel, `or` defaults, `typing.Optional` | +| `Tier15_Async` | 21500-21503 | coroutines, async/await, `asyncio.gather`, cancellation | +| `Tier16_Advanced` | 21600-21604 | `*args`/`**kwargs`, generators (`yield`), tuple unpacking, `__getitem__`, dunder operator overloading | +| `TierP1_Pythonic` | 21700-21704 | f-string format spec, dict / set comprehensions, `enumerate`+`zip`, advanced slicing | + +### JavaScript curriculum tiers + +JavaScript lessons live under [`SpikerSoft.Business/Domain/Lessons/Curriculum/JavaScript/`](spikersoft-backend/SpikerSoft.Business/Domain/Lessons/Curriculum/JavaScript/) and use the **30000–31999** lesson-number range with `DisplayNumber = LessonNumber - 30000` so the sidebar shows clean 1..N badges. See [`.cursor/rules/javascript-curriculum.mdc`](.cursor/rules/javascript-curriculum.mdc) for the locals-first / Node↔QuickJS parity rules: + +| Tier | Numbers | Topic | +|------|---------|-------| +| `Tier00_Welcome` | 30001-30008 | Welcome tutorials (history of JS, engines, browser vs Node, console basics, code style, reading errors, first submit) | +| `Tier01_Foundations` | 30100-30109 | console.log, numbers, strings, booleans, null/undefined, typeof, let vs const, template literals, multi-line | +| `Tier02_Operators` | 30200-30207 | Arithmetic, `===`/`!==`, logical, ternary, modulo, ++/--, compound assignment, concat-vs-template | +| `Tier03_ControlFlow` | 30300-30308 | if/else, switch, for, for-of, for-in, while, do-while, break/continue | +| `Tier04_Functions` | 30400-30409 | Declaration vs expression, arrow, defaults, rest, spread, early return, purity, closures, hoisting, higher-order | +| `Tier05_Arrays` | 30500-30508 | literal/index, push/pop, length, slice/splice, concat/spread, indexOf/includes, sort, join/split, Array.from/of | +| `Tier06_Strings` | 30600-30607 | length/index, case, slice, includes, replaceAll, split, trim, padStart | +| `Tier07_Objects` | 30700-30708 | literal, dot/bracket, mutation, keys/values/entries, destructuring, shorthand, spread/assign, JSON, nested | +| `Tier08_Iteration` | 30800-30808 | forEach, map, filter, reduce, find, some/every, flat/flatMap, chaining, sort comparator | +| `Tier09_Classes` | 30900-30908 | class, ctor, methods, getters/setters, static, this rules, arrow this, bind/call/apply, #private | +| `Tier10_Inheritance` | 31000-31005 | extends/super, override, super in methods, instanceof, mixins, Object.create | +| `Tier11_Modules` | 31100-31104 | IIFE module, Symbol.iterator, Symbol keys, Object.freeze, namespace export | +| `Tier12_Errors` | 31200-31205 | throw/catch, finally, custom Error, name+message, re-throw, async errors | +| `Tier13_Async` | 31300-31309 | callbacks, Promise basics, .then chains, resolve/reject, async, await, all/race/allSettled, sequential vs parallel | +| `Tier14_Modern` | 31400-31407 | optional chaining, nullish coalescing, Map, Set, WeakMap, generators, for-of generator, generator iterator | +| `Tier15_Patterns` | 31500-31504 | revealing module, observer (event bus), factory, strategy, singleton | +| `Tier16_Performance` | 31600-31604 | memoization, debounce, throttle, avoiding O(n²), lazy generators | + +### Dual execution + +| Path | Where | When | Latency | +|------|-------|------|---------| +| **Server (default)** | `SpikerSoft.EventHandlers.CodeExecution` Docker worker | Online and the user has not enabled "Run locally" | ~150-400 ms (RabbitMQ round-trip + Roslyn / CPython / Node subprocess) | +| **Browser (Roslyn WASM)** | `SpikerSoft.Wasm` runtime in a Web Worker | C# only; offline OR user toggled "In-browser compile" | ~30-60 ms warm; first boot 8-15 s | +| **Browser (Pyodide)** | `/assets/vendor/pyodide/` loaded by `PyodideRuntimeService` | Python only; offline OR user toggled "Run locally (Pyodide)" | ~50-150 ms warm; first download ~10 MB | +| **Browser (QuickJS)** | `/assets/vendor/quickjs/quickjs.global.js` loaded by `QuickJsWorkerClient` | JavaScript only; offline OR user toggled "Run locally (QuickJS)" | ~5-30 ms warm; first download ~2.3 MB (single-file UMD with WASM inlined) | + +Frontend state (runtime downloads cached by service worker, lesson catalog cached in IndexedDB, queued progress flushed on reconnect) means once a student loads a lesson online they can keep working offline indefinitely. + +### Worker container Python + Node layer + +The worker image installs `python3` (Debian default) and a **Node.js 20.x LTS** via the official NodeSource setup script (Debian's bundled `nodejs` package is too old for some lesson features). Both interpreter paths are pinned explicitly: + +```dockerfile +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl ca-certificates gnupg python3 \ + && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* \ + && test -x /usr/bin/python3 && /usr/bin/python3 --version \ + && test -x /usr/bin/node && /usr/bin/node --version +ENV CodeExecution__PythonExecutable=/usr/bin/python3 +ENV CodeExecution__NodeExecutable=/usr/bin/node +``` + +Two startup probes (`PythonInterpreterProbeHostedService` and `NodeInterpreterProbeHostedService`) each run `--version` once and log the resolved path so a misconfigured deploy fails loudly. **Pin worker images away from `:latest`** so Swarm picks up new layers reliably (the Python + Node installs live in the runtime stage of the Dockerfile). + +> **WASM coupling note.** [`SpikerSoft.Wasm.csproj`](spikersoft-backend/SpikerSoft.Wasm/SpikerSoft.Wasm.csproj) link-includes `Domain/CodeExecution/Execution/**/*.cs` from `SpikerSoft.Business`, but excludes server-only files via a list (Python*, JavaScript*, executor/factory abstractions). When you add a new server-only executor, add its file pattern to that exclude list — otherwise the WASM build pulls in `IOptions` / `ILogger` / `Microsoft.Extensions.Services` that the WASM project deliberately doesn't reference, and you'll see a wall of "type or namespace not found" errors. + +--- + +## C# Coding Curriculum & Playground + +The C# Playground is SpikerSoft's flagship coding-education tool — a 95-lesson curriculum that teaches C# from "Hello, World" through async/await, LINQ, and operator overloading. It runs both online (server-graded) and offline (browser-graded) with progress preserved across the boundary, so a student can start a lesson at home, finish it on a campus bus with no signal, and see their unlocks update the moment connectivity returns. + +### Curriculum Structure + +95 lessons grouped into 17 progressive tiers. Each lesson is gated behind explicit prerequisites and unlocks the next as it is completed: + +| Tier | Theme | Sample Topics | +|------|-------|---------------| +| `Tier00_Welcome` | Orientation | Hello World, Anatomy of a Class, Anatomy of a Method | +| `Tier01_Foundations` | Variables | int, double, bool, string, var | +| `Tier02_Operators` | Math & comparison | Arithmetic, comparison, logical, string interpolation | +| `Tier03_ControlFlow` | Branching & loops | if/else, switch expressions, for/while/foreach | +| `Tier04_Methods` | Functions | void/return, multi-param, out/ref params | +| `Tier05_Collections` | Data structures | Arrays, `List`, `Dictionary`, 2D arrays | +| `Tier06_Strings` | Text manipulation | String methods, `StringBuilder`, `TryParse` | +| `Tier07_Classes` | OOP basics | Class definition, constructors, properties, validation | +| `Tier08_Inheritance` | Polymorphism | base/derived, virtual/override, single & multiple interfaces | +| `Tier09_Generics` | Type parameters | Generic methods, generic classes, constraints | +| `Tier10_Exceptions` | Error handling | try/catch/finally, `using` / `IDisposable` | +| `Tier11_Linq` | Querying | Select, OrderBy, Any/All/First, query syntax | +| `Tier12_Delegates` | Function references | `delegate`, `Action`/`Func`, lambdas | +| `Tier13_Patterns` | Modern C# | Pattern matching, records | +| `Tier14_Nullability` | Null safety | Nullable reference types | +| `Tier15_Async` | Concurrency | async/await | +| `Tier16_Advanced` | Power features | params arrays, operator overloading | + +Each lesson is a server-side `LessonStrategyBase` C# class that owns: + +- **`StarterCode`** — what the student sees on first visit +- **`TutorialPanels`** — interactive, narrated walk-throughs presented before the gradable challenge +- **`TestCode`** — the grader (executes student code + asserts on output, exit code, exceptions) +- **`Prerequisites`** — the lesson numbers that must be completed first (drives the sidebar lock state) +- **`Hints`** — heuristic checks that surface common errors *before* Roslyn even runs + +Lesson definitions are the single source of truth. A `LessonCatalogHydrationService` reads them on backend boot and writes the catalog to MongoDB's `lessons` collection. The same `.cs` files are link-included by the `SpikerSoft.Wasm` project so the browser grades with byte-identical logic. + +### Two Execution Paths, One UX + +| Path | Where code runs | When it's used | Latency | +|------|-----------------|----------------|---------| +| **Server (default)** | `SpikerSoft.EventHandlers.CodeExecution` Docker worker, 4 concurrent consumers reading from `code.execution.requests`, results published to `code.execution.responses` | Online and the user has not enabled "Run compiles locally" | ~150–400 ms (RabbitMQ round-trip + Roslyn) | +| **Browser (WASM)** | `SpikerSoft.Wasm` runtime hosted in a Web Worker, Roslyn 5.x compiled and run in-browser | Offline, **or** the user toggled "Run compiles locally" on, **or** the server is unreachable | ~30–60 ms warm; first boot 8–15 s | + +`CSharpRunnerService` picks a path automatically and re-attempts on the alternate path if the chosen one fails — for example, a momentary network blip during a server submit silently re-grades against the WASM runtime so the student never sees an error. + +### Browser-Side Grader (`SpikerSoft.Wasm`) + +A .NET 10 `Microsoft.NET.Sdk.WebAssembly` project that ships Roslyn + the lesson strategies + the same `RoslynCodeExecutor` the server uses, exposed to JavaScript via `[JSExport]`: + +| Export | Purpose | +|--------|---------| +| `Ping()` | Readiness probe | +| `GetAttempt(lessonNumber)` | Issues a single-use compile token (mirrors the server-side Redis token model, but in-memory) | +| `ExecuteLessonCode(json)` | Compiles + runs + grades a lesson submission | +| `ExecuteFreePlayCode(json)` | Compiles + runs free-form code (no grading, no token) | +| `AddReferenceImage(byte[])` | Fed by `main.js` after boot so Roslyn has framework PE bytes to compile against | + +Important runtime constraints baked into the executor: + +- **WebCIL is disabled** (`false`) so the same downloaded assembly bytes are valid PE images Roslyn can use as `MetadataReference.CreateFromImage` references — no unwrapping step required. +- **`ConcurrentBuild` is forced off** under `OperatingSystem.IsBrowser()` because Mono's WASM runtime has no monitor wait support; Roslyn's parallel compile path deadlocks instantly without this guard. +- **Invariant globalization** drops ICU (~5 MB saving). Lesson `TestCode` uses ordinal comparisons. + +Bundle stats: ~7 MB Brotli-compressed, one-time download, then cached by the Angular service worker (lazy strategy). Visitors who never enable the toggle pay zero download cost. + +### Lesson Sidebar & Progress + +The `LessonSidebar` Angular component renders the curriculum and computes lock/checkmark state client-side from two signals: `lessons` (the catalog) and `completedLessons` (the user's progress). A lesson unlocks when **all** of its `Prerequisites` are present in the completed set. + +Progress reconciliation strategy (added to fix a real-world stale-state bug): + +- **Optimistic on submit** — `completedLessonsSignal` updates the moment a lesson grades green, so the next lesson unlocks without waiting for the server round-trip. +- **Forced refresh after a passing submit** — both catalog and progress are re-fetched with throttle bypassed, ensuring server-side prereq edits land immediately and the lesson catalog stays accurate. +- **Forced refresh after tutorial completion** — same mechanism, so newly unlocked downstream lessons appear immediately when the student finishes the last tutorial panel. +- **Tab-focus refresh** — when the browser tab regains focus (e.g., after a long offline session, or after switching back from another app), a throttled refresh runs (15 s minimum interval) so multi-device or multi-session progress catches up automatically without F5. +- **Union semantics** — server progress and local optimistic updates are merged via `Set` union, never overwritten. This eliminates a race where a slow `progressSync.enqueue` round-trip could re-lock a just-completed lesson if the server refresh landed before the persist call did. +- **Service-worker freshness** — `/api/Lessons/progress` uses the `freshness` strategy (network-first with 3 s timeout, cache fallback) so the very first paint after launching the app is always current rather than serving a 10-minute-old cached response. + +### Offline Mode + +When the user disconnects or `navigator.onLine === false`: + +1. The runner auto-routes new submissions through the WASM grader. +2. Successful completions are queued in IndexedDB by `progress-sync.service`. +3. On reconnect, the queue flushes to `POST /api/Lessons/progress/batch` (batched items, each with `lessonNumber` + `attemptToken`). +4. The next online catalog refresh confirms server-side persistence and merges any progress made on other devices. + +Free-play code (anything outside a lesson) also runs against the WASM runtime when offline — students never lose access to the playground because the network blipped. + +### Pre-Flight WASM Cache (Site Settings → Offline) + +The WASM bundles that power the playgrounds and games are downloaded **lazily** on first use. That's great for visitors who only ever read blogs, but it's a problem for the user about to board a flight who wants to keep grading Python lessons or playing the SQL Clue game without wifi. + +The **Site Settings** page (under the user profile, accessible via the menu's "Site Settings" entry, replacing the older "Appearance" link) carries an **Offline** section that lets users explicitly preload every WASM-backed runtime ahead of time and verify, at a glance, which ones are already cached. + +| Module | Ships with | Used for | +|---|---|---| +| **Pyodide** | `/assets/vendor/pyodide/pyodide.asm.wasm` + loader | Python playground, Python lessons (offline grading) | +| **.NET WASM** | `main.js` + `_framework/dotnet.js` + every hashed name in the embedded boot manifest (for example `dotnet.native.*.wasm`, Roslyn DLLs) | C# playground, C# lessons (offline grading) | +| **DuckDB** | `/assets/duckdb-wasm/duckdb-mvp.wasm` + worker | SQL playground, **Clue for SQL** game | +| **QuickJS** | `/assets/vendor/quickjs/quickjs.global.js` (single-file UMD with WASM inlined) + classic worker | JavaScript playground, JavaScript lessons (offline grading); future TypeScript curriculum reuses this same runtime via `LessonPreprocessor.TypeScript` | + +Per-module UI: + +- **Status badge** — *Ready offline*, *Partially cached*, *Not cached*, *Downloading…*, *Verifying…*, or *Error*. Computed by hitting `caches.match(url)` for each *primary* asset; sidecar files (lockfiles, secondary worker variants) don't drag the status to "partial" if missing. +- **Preload button** — Issues `fetch(url, { cache: 'reload' })` for every asset, then `cache.put` into a dedicated named cache (`spikersoft-offline-preload-v1`) so status checks succeed even when the Angular service worker is off (typical `ng serve`). When the SW is on, entries exist in both places. Re-checks status on completion. Idempotent (the label switches to "Re-preload" for a forced refresh). For **.NET**, the URL list is not hard-coded: `OfflineCacheService` parses the JSON embedded in `_framework/dotnet.js` after each `dotnet publish`, so hashed `_framework/*` names stay in sync with the bundle on disk. +- **Preload everything** — Sequential per-module so a slow connection isn't asked to download 50 MB at once; the user gets per-module progress feedback either way. +- **First use vs preload** — Preload answers “are the bytes on disk?” The C# playground’s first in-browser run still **starts** the Web Worker and initializes Mono + WebAssembly (CPU work, not a second full download). That one-time cost often lands around one second even when every DLL was pre-cached; later runs stay in the tens of milliseconds. Turn **Verify by initializing** on for the .NET module if you want that startup to happen on the settings page instead of on first Run. +- **Verify by initializing** — Optional advanced toggle (defaults off). When on, after each module's bytes finish downloading the section asks the runtime to actually *boot* and prove it runs in this browser: + - **Pyodide** loads via `PyodideRuntimeService.getPyodide()` and round-trips `1+1` through `runPythonAsync` so any JS↔WASM bridge issue surfaces immediately. The runtime stays resident, so the next playground use is warm. + - **.NET WASM** probes the bundle via `WasmRuntimeService.isBundleAvailable()` (gives a friendly "run dotnet publish" error when the bundle was never built) before booting the worker via `ensureReady()`. Same residency benefit as Pyodide. + - **DuckDB** lazy-imports `@duckdb/duckdb-wasm`, instantiates a throwaway engine in a fresh worker, runs `SELECT 1`, then tears the worker down — no shared singleton to keep alive (each consumer of DuckDB owns its own `AsyncDuckDB`), so the verify path doesn't leak memory. + + Verifier registration happens lazily in `WasmVerifierBootstrap.registerAll()` when the panel renders, not at app startup — visitors who never open Site Settings don't pay any DI cost for the runtime services. + +The registry (`projects/spikersoft/src/app/_services/offline-cache/wasm-module-registry.ts`) is the single source of truth for what shows up in this panel. Adding a new WASM-backed feature later means appending one `WasmModuleDescriptor` to `DEFAULT_WASM_MODULES` — no UI changes required. Wiring an *initialize-time* verifier for the new module is one extra line in `WasmVerifierBootstrap.registerAll()`. + +### Tutorial Panels + +Before the gradable challenge, each lesson can present any number of `TutorialPanel`s — short, focused interactive segments authored in C# (so they live in source control alongside the lesson). The component records tutorial completion locally and forces a catalog refresh on the final panel so newly unlocked downstream lessons appear immediately. + +### Hint Analyzer + +`HintAnalyzer` (in `SpikerSoft.Business/Domain/CodeExecution/Hints`) runs heuristic pattern checks on student source *before* it reaches Roslyn. Common mistakes (missing `using`, wrong return type, null-check inversions, off-by-one in `for`-loop bounds) surface as friendly hints instead of cryptic compiler errors. The same analyzer runs server- and browser-side because the file is link-included by `SpikerSoft.Wasm`. + +### Activity Tracking + +Each meaningful interaction (`lesson-attempt`, `lesson-complete`, `tutorial-complete`, `free-play-run`, `path-failover`) emits an event to `ActivityTrackingService`, feeding the personalization layer (recommended next lessons, parent dashboards, learning streaks). + +### Building the WASM Bundle + +The bundle is **not committed** to source control — `spikersoft-angular/projects/spikersoft/src/assets/dotnet/` contains only its README. Build it locally with: + +```bash +# from spikersoft-backend/ +dotnet workload install wasm-experimental # one-time +dotnet publish SpikerSoft.Wasm -c Release # auto-flattens into ../spikersoft-angular/projects/spikersoft/src/assets/dotnet/ +``` + +The publish target can be overridden: `dotnet publish SpikerSoft.Wasm -c Release -p:AngularAssetsDotnet=/elsewhere/`. + +Rebuild whenever any of the following change: + +- A lesson strategy under `SpikerSoft.Business/Domain/Lessons/Curriculum/**` +- `RoslynCodeExecutor` or anything else under `SpikerSoft.Business/Domain/CodeExecution/Execution/` +- The hint analyzer +- `WasmCompilerEntry.cs` or the `[JSExport]` surface +- The `Microsoft.CodeAnalysis.CSharp` package version + +In CI the bundle is published by `spikersoft-backend/.gitea/workflows/spikersoft-wasm.yml` to a Gitea Generic Package (`git.spikersoft.com/spikerj/-/packages/generic/spikersoft-wasm`) and downloaded by the frontend pipeline before `pnpm run build`, so the deployed Docker image always ships with a current bundle. The frontend can pin to a specific build by setting the `WASM_VERSION` repository variable; defaults to `latest`. + +**Gitea Actions — `git` exit 128 on arm64 only:** The frontend workflow (`.gitea/workflows/main.yml`) runs a multi-arch Docker publish on `ubuntu-amd64` and `ubuntu-arm`. If `actions/checkout` fails with exit code 128 on the ARM runner while AMD64 succeeds, the cause is usually **runner configuration**, not the app repo: (1) **Wrong job image architecture** — the act runner’s default job container must be an **aarch64** image on ARM hosts; using an amd64-only image produces `invalid ELF header` when `git-remote-https` loads (see [actions/checkout#417](https://github.com/actions/checkout/issues/417) and related runner-image reports). Fix by setting the job image in act runner `config.yaml` to a multi-arch or `arm64v8` image, or use the host’s native arch. (2) **Network** — job containers that cannot reach `git.spikersoft.com` need `container.network: host` or the same Docker network as Gitea ([Gitea issue discussion](https://github.com/go-gitea/gitea/issues/27974)). (3) **Missing `git`** in the job image — checkout requires Git in PATH. The workflow also sets `http.version` to HTTP/1.1 and a larger `http.postBuffer` before clone to reduce flaky fetches. **Seeing the real error:** expand every collapsed log section in the UI and search for `fatal:` (the summary line is often generic). Re-run via *Run workflow* with input **`debug_git`** enabled to turn on `GIT_TRACE` and `GIT_CURL_VERBOSE` for that run. The **Git binary diagnostics** step prints `uname`/`file` for `git` and `git-remote-https` so a wrong CPU arch shows up as `invalid ELF header` before checkout. + +### Key Files + +| Layer | File | Purpose | +|-------|------|---------| +| **Lesson definitions** | `SpikerSoft.Business/Domain/Lessons/Curriculum/Tier{NN}_*/Lesson*.cs` | All 95 lessons (one class per lesson) | +| **Lesson base** | `SpikerSoft.Business/Domain/Lessons/LessonStrategyBase.cs` | Abstract base — StarterCode, TestCode, Prerequisites, TutorialPanels | +| **Catalog hydration** | `SpikerSoft.Business/Domain/Lessons/LessonCatalogHydrationService.cs` | Reads strategies on boot, upserts into Mongo `lessons` | +| **Roslyn executor** | `SpikerSoft.Business/Domain/CodeExecution/Execution/RoslynCodeExecutor.cs` | Shared compile + run engine; sequential build under WASM | +| **Hint analyzer** | `SpikerSoft.Business/Domain/CodeExecution/Hints/HintAnalyzer.cs` | Pre-Roslyn pattern checks | +| **Server worker** | `SpikerSoft.EventHandlers.CodeExecution/` | RabbitMQ-driven sandboxed grader (4 consumers) | +| **WASM project** | `SpikerSoft.Wasm/SpikerSoft.Wasm.csproj` | .NET 10 WebAssembly app; publishes to `assets/dotnet/` | +| **JS interop** | `SpikerSoft.Wasm/WasmCompilerEntry.cs` | `[JSExport]` shim for the browser worker | +| **Worker boot** | `spikersoft-angular/projects/spikersoft/src/assets/dotnet/main.js` | Pre-fetches managed assemblies, feeds PE bytes to Roslyn via `AddReferenceImage` | +| **Web Worker** | `libraries/tools/src/services/csharp-runner/wasm-runtime.worker.ts` | Hosts the .NET runtime on a worker thread | +| **Path-routing service** | `libraries/tools/src/services/csharp-runner/csharp-runner.service.ts` | Server↔WASM routing with auto-failover | +| **Runner component** | `libraries/tools/src/components/csharp-runner/csharp-runner.ts` | Lesson UI, sidebar, refresh orchestration | +| **Lesson sidebar** | `libraries/tools/src/components/csharp-runner/lesson-sidebar/` | Tier-grouped lesson list with prereq-driven lock state | +| **Offline queue** | `libraries/tools/src/services/csharp-runner/progress-sync.service.ts` | IndexedDB enqueue + flush-on-reconnect | +| **SW config** | `projects/spikersoft/ngsw-config.json` | `freshness` for `/api/Lessons/progress`, lazy for `/assets/dotnet/` | + +### API Endpoints + +| Method | Endpoint | Purpose | +|--------|----------|---------| +| `GET` | `/api/Lessons` | Lesson catalog (number, title, tier, prereqs, tutorial panels) | +| `GET` | `/api/Lessons/progress` | Caller's completed lesson numbers | +| `POST` | `/api/Lessons/progress/batch` | Apply batched progress (offline WASM queue flush); idempotent per `(userId, lessonNumber, attemptToken)` | +| `GET` | `/api/Lessons/{lessonNumber}/attempt` | Lesson attempt payload + single-use compile token (server path) | +| `POST` | `/api/Lessons/{lessonNumber}/complete-tutorial` | Mark a tutorial lesson complete (`LessonsController`) | +| `POST` | `/api/CSharpCodeRunner/lesson` | Submit code for grading via the server worker | +| `POST` | `/api/CSharpCodeRunner/run` | Run arbitrary C# (no grading; free-play in the UI) | + +All endpoints require authentication. **`POST /api/CSharpCodeRunner/*`** and **`GET /api/Lessons/{n}/attempt`** are unused when the student runs entirely on the WASM path — Angular uses the in-browser runtime and batches progress via **`POST /api/Lessons/progress/batch`** when back online. + +--- + +## Project Structure + +### Frontend (spikersoft-angular/) + +``` +spikersoft-angular/ +├── projects/ +│ └── spikersoft/ # Main application +│ └── src/app/ +│ ├── _components/ +│ │ ├── team/ # Staff profile cards (from Keycloak roles) +│ │ ├── employment/ # Hiring page with open positions + application dialog +│ │ ├── admin/ +│ │ │ ├── application-review/ # Admin review for ambassador applications +│ │ │ └── location-management/ # Admin CRUD for locations & positions +│ │ └── _games/ +│ │ ├── space-game/ # Orbital mechanics, spacecraft, fleet management +│ │ ├── wasm-voxel-game/ # Voxel world with robot programming +│ │ │ ├── robot-programming-panel/ # Blockly/Rete in-world editor +│ │ │ │ └── program-library/ # Save/load/share programs +│ │ │ └── fleet-panel/ # Multi-robot fleet management +│ │ ├── dungeon-crawler/ # Multiplayer RPG with stronghold PvE +│ │ ├── chess/ # Chess (local, AI, online multiplayer) +│ │ ├── fishing-game/ # Fishing simulation +│ │ └── ... # Puzzle & learning games +│ └── _services/ +│ ├── game-server/ # GameServerService (WebSocket, commands, events) +│ │ └── robot-program-cache.service.ts # Offline program editing +│ ├── location/ # LocationService (API-driven country list) +│ ├── position/ # PositionService (open positions + auto ambassadors) +│ └── team/ # TeamService (staff member profiles) +├── libraries/ +│ ├── keycloak-admin/ # Keycloak administration UI +│ ├── marks-site-models/ # Shared models for field-service customer management +│ ├── spikersoft-environment/ # Environment configuration library +│ ├── spikersoft-models/ # Shared TypeScript models +│ ├── spikersoft-theme/ # Glassmorphic theme and styling utilities +│ ├── tools/ # Developer & visual programming utilities +│ │ ├── blockly/ # Blockly editor + robot blocks +│ │ ├── diagram/ # Rete.js node editor + robot nodes +│ │ ├── ram-calculator.ts # Program RAM cost calculation +│ │ └── src/ +│ │ ├── components/csharp-runner/ # C# Playground UI (lessons, sidebar, free-play) +│ │ └── services/csharp-runner/ # Server↔WASM router, Web Worker, offline progress queue +│ └── wasm-voxel/ # TypeScript Minecraft port (voxel engine) +├── nx.json # Nx workspace config +└── package.json +``` + +### Backend (spikersoft-backend/) + +``` +spikersoft-backend/ +├── SpikerSoft.Api/ # Main REST API + SignalR hubs +├── SpikerSoft.GameServer/ # Real-time game server (30 Hz) +│ ├── Zones/ +│ │ ├── SpaceZone.cs # Orbital mechanics, spacecraft physics +│ │ ├── VoxelZone.cs # Block world, robot management +│ │ └── CampZone.cs # RPG zones (camp, dungeon, stronghold) +│ ├── Services/ +│ │ ├── RobotExecutionEngine.cs # Robot instruction interpreter +│ │ ├── MongoRobotProgramRepository.cs # Program persistence +│ │ └── GameLoopService.cs # 30Hz fixed-timestep loop +│ └── Network/ +│ ├── MessagePackSerializer.cs # Binary protocol (v2) +│ └── ConnectionManager.cs # WebSocket session management +├── SpikerSoft.Business/ # Business logic (CQRS + MediatR) +│ └── Domain/ +│ ├── GameServer/ # Game domain logic +│ ├── Lessons/ +│ │ ├── LessonStrategyBase.cs # Abstract base for lesson definitions +│ │ ├── LessonCatalogHydrationService.cs # Boots Mongo `lessons` from C# strategies +│ │ └── Curriculum/Tier{NN}_*/Lesson*.cs # 95 lessons across 17 tiers +│ └── CodeExecution/ +│ ├── Execution/RoslynCodeExecutor.cs # Shared compile + run engine (server + WASM) +│ └── Hints/HintAnalyzer.cs # Pre-Roslyn pattern checks +├── SpikerSoft.Wasm/ # .NET 10 WebAssembly bundle of Roslyn + lesson strategies +│ ├── WasmCompilerEntry.cs # [JSExport] surface (Ping, GetAttempt, ExecuteLessonCode, ExecuteFreePlayCode, AddReferenceImage) +│ └── main.js # Worker boot script — pre-fetches PE bytes, feeds Roslyn +├── SpikerSoft.Common/ # Shared models and interfaces +│ └── Models/ +│ └── GameServer/ # Commands, Events, Entities, RobotInstruction +├── SpikerSoft.Data/ # Data access layer +│ └── Mongos/ # MongoDB documents (incl. game state, lessons, userLessonProgress) +├── SpikerSoft.Contracts.SignalR/ # SignalR hub contracts +├── SpikerSoft.AI.MCPServer/ # AI/ML model server +├── SpikerSoft.EventHandlers.*/ # Distributed event processors (incl. CodeExecution worker) +├── SpikerSoft.Tests.Unit/ # ~5.1k unit tests (count via `dotnet test SpikerSoft.Tests.Unit --list-tests`) +└── SpikerSoft.sln +``` + +--- + +## Getting Started + +### Prerequisites + +**Frontend:** +- Node.js (see `.node-version` for required version) +- pnpm package manager +- fnm (Fast Node Manager) recommended + +**Backend:** +- .NET 10 SDK (10.0.100 or later) +- Docker Desktop with Swarm support +- MongoDB, Redis, RabbitMQ (via Docker or local) +- Keycloak instance + +### Quick Start + +#### Frontend + +```bash +cd spikersoft-angular + +# Install dependencies +pnpm install + +# Start development server +pnpm run serve:spikersoft-development + +# Run tests +pnpm run test-spikersoft + +# Build for production +pnpm run build +``` + +#### Backend API + +```bash +cd spikersoft-backend + +# Restore dependencies +dotnet restore + +# Run API in development +dotnet run --project SpikerSoft.Api + +# Or use Docker Compose +docker-compose up +``` + +#### Game Server + +```bash +cd spikersoft-backend + +# Run game server +dotnet run --project SpikerSoft.GameServer + +# With specific port +dotnet run --project SpikerSoft.GameServer -- --port 7777 +``` + +--- + +## Configuration + +### Environment-Specific Settings + +Both frontend and backend support environment-specific configuration: + +**Frontend:** +- `environment.ts` - Development +- `environment.production.ts` - Production + +**Backend:** +- `appsettings.json` - Base configuration +- `appsettings.Development.json` - Development overrides +- `appsettings.Production.json` - Production settings +- `appsettings.Windows.json` / `appsettings.Linux.json` - Platform-specific + +### Required Services + +| Service | Default Port | Purpose | +|---------|-------------|---------| +| MongoDB | 27017 (host: 27117) | Primary database (sharded cluster via mongo-router) | +| Redis | 6379 | Caching, SignalR backplane, vector search | +| RabbitMQ | 5672 / 15672 (mgmt) | Message queue with DLQ + retry tiers | +| Keycloak | 8080 | Identity provider (OAuth2 / OIDC) | +| Seq | 5341 | Structured log aggregation | +| Jaeger | 4317 | Distributed tracing (OTLP gRPC) | +| InfluxDB | 8086 | Time-series metrics and dashboards | +| TileServerGL | 443 (external) | Self-hosted terrain tile server for maps | +| DNS Server | 5380 | Self-hosted DNS management | +| Email (SMTP) | 587 | Mail server with DKIM, DMARC, SPF | +| Checkr API | N/A (external) | Background checks — USA (optional, falls back to Manual) | +| Sterling API | N/A (external) | Background checks — International (optional, falls back to Manual) | + +--- + +## Account Types & Age Requirements + +SpikerSoft supports three logical account categories: + +| Account Type | Age | Description | +|---|---|---| +| **Parent** | 18+ | Adult account that can create and manage child accounts. Has full platform access and a parental dashboard for overseeing children's activities, progress, and travel arrangements. | +| **Child** | 10–17 | Managed account created by a parent. Requires parental approval (staff-reviewed). Access to learning experiences, interest tracking, and (if eligible) travel programs. | +| **Independent Solo Traveler** | 18+ | Functionally identical to a Parent account but without linked children. Any adult account without child accounts is an independent solo traveler by default — no separate registration flow is needed. | + +### Age-Gated Rules + +| Rule | Minimum Age | Details | +|---|---|---| +| Create a child account | 10 | Children under 10 are not eligible for accounts. | +| Travel programs (domestic or international) | 12 | Children ages 10–11 can have an account for learning and interests, but travel programs require age 12+. A "domestic travel only" option is available, and the system enforces this on both frontend and backend. | +| Child account upper bound | 17 (under 18) | Adults (18+) should register directly as a Parent or Independent Solo Traveler. | +| Child account conversion warning | 18 | At 18, child accounts are no longer considered minors legally. A dismissable warning banner is shown prompting the user to prepare for account conversion. | +| Child account mandatory conversion | 19 | At 19, the user must convert their child account to an independent individual account. A non-dismissable dialog is shown on login. If declined, the account is disabled in Keycloak and marked as `Inactive`. | + +### Children Tab Visibility + +The "Children" tab on the profile page is visible for all adult accounts (18+) that are not child accounts themselves. This is controlled by a computed `canManageChildren` field on the profile DTO, calculated from the user's age and `IsChildAccount` status. Removing all linked children does **not** hide the tab -- adults can always add new children. + +### Child Account Aging-Out + +When a child account user's age naturally progresses past the child threshold, the system handles the transition automatically: + +1. **At 18**: `IsMinor` is set to `false` on the next profile load. A warning banner appears in the navigation bar informing the user that their account will need to be converted before age 19. The banner is dismissable per session. +2. **At 19**: The `AccountStatus` is set to `RequiresConversion`. A mandatory (non-dismissable) dialog is shown on login with two options: + - **Convert**: Sets `IsChildAccount = false`, clears `ParentalControls`, sets `AccountStatus = Active`. The account becomes a full independent individual account with all data preserved. + - **Decline**: The Keycloak user is disabled (`enabled: false`) and `AccountStatus` is set to `Inactive`. The user is logged out. + +### Parent--Child System + +- **Account Creation Flow**: Parents use the parental dashboard to create child accounts via a multi-step wizard: Travel/Passport → Child Info → Health → Emergency Contacts → Credentials → Review. +- **Passport & Travel**: An optional passport OCR feature (Tesseract + MRZ parsing) can auto-populate passport fields from an uploaded photo. Children who only travel domestically can opt out of passport requirements entirely. +- **Staff Review**: All child account requests go through staff review before activation. Staff access the review queue at `/admin/child-account-review`, which is protected by a `RoleGuard` requiring the `Admin` or `Staff` Keycloak realm role. +- **Encrypted PII**: Passport data (number, full name, DOB, issue/expiry dates) is encrypted at rest using envelope encryption. +- **Interest Profiles**: Both parents and children maintain interest profiles with star ratings across dynamically managed areas of interest (e.g., Hiking, Robotics, Photography) that inform curriculum and experience recommendations. +- **Withdraw Pending Requests**: Parents can withdraw (cancel) pending child account requests before staff reviews them. The request is permanently deleted. Available from both the parent dashboard and the profile's Children tab. +- **Remove Linked Children**: Parents can remove an approved child account. This deletes the child's Keycloak user, deletes their provisioned email mailbox (via PostfixAdmin), removes their profile from MongoDB, and revokes the original request. The parent's `IsParent` flag is preserved (once a parent, always a parent). If the email mailbox deletion fails (e.g., database offline), a `DeleteEmailMailbox` provisioning task is created for staff to retry or manually resolve from the System Issues page. + +### Role-Based Access + +| Role | Scope | Grants | +|---|---|---| +| **Admin** | Keycloak realm role | Full access: Keycloak admin panel, child account review, health info for any child | +| **Staff** | Keycloak realm role | Child account review (approve/reject), passport viewing (audit-logged), health info access | + +Both roles are Keycloak **realm roles** and can be assigned to users via the Keycloak Admin Panel (Roles tab → assign to user). The frontend `RoleGuard` reads roles from the JWT `realm_access.roles` claim. The backend checks `User.IsInRole()` for both `Admin`/`admin` and `Staff`/`staff` (case-insensitive). + +--- + +## System Issues & Provisioning Tasks + +The **System Issues** page (`/admin/staff-issues`) provides staff and admin users with visibility into third-party integration failures — such as email mailbox provisioning — and tools to resolve them. The system is designed so that **third-party failures never block core operations** (e.g., child account approval always succeeds, even if the email server is unreachable). + +### How It Works + +1. **Failure Logging**: When a third-party operation fails (e.g., creating a `username@spikersoft.com` mailbox during child account approval), the error is captured as a `ProvisioningTask` in MongoDB with status `Failed`, along with all metadata needed to retry the operation later. +2. **Menu Badge**: A badge counter on the "System Issues" menu item shows staff/admin users how many outstanding (failed) tasks need attention. This uses a lightweight `GET /api/admin/provisioning-tasks/count` endpoint. +3. **Issue Dashboard**: The staff issues page lists all failed provisioning tasks with details (error message, related entity, timestamps, retry count). Staff can: + - **Retry** — Automatically re-attempt the failed operation (e.g., re-try creating the mailbox). + - **Mark Resolved** — Record that the issue was handled manually (e.g., mailbox created via PostfixAdmin), with optional resolution notes. +4. **Resolution History**: A toggleable "Resolution History" section shows all previously resolved tasks, including who resolved them, when, and any notes. This provides a full audit trail of system issues and their resolutions. + +### Provisioning Task Lifecycle + +``` +Operation Fails → ProvisioningTask (Failed) created + ↓ +Staff sees badge → opens System Issues page + ↓ +┌──── Retry ────────────────────────────────────────┐ +│ Success → status = Resolved, badge decrements │ +│ Failure → stays Failed, error updated, retry++ │ +└───────────────────────────────────────────────────┘ + or +┌──── Mark Resolved ────────────────────────────────┐ +│ status = ManuallyResolved, resolver + notes saved │ +│ Badge decrements, task moves to history │ +└───────────────────────────────────────────────────┘ +``` + +### API Endpoints + +| Method | Endpoint | Purpose | +|---|---|---| +| `GET` | `/api/admin/provisioning-tasks` | List all failed tasks | +| `GET` | `/api/admin/provisioning-tasks/count` | Count of failed tasks (for badge) | +| `GET` | `/api/admin/provisioning-tasks/history` | List all resolved/manually-resolved tasks | +| `POST` | `/api/admin/provisioning-tasks/{id}/retry` | Retry a failed task | +| `POST` | `/api/admin/provisioning-tasks/{id}/resolve` | Mark a task as manually resolved | + +All endpoints require authentication and an `Admin` or `Staff` role. + +--- + +## Profile & Travel + +### Personal Tab — Mailing Address & Home Country + +The **Personal** tab includes a structured mailing address section (Street 1, Street 2, City, State/Province, Postal Code, Country) and a **Home Country** dropdown. The home country setting is used by the domestic travel map to automatically load the correct country view. + +### Travel Map + +The profile's **Travel** tab provides an interactive Leaflet-based map for tracking travel history, wishlists, and parental approvals. + +### Travel Preferences + +Users select their travel interests from the **Interests** tab via two checkboxes: + +| Checkbox | Effect | +|---|---| +| **International Travel** | Enables the world map view on the Travel tab | +| **Domestic Travel** | Enables the home-country sub-region map on the Travel tab | + +When both are selected, a toggle lets the user switch between International and Domestic views. + +### Available Countries + +SpikerSoft currently operates in 13 countries. Only these countries are interactive on the world map; all others appear dimmed with a "Not available yet" tooltip: + +> Canada, United States, Mexico, Belize, Honduras, Colombia, Brazil, Peru, Ecuador, Argentina, Chile, Uruguay, Cuba + +### Country Drill-Down + +When **International Travel** is enabled, an "Explore Country" dropdown appears above the world map. Selecting a country renders a sub-region (state/province) map between the dropdown and the world map, allowing drill-down without leaving the international view. + +For **domestic-only** travelers, the domestic map automatically loads the user's home country from the Personal tab — no dropdown is needed. + +### Visited Status (Split Tracking) + +Each country or sub-region supports two distinct visited categories: + +| Status | Color | Meaning | +|---|---|---| +| **Visited (External)** | Green | Traveled there independently, outside SpikerSoft | +| **Visited (SpikerSoft)** | Purple | Traveled there through a SpikerSoft experience | +| **Wishlist** | Blue | Wants to visit | + +Clicking a country/region cycles through: Wishlist → Visited (External) → Visited (SpikerSoft) → Clear. + +### Domestic Map + +When "Domestic Travel" is enabled and a home country is selected on the Personal tab, the Travel tab shows an admin-level-1 (state/province) GeoJSON map for that country. Sub-region GeoJSON files are stored in `assets/geo/states/{ISO_A3}.geo.json` for all 13 available countries. The map dynamically reloads when the home country selection changes. + +### Travel Documents + +Users can upload, download, and delete travel documents (visas, travel permits, vaccination records, and other files) on the Travel tab. Documents are stored using encrypted GridFS storage via the `SecureDocumentStorageService` and are associated with the user's profile. Supported file types include PDF and common image formats (JPEG, PNG, WebP). Each document has a type classification and an optional label. + +### Parental Approval + +For child accounts (`mode: "minor"`), wishlisted countries appear as "Requested" until a parent approves them from the parent dashboard (`mode: "parent-approve"`). + +### Unsaved Changes Guard + +All profile forms (Personal, Interests, Travel, Appearance) are protected by an unsaved changes guard: + +- **Tab switching**: Prompts save/discard/cancel when switching between profile tabs with dirty forms. +- **Route navigation**: A `canDeactivate` route guard intercepts navigation away from the profile page. +- **Browser close/refresh**: A `beforeunload` handler warns users about unsaved changes. + +--- + +## Development Guidelines + +### Code Style + +**Frontend (Angular):** +- Standalone components (no NgModules) +- Signals for state management +- OnPush change detection +- See `.cursor/rules/spikersoft-angular/` for detailed guidelines + +**Backend (C#):** +- CQRS pattern with MediatR +- Repository pattern for data access +- .NET 10 / C# 14 features encouraged +- See `.cursor/rules/spikersoft-backend/` for detailed guidelines + +### Testing + +**Frontend:** +```bash +pnpm run test-spikersoft # Unit tests for spikersoft only (Nx → Vitest) +pnpm run test-all # All test targets (5 projects: spikersoft, tools, keycloak-admin, spikersoft-theme, wasm-voxel) +pnpm run test-all:detect-leaks # Same projects, but with VITEST_DETECT_ASYNC_LEAKS=true → forks pool flags dangling timers / unhandled promises +``` + +`test-all:detect-leaks` flips every project into Vitest's `forks` pool with `detectAsyncLeaks: true`. Each project's `vitest.config.ts` checks the env var and toggles the pool accordingly so the regular `test-all` keeps the faster `threads` pool. Cross-platform env-var setting uses `cross-env` (devDependency). + +There is no `e2e` script in `spikersoft-angular/package.json`; add an Nx E2E target if you introduce browser E2E. + +**Backend:** +```bash +dotnet test # All tests +dotnet test --filter "Category=Unit" # Unit tests only +``` + +### QA: Gitea issues and the AI agent (MCP) + +Product and engineering tickets for SpikerSoft are tracked in Gitea, including cross-cutting and roadmap items in the dedicated issues repo: **[`spikerj/spikersoft-issues`](https://git.spikersoft.com/spikerj/spikersoft-issues)** (e.g. future work that mirrors the [TODO / Future work](#todo--future-work) section). + +**Using the Gitea MCP in Cursor** so QA (and devs) can work tickets through the AI agent without leaving the IDE: + +1. **Enable the integration** — In **Cursor Settings → MCP**, ensure the **Gitea** server is on and configured (Instance URL, access token, and path to the `gitea-mcp` command if you use the standalone binary). The token must have API scope to read and create issues in the org/repos you use. +2. **Confirm the session** — The Gitea tools appear in the project only when the MCP is connected. If a chat reports that the Gitea server is missing, re-open MCP settings and toggle or reconnect, then start a new agent message. +3. **What to ask the agent** — Natural-language requests map to the MCP, for example: *“List open issues in `spikerj/spikersoft-issues`”*, *“Create an issue for …”*, *“Add a comment on issue #5 summarizing the repro”*, *“Show details for issue #2”*, or *“Search repos named …”* (the exact tool surface depends on your `gitea-mcp` build; list/search/create/edit issues and comments are the usual workflows). +4. **Good practices** — Prefer filing reproduction steps, build or environment, and expected vs actual behavior in the issue body so the agent can quote them back accurately. For sensitive data, do not paste secrets into issues; use references to internal logs or redacted snippets. + +This complements manual use of the Gitea web UI: the same [https://git.spikersoft.com](https://git.spikersoft.com) data, with faster handoff from chat, terminal output, and code context while triaging or closing the loop on QA. + +### Observability + +- **Logging**: Serilog → Seq (structured logging with correlation IDs) +- **Tracing**: OpenTelemetry → Jaeger (OTLP gRPC on port 4317) +- **Metrics**: OpenTelemetry exporters → InfluxDB v2.7 +- **Health**: `/healthz` endpoint with 13 infrastructure checks (see [Health Check & Observability](#health-check--observability)) + +All cross-service calls include correlation IDs for distributed tracing. + +--- + +## Deployment + +### Production Stack + +- **Orchestration**: Docker Swarm +- **Load Balancer**: Traefik v3.6 with Let's Encrypt SSL +- **MongoDB**: Sharded cluster (3 shards × 3 replicas + router + config servers) +- **Redis**: 6-node cluster (3 masters + 3 replicas) + +### Container Images + +```bash +# Build API image +docker build -t spikersoft-api -f SpikerSoft.Api/Dockerfile . + +# Build Game Server image +docker build -t spikersoft-gameserver -f SpikerSoft.GameServer/Dockerfile . + +# Build CodeRunner (sandboxed C# grader worker) image +docker build -t spikersoft-coderunner -f SpikerSoft.EventHandlers.CodeExecution/Dockerfile . + +# Build SpikerSoft.Wasm (browser-side Roslyn bundle) — published to Gitea Generic Package by spikersoft-wasm.yml +dotnet publish SpikerSoft.Wasm -c Release # writes to ../spikersoft-angular/projects/spikersoft/src/assets/dotnet/ + +# Build Angular image (downloads SpikerSoft.Wasm bundle from Gitea Generic Package in CI) +docker build -t spikersoft-angular -f spikersoft-angular/Dockerfile . +``` + +--- + +## Documentation + +| Component | README / Section | +|-----------|-----------------| +| Angular Frontend | `spikersoft-angular/README.md` | +| Backend API | `spikersoft-backend/README.md` | +| Game Server | `spikersoft-backend/SpikerSoft.GameServer/README.md` | +| Integrated Game Ecosystem | See main README — [Integrated Game Ecosystem](#integrated-game-ecosystem) | +| Robot & Visual Programming | See main README — [Robot & Visual Programming](#robot--visual-programming) and `ARCHITECTURE-ROBOT-VISUAL-PROGRAMMING.md` | +| C# Coding Curriculum & Playground | See main README — [C# Coding Curriculum & Playground](#c-coding-curriculum--playground); WASM bundle: `spikersoft-angular/projects/spikersoft/src/assets/dotnet/README.md`; Tools library: `spikersoft-angular/libraries/tools/README.md` | +| Event Handlers | See main README — [Event Handlers](#event-handlers); shared infra: `spikersoft-backend/SpikerSoft.EventHandlers.Infrastructure/README.md`; code worker: `spikersoft-backend/SpikerSoft.EventHandlers.CodeExecution/README.md` | +| Chess Game | See main README — [Chess Game](#chess-game) section | +| Dungeon Crawler | `spikersoft-angular/projects/spikersoft/src/app/_components/_games/dungeon-crawler/README.md` | +| Team & Hiring | See main README — [Team Page](#team-page), [Hiring Page](#hiring-page), [Data-Driven Locations](#data-driven-locations) sections | +| Sponsorship | See main README — [Sponsorship & Donation System](#sponsorship--donation-system) | +| Fundraisers | See main README — [Fundraiser System](#fundraiser-system) | +| Info Vault | See main README — [Info Vault](#info-vault) | +| Blog System | See main README — [Blog System](#blog-system) | +| Reading Journey | See main README — [Reading Journey & Book System](#reading-journey--book-system) | +| Real-time Communication | See main README — [Real-time Communication](#real-time-communication) | +| Marks Field Service | See main README — [Marks Field Service](#marks-field-service) | +| Health & Observability | See main README — [Health Check & Observability](#health-check--observability) | + +--- + +## Chess Game + +The platform includes a full-featured chess game with three play modes. + +### Game Modes + +| Mode | Description | +|------|-------------| +| **2 Players — Same Computer** | Classic local play, two players share one screen | +| **Versus AI** | Play against a client-side AI with three difficulty levels | +| **Queue vs SpikerSoft Members** | Real-time online multiplayer coordinated via SignalR | + +### AI Difficulty Levels + +- **Easy** — Random legal move selection +- **Normal** — Minimax with alpha-beta pruning (depth 2), basic piece-value evaluation + center control bonus +- **Hard** — Minimax (depth 3) with piece-square positional tables (pawn structure, knight outposts, king safety) + +The AI runs entirely client-side — no backend compute required. After the human moves, the `ChessAIService` evaluates the position in a deferred `setTimeout` to keep the UI responsive. + +### Online Multiplayer + +Online play uses a dedicated `ChessHub` SignalR hub (`/hubs/chess`) with Keycloak authentication. + +**Matchmaking flow:** + +1. Player joins the queue (can also queue while playing an AI game) +2. When two players are queued, the server pairs them, assigns random colors, and notifies both +3. A confirmation dialog appears — accepting starts the online match (pausing any in-progress AI game) +4. Moves are sent to the server and relayed to the opponent in real time +5. If a player disconnects, a 60-second timer starts; if they don't reconnect, the opponent wins by abandonment + +**Available actions during an online game:** Offer Draw, Accept/Decline Draw, Resign. + +### Game Persistence + +Completed online games (checkmate, resignation, draw, abandonment) are persisted to MongoDB as `ChessGame` documents, storing both players, all moves, result, and timestamps. + +### Key Files + +| File | Purpose | +|------|---------| +| `chess.component.ts/html/scss` | UI component with mode selection, board, dialogs | +| `chess-game.service.ts` | Core game logic — move validation, castling, en passant, promotion | +| `chess-ai.service.ts` | Client-side AI engine (minimax + alpha-beta pruning) | +| `chess-online.service.ts` | SignalR service for online multiplayer | +| `ChessHub.cs` | Backend SignalR hub — matchmaking, move relay, disconnect handling | +| `ChessGame.cs` | MongoDB document for completed game history | + +--- + +## Team Page + +The `/team` route displays profiles of all SpikerSoft staff members. Profiles are fetched by querying Keycloak for users with the "Staff" and "Admin" realm roles, then merging with MongoDB `UserProfile` data (avatar, handle, bio). + +- **Backend**: `TeamController` (`GET /api/team`) calls `KeycloakAdminService.GetRoleMembersAsync()` to list role members, then joins with the `user-profiles` collection. +- **Frontend**: `TeamComponent` uses `TeamService` to load members and displays them in a responsive glassmorphic card grid. Each card shows an avatar (or generated initials), name, handle, and bio excerpt. + +| File | Purpose | +|------|---------| +| `TeamController.cs` | API endpoint merging Keycloak role members with MongoDB profiles | +| `KeycloakAdminService.GetRoleMembersAsync()` | Queries Keycloak Admin API for users in a given role | +| `team.service.ts` | Frontend service fetching `GET /api/team` | +| `team.component.ts/html/scss` | Staff profile card grid with loading and error states | + +--- + +## Hiring Page + +The `/employment` route lists open positions at SpikerSoft, divided into two sections: + +1. **Ambassador Positions** — Auto-generated for every active `Location` that does not have an assigned ambassador. These positions are dynamically created by the backend so the hiring page always reflects current staffing gaps. +2. **Other Roles** — Manually created positions (developer, designer, teacher, etc.) managed through the admin Location Management page. + +- **Backend**: `PositionsController` (`GET /api/positions`) merges manually created `OpenPosition` documents with auto-generated ambassador entries for unassigned locations. +- **Frontend**: `EmploymentComponent` uses `PositionService` and displays positions grouped by type with color-coded badges. + +### Ambassador Application Workflow + +Clicking an **open** position card launches a multi-step application dialog (`ApplicationDialogComponent`). Applicants must be logged in. + +**Intake Form Steps:** + +1. **Position & Passport** — Read-only position summary, optional passport image scan via OCR (reuses the same `IPassportOcrService` / Tesseract OCR pipeline as the child account flow). +2. **Personal Information** — Name, DOB, sex, nationality, citizenship, government ID (SSN / National ID / Tax ID), email, phone. Fields auto-populate from passport scan results. +3. **Address History** — Current and previous addresses with date ranges. +4. **Work & Education** — Employment history and education entries. +5. **References** — Minimum 2 professional/personal references. +6. **Consent & Submit** — Background check authorization and terms acceptance. + +**Application Status Lifecycle:** + +`Submitted` → `UnderReview` → `BackgroundCheckPending` → `BackgroundCheckComplete` → `Approved` → `Hired` + +Applications can be `Rejected` at any review stage with a reason. + +**Admin Review:** + +Staff/Admin users access the review panel at `/admin/application-review` (menu item: "Applications" with pending count badge). From there they can: + +- Begin review, initiate background checks (provider selected automatically by country), approve/reject, and hire. +- The **Hire** action creates a Keycloak account with the `Staff` role, creates a `PositionAssignment`, and marks the application as `Hired`. + +| File | Purpose | +|------|---------| +| `PositionsController.cs` | Merges manual positions with auto-generated ambassador positions | +| `ApplicationController.cs` | Application submission, passport scan, admin review, background check, hire endpoints | +| `AmbassadorApplication.cs` | MongoDB entity for applications with encrypted PII | +| `OpenPosition.cs` | MongoDB model for manually managed positions | +| `position.service.ts` | Frontend service for positions CRUD | +| `application.service.ts` | Frontend service for application API calls | +| `employment.component.ts/html/scss` | Hiring page with grouped position cards (clickable for applications) | +| `application-dialog.component.ts` | Multi-step application stepper dialog | +| `application-review.component.ts` | Admin review panel with status workflow | + +## Third-Party Background Check Providers + +The background check system uses a pluggable provider architecture. Each provider implements `IBackgroundCheckProvider` and is resolved per-country via `BackgroundCheckProviderFactory`. + +### Supported Providers + +| Provider | Coverage | API Documentation | Notes | +|----------|----------|-------------------|-------| +| **Checkr** | USA | [docs.checkr.com](https://docs.checkr.com) | API-first platform; industry standard for US background checks. Requires API key from Checkr dashboard. | +| **Sterling** | International (Latin America, Canada, etc.) | [sterlingcheck.com](https://sterlingcheck.com) | Broadest international coverage for non-US countries. | +| **Manual** | Fallback (all countries) | N/A | No external API. Staff perform checks externally and record results in the admin panel. Used when no automated provider is configured for a country. | + +### Configuration + +Country-to-provider mappings and API keys are configured in `appsettings.json`. **API keys must not be committed** — use environment variables or a secrets manager in deployment. + +```json +"BackgroundCheck": { + "DefaultProvider": "Manual", + "CountryProviders": { + "US": { "Provider": "Checkr", "ApiKey": "" }, + "International": { "Provider": "Sterling", "ApiKey": "" } + } +} +``` + +### Adding a New Provider + +1. Create a class implementing `IBackgroundCheckProvider` in `SpikerSoft.Business/Services/BackgroundCheck/Providers/`. +2. Register it in DI (`ServiceCollectionExtensions.cs`). +3. Add the country mapping in `appsettings.json` under `BackgroundCheck:CountryProviders`. + +### API Key Management + +Background check API keys follow the same pattern as `GoogleMaps:ApiKey`: empty strings in `appsettings.json` with actual values injected via environment variables (`BackgroundCheck__CountryProviders__US__ApiKey`) or a secrets vault in production. + +--- + +## Postal Code / City / State Lookup + +The platform includes a standardized postal code lookup system powered by **GeoNames** open data. When a user enters a postal code in any address form (ambassador application, profile, marks addresses), the system auto-populates the city and state fields and presents a state dropdown for the selected country. + +### Data Source + +| Source | URL | License | +|--------|-----|---------| +| **GeoNames Postal Codes** | [download.geonames.org/export/zip/](https://download.geonames.org/export/zip/) | [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/) | + +Data covers all 13 operating countries (US, CA, MX, BZ, HN, CO, BR, PE, EC, AR, CL, UY, CU). + +### Architecture + +- **MongoDB collection**: `postal-codes` stores parsed GeoNames entries (country, postal code, city, state, coordinates) +- **CQRS Queries**: `LookupPostalCodeQuery` and `GetStatesForCountryQuery` in `SpikerSoft.Business/Domain/PostalCodes/` +- **Controller**: `PostalCodesController` at `api/PostalCodes` +- **Frontend**: `PostalCodeService` provides `lookupPostalCode()` and `getStatesForCountry()` with caching + +### API Endpoints + +| Endpoint | Auth | Purpose | +|----------|------|---------| +| `GET /api/PostalCodes/lookup?countryCode={cc}&postalCode={pc}` | Anonymous | Returns matching city/state entries | +| `GET /api/PostalCodes/states/{countryCode}` | Anonymous | Returns distinct states for a country | +| `POST /api/PostalCodes/seed?countryCode={cc}` | Admin | Downloads and imports GeoNames data | +| `GET /api/PostalCodes/status` | Staff | Returns record counts per country | + +### Seeding Data + +After deployment, an admin must seed the postal code data: + +```bash +# Seed all operating countries +curl -X POST https://your-api/api/PostalCodes/seed -H "Authorization: Bearer " + +# Seed a single country +curl -X POST "https://your-api/api/PostalCodes/seed?countryCode=US" -H "Authorization: Bearer " +``` + +### Attribution + +This product includes data from GeoNames ([geonames.org](https://www.geonames.org/)), licensed under Creative Commons Attribution 4.0. + +--- + +## Data-Driven Locations + +All country/location data is now stored in MongoDB instead of being hardcoded. The `Location` model holds country codes, names, active status, and optional ambassador assignments. + +**Key changes:** +- The `AVAILABLE_COUNTRIES` constant in `country-map.component.ts` is replaced with an `availableCountries` input, allowing dynamic country lists from the API. +- The `ProfileComponent` fetches active locations from `LocationService` on init and passes them to the country map and travel dropdowns. +- The `LocationsController` auto-seeds the initial 13 countries (CAN, USA, MEX, BLZ, HND, COL, BRA, PER, ECU, ARG, CHL, URY, CUB) on first request if the collection is empty. + +| File | Purpose | +|------|---------| +| `Location.cs` | MongoDB model for locations | +| `LocationsController.cs` | Full CRUD + auto-seed for locations | +| `location.service.ts` | Frontend service for location CRUD | +| `country-map.component.ts` | Accepts dynamic `availableCountries` input | +| `profile.component.ts` | Loads locations from API instead of hardcoded array | + +--- + +## Location Management + +Staff and admin users can manage locations and open positions via `/admin/location-management`, accessible from the "Location Management" menu item in the user dropdown. + +- **Locations Tab**: View all locations, toggle active/inactive, add new countries, delete locations. +- **Positions Tab**: View manually created positions, create new roles, toggle active status, delete positions. Auto-generated ambassador positions (from unassigned locations) appear on the hiring page but are not directly editable here. + +| File | Purpose | +|------|---------| +| `location-management.component.ts/html/scss` | Admin page with tabbed location and position management | +| `routes.ts` | Route registered at `admin/location-management` with AuthGuard + RoleGuard | +| `menu-bar.component.html` | "Location Management" added to user dropdown (staff section), "Hiring" added to main nav | + +--- + +## Sponsorship & Donation System + +SpikerSoft is a nonprofit organization. The sponsorship system enables donors to fund travel experiences for approved individuals and children. + +**How It Works:** + +1. **Enable Sponsorship** — Parents or individuals 18+ enable the sponsorship toggle in the Sponsorship section of their Profile > Personal tab, configuring display preferences (name mode, bio, photo, interests, destinations, custom message). Parents can also enable sponsorship per-child via the sponsorship toggle on each child's card in the Children tab. +2. **Staff Approval** — Staff members review and approve sponsorship profiles via Admin > Sponsorship Management. +3. **Trip Cost Assignment** — Staff sets estimated travel costs per wishlisted destination for each approved profile. +4. **Public Donation Page** — A public `/sponsor` page (no login required) displays approved profiles with their bio, interests, destinations, and funding progress. +5. **Stripe Checkout** — Donors can donate to an individual or to the SpikerSoft organization. Payments are processed via Stripe Checkout with automatic nonprofit receipts. +6. **Organization Donations** — Organization-wide donations are distributed evenly among all eligible (approved, not fully funded) profiles at the time of donation. New applicants do not retroactively receive prior donations. +7. **Overfunding** — When an individual is fully funded (total raised >= total trip cost), additional donations automatically redirect to the organization pool. +8. **QR Code & Sharing** — Each sponsor profile page has a "Generate QR Code" button for creating business-card-ready QR codes linking to the donation page. + +**Account Types & Sponsorship Eligibility:** + +- Only **parents** and **individuals 18+** can enable their own sponsorship (in the Personal tab's Sponsorship section) +- Parents can toggle per-child sponsorship via the child's card in the Children tab +- **Child accounts** cannot toggle their own sponsorship status +- Staff must approve before profiles go public + +**Privacy Controls (Parent-Configurable):** + +- Display name mode: First name + last initial, handle/alias only, or full name +- Photo mode: Original, Cartoon (recommended for minors), or Hidden + - **Cartoon mode** uses SixLabors.ImageSharp to generate a stylized cartoon version of the avatar (Gaussian blur + color quantization + edge detection composite), stored as a separate GridFS file + - This protects the child's identity while still showing a personalized image on the public donation page +- Toggles for showing: bio, interests, destinations + +**Stripe Configuration:** + +Configuration is in `appsettings.json` under the `Stripe` section: + +```json +"Stripe": { + "PublishableKey": "pk_test_...", + "SecretKey": "sk_test_...", + "WebhookSecret": "whsec_...", + "NonprofitTaxId": "" +} +``` + +| Key | Description | +|-----|-------------| +| `PublishableKey` | Stripe publishable key (safe for frontend; exposed via `GET /api/sponsor/config`) | +| `SecretKey` | Stripe secret key (**NEVER** commit to source control in production — use environment variables or a secrets manager) | +| `WebhookSecret` | Stripe webhook signing secret used to verify incoming webhook payloads | +| `NonprofitTaxId` | Organization EIN included on donation receipts for tax-deductible contributions | + +**Webhook Setup (Stripe Dashboard):** + +Stripe webhooks notify the backend when a payment is completed. Without a properly configured webhook, donations will remain in `Pending` status indefinitely. + +1. Navigate to [Stripe Dashboard → Developers → Webhooks](https://dashboard.stripe.com/webhooks). +2. Click **Add endpoint**. +3. Set the **Endpoint URL** to your publicly accessible backend URL: + - Production: `https://your-domain.com/api/sponsor/webhook` + - Local development: Use [Stripe CLI](https://stripe.com/docs/stripe-cli) to forward events (see below). +4. Under **Events to send**, select: `checkout.session.completed`. +5. Click **Add endpoint**. +6. Copy the **Signing secret** (`whsec_...`) from the endpoint details page and set it as `Stripe:WebhookSecret` in `appsettings.json` (or your environment config). + +**Local Development with Stripe CLI:** + +For testing webhooks locally without a public URL: + +```bash +# Install Stripe CLI: https://stripe.com/docs/stripe-cli#install +stripe login + +# Forward webhook events to your local backend +stripe listen --forward-to https://localhost:5001/api/sponsor/webhook + +# The CLI prints a webhook signing secret (whsec_...) — copy it into appsettings.Development.json +``` + +The backend gracefully handles a missing `WebhookSecret` — if the value is empty, webhook payloads are parsed without signature verification and a warning is logged. This is acceptable during development but **must** be configured in production. + +**Webhook Event Flow:** + +``` +Donor completes Stripe Checkout + → Stripe fires checkout.session.completed event + → POST /api/sponsor/webhook receives the event + → Signature verified against WebhookSecret (if configured) + → Matching Donation record updated: Status → Completed, PaymentIntentId saved + → For Organization donations: amount distributed evenly among eligible profiles + → Duplicate webhooks are safely ignored (idempotent) +``` + +**Donation Lifecycle:** + +| Status | Meaning | +|--------|---------| +| `Pending` | Checkout session created, awaiting payment | +| `Completed` | Webhook confirmed successful payment | +| `Failed` | Payment failed or was cancelled | + +**Test vs. Production Keys:** + +- Use `pk_test_` / `sk_test_` keys during development — no real charges are made. +- Switch to `pk_live_` / `sk_live_` keys for production. Update the webhook endpoint URL to your production domain and create a new webhook signing secret. +- Stripe provides [test card numbers](https://stripe.com/docs/testing#cards) (e.g., `4242 4242 4242 4242`) for simulating payments. + +**API Endpoints:** + +Public (no auth): +- `GET /api/sponsor/profiles` — List approved sponsor profiles +- `GET /api/sponsor/profiles/{id}` — Individual profile detail +- `GET /api/sponsor/profiles/{id}/progress` — Donation progress +- `POST /api/sponsor/checkout` — Create Stripe Checkout session +- `POST /api/sponsor/webhook` — Stripe webhook handler +- `GET /api/sponsor/config` — Stripe publishable key + +Authenticated: +- `GET /api/sponsor/settings` — Own sponsorship settings + +Staff only: +- `GET /api/sponsor/profiles/pending` — Pending profiles +- `POST /api/sponsor/profiles/{id}/approve` — Approve profile +- `DELETE /api/sponsor/profiles/{id}/approve` — Revoke approval +- `PUT /api/sponsor/profiles/{id}/trip-costs` — Set trip costs +- `GET /api/sponsor/donations` — All donations + +**Lifecycle:** + +``` +Parent enables sponsorship → Staff approves → Staff sets trip costs → Profile appears on /sponsor → Donors contribute → Stripe processes payment → Webhook confirms → Donation recorded +``` + +--- + +## Fundraiser System + +While sponsorship connects donors to travelers, the **Fundraiser** system teaches members how to raise their own funds — giving students an introduction to business while working toward their travel goals. + +### Concept + +Members create fundraisers tied to a specific trip destination. Each fundraiser models a real-world business activity (buying and reselling products, bake sales, services, or custom approaches). The system provides: + +- **Profit Calculator** — Enter item cost, set a selling price via slider or manual input (minimum = cost, maximum = 10x cost), and instantly see profit per unit, margin %, and how many units are needed to reach the goal. +- **"What If" Simulator** — A units-sold slider lets members preview partial progress: "If I sell 15 units, I'll earn $X and be Y% toward my goal." +- **Solicitation Map** — A Leaflet + leaflet-draw map where members draw polygon zones for where they plan to sell, with recommendation pins for high-traffic areas powered by the Overpass API. +- **Area Recommendations** — The backend queries OpenStreetMap for nearby POIs (shops, schools, parks, transit stops) and scores them by density and accessibility. + +### Fundraiser Types + +| Type | Description | +|------|-------------| +| **Product Resale** | Buy items wholesale and sell at a profit | +| **Bake Sale** | Sell homemade baked goods | +| **Service Based** | Offer services like car washes or lawn care | +| **Custom** | Design a unique fundraiser approach | + +### Creation Flow + +A 4-step dialog guides members through fundraiser creation: + +1. **Type** — Select fundraiser type; displays the trip cost as the fundraising goal. +2. **Items** — Enter item name and cost (in dollars), use the profit calculator to set a selling price, and save items. The calculator shows units-to-goal and a simulation slider. +3. **Zones** — Draw solicitation areas on the map; view AI-suggested high-traffic locations. +4. **Review** — Summary of items, profit projections, goal coverage, and zone count before submission. + +### Access Points + +- **Destination Cards** — A storefront icon button on each destination card header opens the create-fundraiser dialog pre-filled with that destination's country and trip cost. +- **Profile Menu** — "Fundraisers" menu item under Info Vault navigates to `/fundraisers`. +- **Fundraiser Dashboard** (`/fundraisers`) — Lists all fundraisers with quick stats (total raised, active count, average margin), status filter chips, and progress bars. Click a card to view full detail. + +### Fundraiser Lifecycle + +| Status | Meaning | +|--------|---------| +| `Draft` | Created but not yet active | +| `Active` | Currently running | +| `Paused` | Temporarily halted | +| `Completed` | Goal reached or manually completed | +| `Cancelled` | Abandoned (soft-deleted) | + +### Backend Architecture (CQRS) + +The fundraiser feature follows the existing MediatR CQRS pattern: + +**Commands:** `CreateFundraiser`, `UpdateFundraiser`, `RecordSale`, `DeleteFundraiser` +**Queries:** `GetFundraisers`, `GetFundraiserById`, `GetAreaSuggestions` +**Validators:** FluentValidation for all commands (country must be alpha-3, selling price >= cost, etc.) + +The `IFundraiserAreaService` interface abstracts the Overpass API integration for area recommendations, with fallback suggestions when the external API is unavailable. + +**API Endpoints:** + +| Method | Endpoint | Purpose | +|--------|----------|---------| +| `GET` | `/api/fundraiser` | List user's fundraisers (optional `country`, `status` filters) | +| `GET` | `/api/fundraiser/{id}` | Get fundraiser detail | +| `POST` | `/api/fundraiser` | Create fundraiser | +| `PUT` | `/api/fundraiser/{id}` | Update fundraiser | +| `PUT` | `/api/fundraiser/{id}/record-sale` | Record item sales | +| `DELETE` | `/api/fundraiser/{id}` | Cancel/soft-delete fundraiser | +| `GET` | `/api/fundraiser/area-suggestions` | Get area recommendations (`lat`, `lng`, `radiusKm`) | + +All endpoints require authentication. + +### Data Model + +The `Fundraiser` entity stores in the `fundraisers` MongoDB collection with nested sub-documents for items (cost, selling price, quantities, computed profit), solicitation zones (GeoJSON polygons), and area recommendations. Indexes on `UserId`, `Status`, and `(UserId, DestinationCountry)` support efficient queries. + +### Key Files + +| Layer | Files | +|-------|-------| +| **Entity** | `SpikerSoft.Data/Mongos/Fundraiser.cs` | +| **Commands** | `SpikerSoft.Business/Domain/Fundraiser/Commands/` | +| **Queries** | `SpikerSoft.Business/Domain/Fundraiser/Queries/` | +| **Validators** | `SpikerSoft.Business/Domain/Fundraiser/Validators/` | +| **Controller** | `SpikerSoft.Api/Domain/Fundraiser/FundraiserController.cs` | +| **Area Service** | `SpikerSoft.Api/Domain/Fundraiser/FundraiserAreaService.cs` | +| **Frontend Models** | `_models/fundraiser.model.ts` | +| **Frontend Service** | `_services/fundraiser/fundraiser.service.ts` | +| **Profit Calculator** | `_components/fundraiser/profit-calculator/` | +| **Solicitation Map** | `_components/fundraiser/solicitation-map/` | +| **Create Dialog** | `_components/fundraiser/create-fundraiser-dialog/` | +| **Dashboard** | `_components/fundraiser/fundraiser-dashboard/` | + +--- + +## Info Vault + +The Info Vault is a personal learning repository where members save, organize, and revisit resources they discover across the platform. Parents can view their children's vault contents. + +### Item Types + +| Type | Description | +|------|-------------| +| **External URL** | Save any web link with optional full-page HTML cache/snapshot | +| **Internal Reference** | Bookmark to in-platform content (facts, blog posts, books) | +| **Note** | Rich-text notes for personal learning reflections | +| **File Upload** | Upload documents and images (stored in GridFS bucket `vault-files`) | + +### Features + +- **Full-page snapshots** — Cache external URLs as complete HTML snapshots for offline access +- **Search & filter** — Full-text search across all vault items with type and category filters +- **Favorites** — Star important items for quick access +- **Stats dashboard** — Item counts by type, storage usage, activity trends +- **Parent access** — Parents can browse a child's vault items via `GET /api/vault/child/{childUserId}` + +### API Endpoints + +| Method | Endpoint | Purpose | +|--------|----------|---------| +| `GET` | `/api/vault` | List vault items (paginated, filterable) | +| `POST` | `/api/vault` | Create a vault item | +| `PUT` | `/api/vault/{id}` | Update a vault item | +| `DELETE` | `/api/vault/{id}` | Delete a vault item | +| `GET` | `/api/vault/stats` | Vault statistics | +| `GET` | `/api/vault/search?q=` | Full-text search | +| `POST` | `/api/vault/{id}/favorite` | Toggle favorite | +| `POST` | `/api/vault/{id}/cache` | Cache/snapshot an external URL | +| `POST` | `/api/vault/upload` | Upload a file (multipart) | +| `GET` | `/api/vault/files/{gridFsId}` | Download a vault file | +| `GET` | `/api/vault/child/{childUserId}` | List a child's vault items (parent access) | + +All endpoints require authentication. + +### Key Files + +| File | Purpose | +|------|---------| +| `VaultController.cs` | API controller with CQRS handlers | +| `VaultItem.cs` | MongoDB entity (collection: `vault-items`) | +| `GridFsVaultFileService.cs` | GridFS file storage for uploads | +| `info-vault.component.ts` | Angular vault UI with search, filters, favorites | +| `vault.service.ts` | Frontend HTTP service | + +--- + +## Blog System + +Members create travel and learning blog posts that connect to the platform's geography and fact system. Posts go through a staff moderation workflow before publishing, and uploaded media passes through an async processing pipeline. + +### Moderation Workflow + +``` +Author creates post → Status: Draft + ↓ +Author submits for review → Status: Pending + ↓ +Staff reviews at /admin/blog-moderation + ↓ +┌──── Approve → Status: Published (visible on platform) +└──── Reject → Status: Rejected (author notified with reason) +``` + +### LinkedFact Integration + +Blog posts can be linked to geographic facts via `LinkedFact` references (factId, countryCode, countryName). This creates a bidirectional connection: the geography explorer shows related blog posts for a fact, and blog posts link out to the interactive map. + +### Media Processing Pipeline + +Uploaded blog media flows through an async RabbitMQ pipeline in `SpikerSoft.EventHandlers.BlogMediaProcessor`: + +1. **Received** — File accepted and queued +2. **ClamAV Scan** — Virus/malware scanning +3. **Metadata Extraction** — EXIF data, dimensions, format detection +4. **Strip** — Remove sensitive metadata (GPS coordinates, camera info) +5. **Move** — Transfer to final storage location + +Each step updates the media's `ProcessingStatus`, and failures are isolated per-step. + +### API Endpoints + +| Method | Endpoint | Purpose | +|--------|----------|---------| +| `GET` | `/api/blog/posts` | List posts (paged, filtered by status/country/tag) | +| `GET` | `/api/blog/posts/{id}` | Get post detail | +| `GET` | `/api/blog/posts/by-fact/{factId}` | Posts linked to a geographic fact | +| `GET` | `/api/blog/posts/by-country/{countryCode}` | Posts about a country | +| `POST` | `/api/blog/posts` | Create post (JSON) | +| `POST` | `/api/blog/posts/upload` | Create post with media (multipart, up to 12 files) | +| `PUT` | `/api/blog/posts/{id}` | Update post | +| `DELETE` | `/api/blog/posts/{id}` | Delete post | +| `POST` | `/api/blog/posts/{id}/media/upload` | Upload additional media | +| `POST` | `/api/blog/posts/{id}/comments` | Add a comment | +| `POST` | `/api/blog/posts/{id}/approve` | Approve post (Staff/Admin) | +| `POST` | `/api/blog/posts/{id}/reject` | Reject post (Staff/Admin) | +| `GET` | `/api/blog/posts/pending` | Pending moderation queue (Staff/Admin) | +| `GET` | `/api/blog/posts/my` | Author's own posts | + +### Key Files + +| File | Purpose | +|------|---------| +| `BlogController.cs` | API controller with moderation endpoints | +| `BlogPost.cs` | MongoDB entity with media, comments, LinkedFacts | +| `LinkedFact.cs` | Geography fact reference sub-document | +| `BlogMediaOrchestrator.cs` | RabbitMQ pipeline coordinator | +| `blog.service.ts` | Frontend HTTP service | + +--- + +## Reading Journey & Book System + +The Reading Journey is the platform's digital library and learning hub. Members upload books (PDF or EPUB), which pass through an AI-powered processing pipeline that generates metadata, embeddings, and comprehension quizzes. The reader supports bookmarks, progress tracking, and multiple reading profiles. + +### Book Processing Pipeline + +``` +User uploads PDF/EPUB → Staff approval queue + ↓ +Staff approves → Processing pipeline begins + ↓ +┌──── MetadataExtractor → Extract title, author, page count, TOC +├──── Embeddings → Chunk text, generate vector embeddings (LLamaSharp) +├──── QuizGeneration → LLM-powered comprehension quizzes per chapter +└──── SecurityScanner → Content safety check + ↓ +SignalR notifications update the user in real time (BookProcessingStage) + ↓ +Book available in library +``` + +### Reader Features + +- **EPUB reader** — Chapter navigation, page rendering, embedded resource loading +- **PDF viewer** — Full document rendering with page tracking +- **Progress tracking** — Automatic page + scroll position saving per book +- **Bookmarks** — Save and annotate specific locations +- **Reading preferences** — Font size, theme, layout customization +- **Reading profiles** — Multiple reading profiles per user (e.g., "Study", "Leisure") +- **Recently read** — Quick access to books in progress +- **Quizzes** — AI-generated comprehension quizzes tied to book chapters + +### API Endpoints + +**Book Management (`api/book`):** + +| Method | Endpoint | Purpose | +|--------|----------|---------| +| `GET` | `/api/book/all` | List all accessible books | +| `GET` | `/api/book/{id}` | Book detail | +| `POST` | `/api/book` | Create book metadata | +| `POST` | `/api/book/process/upload-pdf` | Upload PDF for processing | +| `GET` | `/api/book/{id}/epub-chapters` | EPUB chapter list | +| `GET` | `/api/book/{bookId}/epub-page` | Render an EPUB page | +| `GET` | `/api/book/{bookId}/epub-resource` | Serve EPUB embedded resources | +| `GET` | `/api/book/{id}/file` | Download original file | +| `GET` | `/api/book/public` | Public book catalog | +| `GET` | `/api/book/my-books` | User's uploaded books | + +**Reader (`api/reader`):** + +| Method | Endpoint | Purpose | +|--------|----------|---------| +| `GET/PUT` | `/api/reader/progress/{bookId}` | Read/save reading progress | +| `GET` | `/api/reader/recently-read` | Recently read books | +| `GET/PUT` | `/api/reader/preferences` | Reading preferences | +| `GET` | `/api/reader/bookmarks/{bookId}` | Bookmarks for a book | +| `POST` | `/api/reader/bookmarks` | Create bookmark | +| `DELETE` | `/api/reader/bookmarks/{bookmarkId}` | Delete bookmark | +| `GET/POST` | `/api/reader/profiles` | List / create reading profiles | +| `DELETE` | `/api/reader/profiles/{profileId}` | Delete a reading profile | + +### Key Files + +| File | Purpose | +|------|---------| +| `BookController.cs` | Book CRUD, upload pipeline, EPUB endpoints | +| `ReaderController.cs` | Progress, bookmarks, preferences, profiles | +| `reading-journey.component.ts` | Frontend library + upload + quiz UI | +| `reader-shell.component.ts` | Book reader shell (EPUB + PDF) | +| `reader.service.ts` | Frontend reader HTTP service | + +--- + +## Real-time Communication + +SpikerSoft provides three real-time communication channels: video calling, live streaming, and platform-wide chat. + +### Video Calling + +Peer-to-peer browser video calls powered by **PeerJS** (WebRTC). Signaling uses the PeerJS hosted broker; media flows directly between browsers via Google STUN servers. + +- One-on-one video calls with camera/mic controls +- Video call picker dialog for selecting contacts +- No backend server required for media relay + +| File | Purpose | +|------|---------| +| `call.service.ts` | PeerJS wrapper — peer creation, call/answer lifecycle | +| `video-call.component.ts` | Call UI with local/remote video streams | + +### Live Streaming + +Live video streaming using **OvenPlayer** (WebRTC client) connected to an **OvenMediaEngine** instance. The media server runs as a separate Docker service behind Traefik. + +- WebRTC source: `wss://stream.spikersoft.com/app/stream/` +- Low-latency streaming for events and demonstrations +- OvenPlayer embedded in the Angular `StreamComponent` + +| File | Purpose | +|------|---------| +| `stream.component.ts` | OvenPlayer integration with WebRTC source | + +### Chat + +Real-time messaging via SignalR hubs. Two hub versions exist: + +- **`ChatHub`** (`/hubs/chat`) — Original chat hub +- **`MultiTenantChatHub`** (`/hubs/chat-v2`) — Multi-tenant chat with organization-scoped rooms + +Chat is entirely hub-based with no REST API; all message exchange happens over WebSocket. + +| File | Purpose | +|------|---------| +| `ChatHub.cs` | SignalR hub for real-time messaging | +| `MultiTenantChatHub.cs` | Multi-tenant SignalR hub (v2) | +| `chat.component.ts` | Angular chat UI | +| `chat.service.ts` | SignalR connection and message handling | + +### Notifications + +A centralized notification system bridges async backend events to real-time client updates via SignalR: + +- **`NotificationsHub`** (`/hubs/notifications`) — General platform notifications +- **Calendar notifications** (`/hubs/calendar-notifications`) — Calendar-specific reminders +- **RabbitMQ bridge** — `SignalRNotificationConsumerService` forwards RabbitMQ messages to connected clients via the hub +- **Retry tiers** — Failed notifications retry through `notifications.signalr.retry.1/2/3` queues before hitting the DLQ + +Event handlers (quiz generation, embeddings, uploads, etc.) publish notifications to the `notifications.signalr` RabbitMQ queue, which the bridge forwards to the appropriate SignalR clients. + +--- + +## Event Handlers + +SpikerSoft uses a distributed event handler architecture where specialized microservices consume messages from RabbitMQ queues. Each handler is a standalone .NET worker service built on the shared `EventHandlerHostBuilder` infrastructure. + +### Handler Inventory + +| Handler | Purpose | +|---------|---------| +| **Infrastructure** | Shared library providing `EventHandlerHostBuilder` with Serilog, MongoDB, RabbitMQ, and OpenTelemetry wiring (not a runnable service) | +| **BlogMediaProcessor** | Async blog media pipeline: receive → ClamAV scan → metadata extract → strip EXIF → move to final storage | +| **BookManagement** | Book lifecycle side effects triggered by processing events | +| **CalendarReminders** | Calendar reminder notification dispatch | +| **CodeExecution** | Sandboxed user code execution worker (request/response queues, 4 concurrent consumers) | +| **DockerMonitor** | Docker host monitoring and event publishing to Redis | +| **Embeddings** | Vector embedding generation for book text chunks (LLamaSharp, GPU-scheduled) | +| **FileMovement** | File transfer between staging and final storage locations | +| **GameEvents** | Game state persistence and Redis hot-path dual-writes for the game server | +| **GpuCoordinator** | GPU VRAM lease management, queue scheduling, and `/gpu/status` HTTP API | +| **InfluxDashboard** | InfluxDB metrics ingestion and `DashboardHub` SignalR streaming | +| **KeycloakEvents** | Keycloak identity event synchronization into the platform | +| **MetadataExtractor** | PDF/EPUB metadata extraction (Aspose, VersOne.Epub) | +| **QuizGeneration** | LLM-powered quiz generation from book content (GPU-scheduled) | +| **Scheduler** | Scheduled task execution (GeoIP updates, HTTP callbacks) | +| **SecurityScanner** | Uploaded content security scanning | +| **UploadCoordinator** | Orchestrates the full book/upload lifecycle across processing stages | + +### Production Queue Architecture + +From the `/healthz` endpoint, the production RabbitMQ instance runs 9 queues: + +| Queue | Consumers | Purpose | +|-------|-----------|---------| +| `calendar.reminders` | 0 | Calendar reminder dispatch | +| `code.execution.requests` | 4 | Sandboxed code execution input | +| `code.execution.responses` | 1 | Code execution results | +| `keycloak.events` | 0 | Keycloak event sync | +| `notifications.signalr` | 1 | Real-time notification bridge to SignalR | +| `notifications.signalr.retry.1` | 0 | First retry tier (delayed) | +| `notifications.signalr.retry.2` | 0 | Second retry tier (longer delay) | +| `notifications.signalr.retry.3` | 0 | Third retry tier (longest delay) | +| `notifications.signalr.dlq` | 0 | Dead letter queue for failed notifications | + +### Key Files + +| File | Purpose | +|------|---------| +| `EventHandlerHostBuilder.cs` | Shared builder with Serilog, Mongo, Rabbit, telemetry | +| `SignalRNotificationConsumerService.cs` | RabbitMQ → SignalR notification bridge | +| `NotificationsHub.cs` | SignalR hub for client notifications | +| `DashboardHub.cs` | SignalR hub for InfluxDB metrics streaming | + +--- + +## Marks Field Service + +An integrated field-service customer management tool for tracking residential and commercial customers, their equipment (generators, propane tanks), and service history. The feature includes a Google Maps-based workflow for visualizing customer locations. + +### Features + +- **Customer CRUD** — Create, view, update residential and commercial customer records +- **Google Maps integration** — Visualize customer locations and plan service routes +- **Equipment tracking** — Generator and propane tank inventory per customer +- **Service contracts** — Track service agreements and maintenance schedules +- **Address management** — Structured addresses with postal code lookup integration + +### API Endpoints + +| Method | Endpoint | Purpose | +|--------|----------|---------| +| `GET` | `/api/mark-wilson/customer` | List all customers | +| `POST` | `/api/mark-wilson/customer` | Create customer | +| `GET` | `/api/mark-wilson/customer/{id}` | Get customer detail | +| `PUT` | `/api/mark-wilson/customer/{id}` | Update customer | + +### Key Files + +| File | Purpose | +|------|---------| +| `CustomersController.cs` | API controller for customer CRUD | +| `marks-customer.component.ts` | Customer management UI | +| `marks-map.component.ts` | Google Maps customer visualization | +| `libraries/marks-site-models/` | Shared TypeScript models | + +--- + +## Health Check & Observability + +The platform exposes a comprehensive `/healthz` endpoint that validates the entire infrastructure stack. This endpoint powers uptime monitoring and aids rapid diagnosis during incidents. + +### Health Checks (13 total) + +Registered in `SpikerSoft.Api` `AddHealthChecksConfiguration` (order in code: RabbitMQ, MongoDB, Redis Cluster, Vector Search, Keycloak, InfluxDB, Email Server, TileServerGL, DNS Server, DKIM, DMARC, SPF, Postal Code Data). + +| Check | What It Validates | +|-------|-------------------| +| **MongoDB** | Cluster reachability via `mongo-router`, ping latency, database list | +| **Redis Cluster** | All 6 nodes connected (3 masters + 3 replicas), cluster state `ok`, slot coverage | +| **RabbitMQ** | Queue health, consumer counts, message depth, DLQ status | +| **Vector Search** | Redis vector index exists (default name `idx:BookPageChunk`, overridable via `VectorSearch:IndexName`) | +| **Keycloak** | Authentication endpoint reachable at `ids.spikersoft.com` | +| **InfluxDB** | Ping, bucket existence, write/query round-trip at `influxdb.spikersoft.com` | +| **Email Server** | SMTP connection to `mail.spikersoft.com:587`, STARTTLS, authentication | +| **TileServerGL** | Terrain tile fetch from `tiles.spikersoft.com` with latency measurement | +| **DNS Server** | Self-hosted DNS at `192.168.0.105:5380` responding | +| **DKIM** | DKIM record valid for `spikersoft.com` (selector: `mail`, RSA key) | +| **DMARC** | DMARC policy active (`quarantine`) with aggregate reporting | +| **SPF** | SPF record valid with `-all` (hard fail for unauthorized senders) | +| **Postal Code Data** | MongoDB `postal-codes` collection has rows; **Degraded** if empty (seed via `POST /api/PostalCodes/seed`) — see `PostalData_HealthCheck` | + +### Observability Stack + +``` +┌─────────────────────────────────────────────────────────────┐ +│ OBSERVABILITY │ +├─────────────────────────────────────────────────────────────┤ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Serilog │ │ OpenTelemetry│ │ InfluxDB │ │ +│ │ ↓ │ │ ↓ │ │ v2.7.12 │ │ +│ │ Seq │ │ Jaeger │ │ (metrics) │ │ +│ │ (logging) │ │ (tracing) │ │ │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ /healthz — 13 infrastructure checks │ │ +│ │ RabbitMQ DLQ monitoring + retry tier escalation │ │ +│ │ Correlation IDs across all cross-service calls │ │ +│ └──────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Email Deliverability + +The health check validates the full email authentication chain: + +- **SPF** — `v=spf1 a mx ip4:204.197.150.99 -all` (hard fail policy) +- **DKIM** — RSA-signed with selector `mail` on `spikersoft.com` +- **DMARC** — `v=DMARC1; p=quarantine` with aggregate reports to `dmarc@spikersoft.com` + +This ensures `@spikersoft.com` emails (child account provisioning, notifications) are trusted by recipients and not flagged as spam. + +--- + +## TODO / Future work + +Planned or exploratory integrations not yet in the product stack: + +- **Apache Guacamole (HTML5 remote desktop)** — Run a [Guacamole](https://guacamole.apache.org/) server and pair it with a **custom Angular 21** client UI: a from-scratch or heavy rewrite of the official web client, using the [guacamole-client `next` branch frontend](https://github.com/apache/guacamole-client/tree/next/guacamole/src/main/frontend) as a behavioral and protocol reference (not necessarily a line-for-line port), aligned with SpikerSoft’s stack and theming. +- **OpenSC2K-style city sim (optional game)** — Investigate embedding or forking ideas from [OpenSC2K](https://github.com/nicholas-ochoa/OpenSC2K) (WebGL / Phaser) as a **SimCity 2000–style** learning game or creative sandbox, subject to licensing, asset, and product-fit review (the upstream project is GPL-3.0; ship only compliant code and assets). +- **PHP in the browser (WASM)** — Add a **PHP learning / playground** path parallel to the existing C# and Python storylines, using [php-wasm](https://github.com/seanmorris/php-wasm) (PHP in the browser via WebAssembly), preloaded and status-tracked like other WASM runtimes in the **Offline** panel where applicable. +- **x86-64 assembly learning (Blink WASM + Angular UI)** — Adopt the **Blink-based emulator** and tooling approach from [x86-64-playground](https://github.com/robalb/x86-64-playground) (assembly editor, emulator, and debugger-style experience for the x86-64 Linux model in the browser). **Reuse their WASM** (the packaged Blink build / integration pattern) as the execution layer; **re-implement the front end natively in Angular 21** (the upstream app is Svelte + Vite) so the experience matches SpikerSoft UI patterns and theming, subject to license review of upstream artifacts. +- **TypeScript curriculum (extension of the JavaScript track)** — JavaScript playground + lessons are already shipping (Node server + QuickJS-WASM browser). The follow-on TypeScript curriculum reuses the same runtime via `LessonMetadata.Preprocessor = LessonPreprocessor.TypeScript`: both Node and the QuickJS worker call `ts.transpileModule` before grading. Add a **"Transpile to JavaScript"** action in TypeScript lessons that uses the **TypeScript compiler in the browser** (same API as the canonical [`typescript` package](https://www.npmjs.com/package/typescript) ships in `lib/typescript.js` — the implementation currently referenced at [`unpkg.com/typescript@latest/lib/typescript.js`](https://unpkg.com/typescript@latest/lib/typescript.js)). **Dependency policy:** add `typescript` as a **first-party pnpm dependency** and serve the browser bundle from our own build/static assets (or vendor the built file in-repo), so **no runtime dependency on a third-party CDN** for the compiler; unpkg is only the upstream reference for which artifact to align with. + +--- + +## Contributing + +1. Follow the coding guidelines in `.cursor/rules/` +2. Write tests for new features +3. Update documentation as needed +4. Submit pull requests for review + +--- + +## License + +Licensing is **not uniform across the monorepo**; use the SPDX / files in each part of the tree: + +- **Angular app** ([`spikersoft-angular/package.json`](spikersoft-angular/package.json)): `"license": "AGPL-3.0"`. +- **Backend** ([`spikersoft-backend/README.md`](spikersoft-backend/README.md)): see that file and, when present, `LICENSE.md` next to [`spikersoft-backend/SpikerSoft.sln`](spikersoft-backend/SpikerSoft.sln) (the solution lists it as a solution item). + +Earlier text here referred to an “Academic License” and a single `LICENSE` at repo root — **there is no shared root license file in this workspace layout**; rely on the paths above. + +--- + +*Bringing families together through exploration -- bridging technology and the outdoors so every child can learn by doing, sponsor by caring, and grow by adventuring.* +