Files
2026-05-09 15:21:12 -04:00

138 KiB
Raw Permalink Blame History

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 — ~101-lesson curriculum graded by CPython subprocess server-side and Pyodide in the browser.
  • JavaScript Playground138-lesson curriculum (10 orientation tutorials + 128 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.
  • Regex Playground — 12-chapter curriculum graded fully in-browser against a pre-computed RegexLessonPlan and re-verified server-side through Node (not .NET) to preserve JavaScript regex flavor. See Regex Playground & Curriculum below.

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/ 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-20007 Welcome tutorials (intro, modules, functions, print, first submit, how the test harness works)
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, dict dispatch, for / while / iteration (match/case is now introduced in Tier 13 patterns)
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-20605 slicing, split/join, replace/case, ''.join accumulation, isdigit() validation, f-strings (canonical)
Tier07_Classes 20699-20707 decorator-syntax recipe, 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/ and use the 3000031999 lesson-number range with DisplayNumber = LessonNumber - 30000 so the sidebar shows clean 1..N badges. See .cursor/rules/javascript-curriculum.mdc for the locals-first / Node↔QuickJS parity rules:

Tier Numbers Topic
Tier00_Welcome 30001-30010 Welcome tutorials (history of JS, engines, browser vs Node, console basics, code style, reading errors, first submit, how the test harness works, function recipe)
Tier01_Foundations 30100-30109 console.log, numbers, strings, booleans, null/undefined, typeof, let vs const, template literals (preview), 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-30608 length/index, case, slice, includes, replaceAll, split, trim, padStart, template literals (canonical)
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 31099-31104 ESM export/import recipe, IIFE module, Symbol.iterator, Symbol keys, Object.freeze, namespace export
Tier12_Errors 31200-31204 throw/catch, finally, custom Error, name+message, re-throw (errors-in-async moved to Tier 13)
Tier13_Async 31300-31310 callbacks, Promise basics, .then chains, resolve/reject, async, await, all/race/allSettled, sequential vs parallel, errors in async functions
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

Pedagogical contract: no concept used before introduction

Every challenge lesson is reachable from "Hello, World" using only language features that were formally introduced (and graded) in an earlier lesson. A student who works the curriculum top-to-bottom never has to leave the platform to look up a syntactic form they have not been taught — if the reference solution uses an f-string, decorator, optional chain, or generator expression, an earlier lesson exists that puts that exact construct in front of them as a "recipe to copy". Reference solutions for every gradable challenge are checked in alongside the lesson and executed end-to-end on every CI run (ReferenceSolutionPasses in SpikerSoft.Tests.Unit), so a contract violation surfaces as a red test, not as a confused student.

The audit findings that drove the most recent Python and JavaScript reorderings are living documents in docs/curriculum/python-curriculum-audit.md and docs/curriculum/javascript-curriculum-audit.md. Re-run the same audit when adding a new tier; the verification gate is dotnet test SpikerSoft.Tests.Unit --filter "ReferenceSolutionPasses".

Regex Playground & Curriculum

The fourth language track is a 12-chapter regular-expressions curriculum under SpikerSoft.Business/Domain/Lessons/Curriculum/Regex/ (Chapter01_LiteralsChapter12_Recipes, covering literals, special characters, character classes, quantifiers, anchors, alternation, capturing groups, back-references, replace, lookarounds, named groups + Unicode property escapes, and idiomatic recipes). Lessons are written as RegexLessonStrategyBase subclasses that hand the SPA a serialized RegexLessonPlan — test text plus expected matches/replacements row-by-row.

Grading mechanics differ deliberately from the C# / Python / JavaScript tracks:

  • Live grading runs entirely in the browser. RegexLessonRunnerService and RegexLessonGraderService (in libraries/features/dev-tools-reg-ex) feed the student's pattern through the browser's native RegExp against the plan's test text and emit row-level pass/fail just like the JS / Python harnesses do.
  • Server re-verification routes through Node, not .NET. When an offline-graded regex submission flushes through POST /api/Lessons/progress/batch, RegexLessonGradingExecutor builds a JS harness via RegexLessonHarnessBuilder and runs it on the worker's Node subprocess (the same JavaScriptLessonExecutor plumbing the JS Playground uses). This is not a casual choice — JavaScript regex differs from .NET's System.Text.RegularExpressions in ways that matter for the curriculum: the v-flag set operations [[a-z]--[aeiou]] / &&, JS-only replacement patterns $` / $', and the supported \p{...} Unicode property names all diverge between engines. Re-grading through Node guarantees the server records the same pass/fail the student saw locally.
  • No second runtime in the browser. Regex lessons need no Pyodide, no QuickJS, no .NET WASM — the SPA already has RegExp. Offline support is automatic.

Server-side regex re-grading rides the Offline Lesson Re-Grading via Worker RPC pipeline (below), so a code-runner crash does not take the API down. SPA-side details and the in-app "Lesson" pane / cheatsheet UI live in libraries/features/dev-tools-reg-ex/README.md.

WASM exclude. SpikerSoft.Wasm.csproj excludes Regex*.cs from the link-included executor sources — regex re-grading is server-only by design.

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:

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 link-includes Domain/CodeExecution/Execution/**/*.cs from SpikerSoft.Business, but excludes server-only files via a list (Python*, JavaScript*, Regex*, executor/factory abstractions, RoslynLessonGradingExecutor). 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.

Offline Lesson Re-Grading via Worker RPC

Browser graders (SpikerSoft.Wasm for C#, Pyodide for Python, QuickJS for JavaScript, the SPA's native RegExp for regex) are convenient and work entirely offline, but they run on the student's machine and are tamperable. Before the platform credits a skill unlock or marks a lesson complete for-real, the server has to re-execute the same lesson against the same grader and confirm it actually passed. That's the trust boundary the offline IndexedDB queue crosses every time it flushes through POST /api/Lessons/progress/batch.

The architectural rule: code execution NEVER runs in the API process. If the code-runner container crashes mid-grade, the API stays up; a fresh client retry round-trips through Rabbit on the next batch flush. This is enforced by the API's DI graph — only ILessonRegradeClient is registered there; no ILessonGradingExecutor implementation is reachable from SpikerSoft.Api.

The flow: LessonsController.BatchProgress builds a LessonRegradeRequest for each queued submission and hands it to LessonRegradeClient, which publishes it to the lesson.regrade.requests exchange and blocks on the worker's reply via RabbitMQ Direct-Reply-To (amq.rabbitmq.reply-to). The worker's LessonRegradeWorkerHostedService consumes the request, dispatches through the same ILessonGradingExecutorFactory that backs the live "Submit" path (Roslyn / CPython / Node / Regex-via-Node — no code duplication between live-submit and regrade), and publishes a LessonRegradeResponse back to the API's reply queue.

sequenceDiagram
    autonumber
    participant SPA as Angular SPA
    participant API as SpikerSoft.Api
    participant MQ as RabbitMQ
    participant Worker as SpikerSoft.EventHandlers.CodeExecution
    Note over SPA: Student grades offline (in-browser)<br/>Submission queued in IndexedDB
    SPA->>API: POST /api/Lessons/progress/batch
    Note over API: For each queued attempt
    API->>API: Build LessonRegradeRequest<br/>(lessonNumber, attemptToken,<br/>studentCode, testCode, runtime)
    API->>MQ: publish to lesson.regrade.requests<br/>reply-to = amq.rabbitmq.reply-to
    MQ->>Worker: deliver request
    Note over Worker: ILessonGradingExecutorFactory<br/>picks Roslyn / CPython / Node / Regex
    Worker->>Worker: Execute & grade
    Worker->>MQ: publish LessonRegradeResponse<br/>to API's Direct-Reply queue
    MQ->>API: deliver response
    alt Response received within timeout
        API->>API: Persist progress if AllTestsPassed
        API->>SPA: 200 OK with per-attempt status
    else Timeout (worker down / overloaded)
        API->>SPA: 5xx server_error<br/>(IndexedDB keeps the attempt; SPA retries)
    end

Idempotency. Each request carries the SPA-generated attemptToken. If the worker grades a token, the API records it; if a network blip causes the SPA to retry, the controller short-circuits on the duplicate token rather than re-publishing — so the worker never regrades the same attempt twice.

Timeout handling. LessonRegradeClient enforces a per-call timeout (default 30 s). On expiry, the call throws TimeoutException, the controller maps it to HTTP server_error, and the SPA leaves the offline attempt in IndexedDB so the next batch flush picks it up. The same attempt token is re-used so the worker still de-dupes correctly when it eventually catches up.

Same factory, same code. Both worker consumer paths (live-submit code.execution.requests and offline-regrade lesson.regrade.requests) dispatch through ILessonGradingExecutorFactory. There is no separate "regrade" code path that could drift from the live grader — fixing a hint in RoslynLessonGradingExecutor fixes both modes simultaneously.


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<T>, Dictionary<K,V>, 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" ~150400 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 ~3060 ms warm; first boot 815 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 (<WasmEnableWebcil>false</WasmEnableWebcil>) 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 submitcompletedLessonsSignal 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 badgeReady 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# playgrounds 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 TutorialPanels — 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:

# 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 runners 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 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 hosts 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). (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/features/dev-tools-csharp-runner/src/lib/wasm-runtime.worker.ts Hosts the .NET runtime on a worker thread
Path-routing service libraries/features/dev-tools-csharp-runner/src/lib/csharp-runner.service.ts Server↔WASM routing with auto-failover
Language-agnostic shell libraries/platform/language-runner/src/lib/language-runner.ts Shared playground SHELL composed by all 3 wrapping shells (C#/Python/JS) via LANGUAGE_RUNNER_CONFIG + LANGUAGE_RUNTIME_ADAPTER
C# wrapping shell libraries/features/dev-tools-csharp-runner/src/lib/csharp-runner.ts Wraps the language-runner with the C#-specific LANGUAGE_RUNNER_CONFIG and csharp-runtime.adapter.ts
Lesson sidebar libraries/shared/lesson-panes/src/lib/lesson-sidebar/ Tier-grouped lesson list with prereq-driven lock state (shared across all 3 playgrounds)
Offline queue libraries/platform/progress-sync/src/lib/progress-sync.service.ts IndexedDB enqueue + flush-on-reconnect (consumed via PROGRESS_SYNC_PORT so the platform shell stays decoupled)
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.


Frontend Architecture: Feature-Oriented Boundaries

The Angular workspace was migrated through a multi-phase refactor (Phases 46, completed 2026-05-09 — see docs/architecture/inventory.md and docs/architecture/boundaries.md) from a monolithic libraries/tools/ lib into ~65 buildable libraries grouped by layer. Each library has its own ng-package.json, vitest.config.ts, project.json, and tests run isolated. Lazy-loaded routes per feature → smaller initial bundle (the Phase 6 sub-phases collectively brought the gzipped main.js to ~302 kB).

Layers and clusters

Every library carries at least one layer:* tag in its project.json. The boundary categories on disk are:

Cluster Layer tag Count Purpose
libraries/domain/ layer:domain 9 Pure data models + thin services bound to backend resources, no UI: blog, book, child-account, fundraiser, geo, mrz, pre-registration, profile, sponsor
libraries/features/ layer:feature 24 Top-level user-facing features that own routes/components/services: the entire dev-tools-* suite (21 tools — C#/Python/JavaScript/regex/x86 playgrounds, decompiler, encoding/conversion, image-to-avif/ico, qr-code, voronoi, diff, diagram, blockly, duckdb, ipv4, quick-type, common-commands), blog, child-account-dialog, fundraiser, games-clue-for-sql, parent-dashboard, sponsor, trellis-3d-generator
libraries/platform/ layer:platform 13 Cross-cutting infrastructure consumed by features: language-runner, lesson-catalog, progress-sync, pyodide-runtime, monaco-editor, tool-storage, tool-file-menu, intro-tour, intro-launching, js-step-debugger, activity-tracking, loading, avif-encoder
libraries/shared/ layer:shared 6 Small reusable units that don't belong to a single feature/domain: api-config, js-formatting-options, lesson-panes, lesson-platform, save-load-dialogs, utils/{crc32,file-hash}
libraries/ui/ layer:ui 2 Pure presentational atoms: confirm-dialog, mrz-crop
libraries/game/ layer:game 1 Game-runtime libs that pair a feature with heavy runtime deps: wasm-voxel
Top-level legacy various 5 Pre-Phase-4 libs that haven't been re-clustered into the new directories yet but already carry the right layer tag: keycloak-admin (layer:feature), marks-site-models + spikersoft-models (layer:domain), spikersoft-environment (layer:platform), spikersoft-theme (layer:ui)

Allowed dependency arrows

Enforced by @nx/enforce-module-boundaries (severity error since Phase 5) on every PR. The full constraint set lives in spikersoft-angular/eslint.config.cjs; the abridged version a feature author cares about:

Source layer May depend on
app feature, ui, platform, domain, runtime, game, editor, shared, generated
feature feature, domain, ui, runtime, game, editor, platform, shared, generated
ui ui, platform, shared
domain domain, shared, generated
platform platform, shared
game game, runtime, shared
shared shared

The shape of the rule, in one diagram:

flowchart TB
    app[app: projects/spikersoft]
    feature[feature: libraries/features, libraries/keycloak-admin]
    domain[domain: libraries/domain, libraries/marks-site-models, libraries/spikersoft-models]
    platform[platform: libraries/platform, libraries/spikersoft-environment]
    ui[ui: libraries/ui, libraries/spikersoft-theme]
    game[game: libraries/game]
    shared[shared: libraries/shared]

    app --> feature
    app --> domain
    app --> platform
    app --> ui
    app --> game
    app --> shared

    feature --> domain
    feature --> platform
    feature --> ui
    feature --> game
    feature --> shared

    ui --> platform
    ui --> shared
    domain --> shared
    platform --> shared
    game --> shared

The arrows go only downward. A domain lib that tries to import from feature or platform fails CI; platform cannot import feature; shared cannot import anything but shared. This is what the boundary refactor bought: the dep graph is now mechanically prevented from collapsing back into the old "everything imports tools" shape.

Cross-cluster ports

Where a feature genuinely needs to consume a service from another cluster without violating the arrows, the lib pair uses a port (an injection token) declared in shared/lesson-platform and provided at the wrapping shell. Examples:

  • PROGRESS_SYNC_PORT — consumed by platform/language-runner so the shared playground shell never hard-imports the Monaco-coupled ProgressSyncService concrete (the feature/dev-tools-csharp-runner shell wires it up at provider time).
  • AVIF_ENCODER port — domain/blog and feature/dev-tools-image-to-avif consume the encoder through a port declared in shared, so the domain lib never depends directly on platform/avif-encoder.

If a new feature seems to require an arrow the layering forbids, the right answer is almost always to extract a port or a contract type into shared/lesson-platform (or its sibling shared/api-config) rather than to widen the rule. See docs/architecture/boundaries.md for the canonical write-up of the rule, baseline counts, and the burn-down protocol that closed the last ~22 violations.


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/                  # Buildable Nx libs grouped by layer (see Frontend Architecture section)
│   ├── domain/                 # layer:domain — pure data + thin services bound to backend resources (9 libs)
│   │   ├── blog/  book/  child-account/  fundraiser/  geo/
│   │   └── mrz/  pre-registration/  profile/  sponsor/
│   ├── features/               # layer:feature — top-level user features w/ routes (24 libs)
│   │   ├── dev-tools-csharp-runner/    # C# Playground wrapping shell (config + adapter)
│   │   ├── dev-tools-python-runner/    # Python Playground wrapping shell
│   │   ├── dev-tools-javascript-runner/# JS Playground wrapping shell
│   │   ├── dev-tools-reg-ex/           # Regex Playground (chapters + cheatsheet)
│   │   ├── dev-tools-x86-playground/  ...# 21 dev-tools-* + blog, sponsor, fundraiser, ...
│   │   └── …                            # see libraries/features/ for full list
│   ├── platform/               # layer:platform — cross-cutting infra (13 libs)
│   │   ├── language-runner/    # Language-agnostic playground SHELL (consumed by 3 wrapping shells)
│   │   ├── lesson-catalog/     # Lesson catalog + prerequisites
│   │   ├── progress-sync/      # Server↔in-browser router + offline IndexedDB queue
│   │   ├── pyodide-runtime/    # Pyodide loader (Python in-browser)
│   │   ├── monaco-editor/  tool-storage/  tool-file-menu/  intro-tour/  intro-launching/
│   │   └── js-step-debugger/  activity-tracking/  loading/  avif-encoder/
│   ├── shared/                 # layer:shared — small reusable units (6 libs)
│   │   ├── lesson-platform/    # Cross-cluster contracts (PROGRESS_SYNC_PORT, SourceFile, ...)
│   │   ├── lesson-panes/       # Lesson + tutorial + sidebar panes
│   │   ├── api-config/  js-formatting-options/  save-load-dialogs/
│   │   └── utils/              # crc32, file-hash
│   ├── ui/                     # layer:ui — presentational atoms (2 libs)
│   │   └── confirm-dialog/  mrz-crop/
│   ├── game/                   # layer:game — game runtime libs (1 lib)
│   │   └── wasm-voxel/         # TypeScript Minecraft port (voxel engine)
│   │
│   │  # Legacy top-level libs (pre-Phase-4 boundary refactor; tagged but not re-clustered)
│   ├── keycloak-admin/         # layer:feature — Keycloak administration UI
│   ├── marks-site-models/      # layer:domain — Shared models for field-service customer management
│   ├── spikersoft-environment/ # layer:platform — Environment configuration
│   ├── spikersoft-models/      # layer:domain — Shared TypeScript models
│   └── spikersoft-theme/       # layer:ui — Glassmorphic theme + styling utilities
├── 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

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

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

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 1017 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 1011 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:

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:

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 (e.g. future work that mirrors the 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 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)

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

# 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
Robot & Visual Programming See main README — Robot & Visual Programming and ARCHITECTURE-ROBOT-VISUAL-PROGRAMMING.md
C# Coding Curriculum & Playground See main README — C# Coding Curriculum & Playground; WASM bundle: spikersoft-angular/projects/spikersoft/src/assets/dotnet/README.md; Wrapping shell: spikersoft-angular/libraries/features/dev-tools-csharp-runner/README.md; Shared playground SHELL: spikersoft-angular/libraries/platform/language-runner/README.md
Event Handlers See main README — 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 section
Dungeon Crawler spikersoft-angular/projects/spikersoft/src/app/_components/_games/dungeon-crawler/README.md
Team & Hiring See main README — Team Page, Hiring Page, Data-Driven Locations sections
Sponsorship See main README — Sponsorship & Donation System
Fundraisers See main README — Fundraiser System
Info Vault See main README — Info Vault
Blog System See main README — Blog System
Reading Journey See main README — Reading Journey & Book System
Real-time Communication See main README — Real-time Communication
Marks Field Service See main README — Marks Field Service
Health & Observability See main README — 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:

SubmittedUnderReviewBackgroundCheckPendingBackgroundCheckCompleteApprovedHired

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 API-first platform; industry standard for US background checks. Requires API key from Checkr dashboard.
Sterling International (Latin America, Canada, etc.) 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.

"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/ Creative Commons Attribution 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:

# Seed all operating countries
curl -X POST https://your-api/api/PostalCodes/seed -H "Authorization: Bearer <admin-token>"

# Seed a single country
curl -X POST "https://your-api/api/PostalCodes/seed?countryCode=US" -H "Authorization: Bearer <admin-token>"

Attribution

This product includes data from GeoNames (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:

"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.
  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 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:

# 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 (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 bridgeSignalRNotificationConsumerService 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:

  • SPFv=spf1 a mx ip4:204.197.150.99 -all (hard fail policy)
  • DKIM — RSA-signed with selector mail on spikersoft.com
  • DMARCv=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 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 as a behavioral and protocol reference (not necessarily a line-for-line port), aligned with SpikerSofts stack and theming.
  • OpenSC2K-style city sim (optional game) — Investigate embedding or forking ideas from OpenSC2K (WebGL / Phaser) as a SimCity 2000style 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 (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 (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 ships in lib/typescript.js — the implementation currently referenced at 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:

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.