# Neuronetz Finetuning Platform — Developer Manual > **Repository:** `ssh://git@gitea.neuronetz.ai:222/Neuronetz/finetuning-plattform.git` > **This delta:** `ssh://git@gitea.neuronetz.ai:222/m17hr1l/finetuning-plattform-setup-delta.git` > **Generated from:** `develop` @ `70b203c`, 2026-05-14 > **Audience:** Senior backend and frontend developers joining the project. --- ## Table of Contents ### Part I — Get Running 1. [Quick Start (10 min)](#1-quick-start-10-min) 2. [Full Setup](#2-full-setup) 3. [First Login & Sanity Checks](#3-first-login--sanity-checks) 4. [Troubleshooting Setup](#4-troubleshooting-setup) ### Part II — The Platform 5. [What This Platform Does](#5-what-this-platform-does) 6. [Architecture Overview](#6-architecture-overview) 7. [The Service Stack](#7-the-service-stack) 8. [The Nibiru Framework](#8-the-nibiru-framework) 9. [Module-Model-View-Controller (MMVC)](#9-module-model-view-controller-mmvc) 10. [Database](#10-database) 11. [API Surface](#11-api-surface) 12. [Authentication & Sessions](#12-authentication--sessions) 13. [Background Work](#13-background-work) 14. [Model Serving & Inference](#14-model-serving--inference) 15. [Frontend](#15-frontend) ### Part III — Working in the Codebase 16. [Hard Rules](#16-hard-rules) 17. [Git Workflow](#17-git-workflow) 18. [Code Style](#18-code-style) 19. [Naming Conventions](#19-naming-conventions) 20. [Testing](#20-testing) 21. [Common Gotchas](#21-common-gotchas) ### Part IV — Operations 22. [Tools & Dashboards](#22-tools--dashboards) 23. [Logging & Observability](#23-logging--observability) 24. [Troubleshooting Runbooks](#24-troubleshooting-runbooks) 25. [Regenerating This Delta](#25-regenerating-this-delta) ### Part V — Context & Culture 26. [Regulatory Stance (Why No GDPR Theater)](#26-regulatory-stance-why-no-gdpr-theater) 27. [The Multi-Agent Orchestrator (Optional)](#27-the-multi-agent-orchestrator-optional) 28. [Glossary](#28-glossary) --- # Part I — Get Running ## 1. Quick Start (10 min) For experienced devs who want the platform running and `git log` open in 10 minutes: ```bash # 1) Add to /etc/hosts (one line, all subdomains) echo "127.0.0.1 local.finetune.neuronetz.ai local.websocket.finetune.neuronetz.ai local.redis-commander.neuronetz.ai local.graylog.finetune.neuronetz.ai local.kibana.finetune.neuronetz.ai" | sudo tee -a /etc/hosts # 2) Clone both repos as siblings mkdir -p ~/projects && cd ~/projects git clone ssh://git@gitea.neuronetz.ai:222/Neuronetz/finetuning-plattform.git git clone ssh://git@gitea.neuronetz.ai:222/m17hr1l/finetuning-plattform-setup-delta.git cd finetuning-plattform && git checkout develop # 3) Start the stack cd local && docker compose up -d cd ../.. # 4) Bootstrap the DB cd finetuning-plattform-setup-delta ./bootstrap-db.sh # 5) Done — open the platform xdg-open http://local.finetune.neuronetz.ai/auth/login # login: admin@finetune.ai / admin123 ``` If anything errors, jump to [§4 Troubleshooting Setup](#4-troubleshooting-setup) or [§21 Common Gotchas](#21-common-gotchas). --- ## 2. Full Setup ### 2.1 Prerequisites (on your host) - **Docker** ≥ 24 with the **compose plugin** v2.20+. Verify: `docker compose version` must succeed (the legacy `docker-compose` script will NOT work — the platform's scripts assume the plugin form). - **Git** with SSH access to `gitea.neuronetz.ai`. Stephan adds your public key to the `Neuronetz` org. - **NVIDIA GPU + driver + nvidia-container-toolkit** if you want to actually run inference (Ollama or llama-server). Without one, set `OLLAMA_GPU_LAYERS=0` in `local/.env` and use CPU inference (slow but functional). - **~30 GB free disk** for images + model downloads. - Linux or macOS. Windows works only via WSL2 + Docker Desktop. ### 2.2 SSH key ```bash ssh-keygen -t ed25519 -C "your@email.example" # if you don't have one cat ~/.ssh/id_ed25519.pub # send this to Stephan ``` Test: ```bash ssh -p 222 -T git@gitea.neuronetz.ai # expected: "Hi ! You've successfully authenticated, ..." ``` ### 2.3 Hosts file The platform uses subdomains routed through nginx-proxy. Add this block to `/etc/hosts`: ``` 127.0.0.1 local.finetune.neuronetz.ai 127.0.0.1 local.websocket.finetune.neuronetz.ai 127.0.0.1 local.redis-commander.neuronetz.ai 127.0.0.1 local.graylog.finetune.neuronetz.ai 127.0.0.1 local.kibana.finetune.neuronetz.ai ``` If you'll touch the multi-agent orchestrator (rare, see [§27](#27-the-multi-agent-orchestrator-optional)): ``` 127.0.0.1 backend.finetune.neuronetz.ai 127.0.0.1 uiux.finetune.neuronetz.ai 127.0.0.1 qa.finetune.neuronetz.ai 127.0.0.1 devops.finetune.neuronetz.ai 127.0.0.1 control.finetune.neuronetz.ai 127.0.0.1 fullstack.finetune.neuronetz.ai ``` ### 2.4 Clone ```bash mkdir -p ~/PhpstormProjects && cd ~/PhpstormProjects git clone ssh://git@gitea.neuronetz.ai:222/Neuronetz/finetuning-plattform.git git clone ssh://git@gitea.neuronetz.ai:222/m17hr1l/finetuning-plattform-setup-delta.git ``` The two repos must be siblings — `bootstrap-db.sh` finds the platform repo by relative path `../finetuning-plattform`. You can pass an explicit path if your layout differs: ```bash ./bootstrap-db.sh /custom/path/to/finetuning-plattform ``` ### 2.5 Branch ```bash cd finetuning-plattform git checkout develop git pull ``` The platform uses **gitflow**: `develop` is the live integration branch, `main` is for releases. Always start feature work from `develop`. Never merge directly into `main`. ### 2.6 Environment file `local/.env` is committed for local-dev defaults — open and skim it. Key vars: | Var | Purpose | Default | |---|---|---| | `MARIADB_USER` / `MARIADB_PASSWORD` | DB credentials | committed | | `OLLAMA_API_URL` | Internal Ollama endpoint | `http://ollama:11434` | | `HUGGINGFACE_API_URL` | HF Hub base | `https://huggingface.co` | | `HUGGINGFACE_API_TOKEN` | Your personal HF token | **empty by default** — set if you need gated models (Llama, etc.) | | `WORKER_*` | Job queue tunables | sensible defaults | | `NFP_LLAMA_GPU_LAYERS` | GPU offload for C++ inference server | `99` (= all layers on GPU; reduce if you OOM) | If you set a real HF token, **do not commit it**. `.gitignore` covers `.claude/credentials.json` and `.claude/youtrack-credentials.json` — extend it if you add other secret files. ### 2.7 Start the stack ```bash cd local docker compose up -d ``` First run pulls ~10 GB of images and takes 5–10 minutes. Subsequent starts are seconds. Verify: ```bash docker compose ps ``` Expect ~14 services running. The `llama-server` service is currently disabled via `profiles: [disabled]` — that's intentional, see [§14](#14-model-serving--inference). ### 2.8 Bootstrap the database From this delta repo: ```bash cd ~/PhpstormProjects/finetuning-plattform-setup-delta ./bootstrap-db.sh ``` The script: 1. Sources `local/.env` from the platform repo to get DB credentials. 2. Waits for MariaDB to accept connections. 3. Loads schema, seed, default users in order. 4. Prints the login credentials. If you prefer the migration path (slower, but matches how the platform self-migrates in CI/production): ```bash cd ~/PhpstormProjects/finetuning-plattform docker compose -f local/docker-compose.yml exec fpm ./nibiru -mi local ``` This runs every `app/src/application/settings/config/database/NNN-*.sql` in numerical order. Use it after pulling new migrations from develop. --- ## 3. First Login & Sanity Checks ```bash curl -sS -o /dev/null -w "%{http_code}\n" http://local.finetune.neuronetz.ai/ # 200 ``` Open `http://local.finetune.neuronetz.ai/auth/login`. | Role | Login | Password | |---|---|---| | Superuser | `admin@finetune.ai` | `admin123` | | Regular user | `testuser@example.com` | `test123` | **These are dev-only credentials.** Change them on any deployment that isn't your laptop. ### Sanity checklist - `/dashboard` loads → ✓ - `/models` → Pulled Models tab lists the 9-ish Ollama models the shared instance has → ✓ - `/admin` (as admin) → billing dashboard, no 500 → ✓ (this 500'd yesterday, [§24](#24-troubleshooting-runbooks) explains) - `/datasets` → ✓ - `/jobs` → ✓ - `/chat` → can chat with any chat-capable model from the list → ✓ If any of these 500, check `docker compose logs fpm | tail -50`. --- ## 4. Troubleshooting Setup | Symptom | Cause | Fix | |---|---|---| | `docker compose: command not found` | Legacy `docker-compose` script installed, not the plugin | Install Docker Engine ≥ 24 with `docker-compose-plugin` | | `Permission denied` cloning | Your SSH key isn't on Gitea | Ask Stephan to add the public key | | Stack starts but `fpm` is unhealthy | Usually means `.env` is missing a var | `docker compose logs fpm | tail -50` shows what's missing | | `bootstrap-db.sh` says "MariaDB not running" | Container takes 10–30s to be ready after `up -d` | Wait longer, then re-run | | Platform returns 502 from nginx | `fpm` crashed | `docker compose restart fpm`; if it crashes again, capture logs and grep for `Fatal error` | | `llama-server` fails to build | Known: NFP-51 vs current llama.cpp API | Ignore — the service is profile-disabled. Other services work without it. | | `composer install` fails with PHP 8.3 platform check error | Stale `composer.lock` (NFP-22 work in progress) | Use the bundled `phpunit-11.phar` for tests; ignore composer until the lock is regenerated. | --- # Part II — The Platform ## 5. What This Platform Does The Neuronetz Finetuning Platform is a self-hosted alternative to OpenAI fine-tuning APIs. End users: 1. **Upload datasets** (JSONL with prompt/completion or instruction format) 2. **Pick a base model** (anything on HuggingFace as GGUF, or already pulled into the local Ollama) 3. **Configure a training job** (epochs, batch size, LoRA rank, learning rate — or use presets like `quick` / `standard` / `thorough`) 4. **Run the job** — the platform spawns a training container, streams progress via WebSocket 5. **Test the result** — chat with the fine-tuned model in-browser 6. **Deploy** — serve the model on an Ollama instance accessible from a stable URL (`api.neuronetz.ai` in production) The platform also includes user management, billing/credits, REST API for headless usage, an admin dashboard, and a multilingual UI (EN, DE, ES, FR, IT, JA, NL, PL, PT). Eventually (NFP-51 + NFP-52) the inference engine moves from Ollama → a custom C++ llama.cpp server with multi-model handling. That's Stephan's in-flight work; you'll inherit it. ## 6. Architecture Overview ``` ┌───────────────────────────────────────────────┐ │ USER (browser) │ └───────────┬────────────────────┬──────────────┘ │ HTTPS │ WSS ▼ ▼ ┌───────────────┐ ┌──────────────────┐ │ nginx-proxy │ │ websocket server │ └───────┬───────┘ │ (Workerman) │ │ └──────────────────┘ ▼ ▲ ┌──────────────────┐ │ │ php-fpm 8.3 │───────────┘ progress msgs │ Nibiru Framework │ └─┬──┬──┬──┬──┬──┬─┘ │ │ │ │ │ │ ┌────┘ │ │ │ │ └────┐ ▼ ▼ │ │ ▼ ▼ MariaDB Redis │ │ Memcached ES │ │ │ └──→ container-manager (Python, Docker socket) │ │ │ └──→ docker daemon │ │ └──→ HTTP /api/* ▼ Ollama containers (shared + per-user) Training containers (on-demand, GPU) ┌───────────────────────────────────────────────┐ │ job-worker │ │ (Workerman, polls DB for queued jobs, │ │ dispatches via container-manager, │ │ monitors progress, streams to websocket) │ └───────────────────────────────────────────────┘ ``` Two principles drive the design: 1. **PHP never calls Docker directly.** The fpm container has no `docker` CLI by design. All container operations route through the `container-manager` microservice (Python) which has the Docker socket mounted. This is the most important invariant — violating it produces silent failures or 500s. 2. **The Nibiru framework owns the dispatch.** Controllers are thin; modules are fat. Routes are configured in INI files; the framework handles all the wiring. Don't fight it. ## 7. The Service Stack Every service in `local/docker-compose.yml`: | Service | Image / Build | Port | Purpose | |---|---|---|---| | `nginx` | `nginx:alpine` | 80, 443 | Reverse proxy, routes subdomains to fpm / websocket / etc. | | `fpm` | custom (PHP 8.3.9 + extensions) | 9000 (internal) | Application server. Runs Nibiru. | | `mariadb` | `mariadb:10.11` | 3306 | Primary data store | | `redis` | `redis:7-alpine` | 6379 | Sessions, queue, cache | | `redis-commander` | `rediscommander/redis-commander` | 8081 | Redis UI at `local.redis-commander.neuronetz.ai` | | `memcached` | `memcached:1.6` | 11211 | Secondary cache (some legacy plugins use it) | | `elasticsearch` | `elasticsearch:7.10.2` | 9200 | Search backend | | `kibana` | `kibana:7.10.2` | 5601 | ES log visualization at `local.kibana.finetune.neuronetz.ai` | | `mongo` | `mongo:6` | 27017 | Graylog backend | | `graylog` | `graylog/graylog:5.0` | 9000 | Centralized log aggregation at `local.graylog.finetune.neuronetz.ai` | | `container-manager` | custom (Python + Flask) | 8080 | Docker socket proxy used by PHP | | `websocket` | custom (Workerman) | 2346 | Real-time progress streaming | | `job-worker` | custom (PHP 8.2 CLI + Workerman) | — | Background job dispatcher | | `ollama` | `ollama/ollama:latest` | 11434 (internal) | Local LLM inference | | `llama-server` | custom (C++ + CUDA) | 8090 | **PROFILE-DISABLED** — broken, see NFP-51 | All services join the `ai-network` Docker network so they can address each other by service name (e.g. PHP talks to `http://container-manager:8080`). ## 8. The Nibiru Framework Nibiru is Stephan Kasdorf's PHP MMVC framework. He built it over 8 years; it's the stable base for several products (this platform, others under `~/PhpstormProjects/tpms-*`, `~/PhpstormProjects/Nibiru-Agent`). The platform code lives in `app/src/application/`; the framework lives in `app/src/core/`. ### 8.1 What you need to know about the framework - **It's FROZEN.** `app/src/core/` is off-limits to all contributors. Any PR touching it is auto-rejected. The only exception was NFP-18 (argon2id), explicitly approved by Stephan, and that was the **last** exception. A core refactor ticket (NFP-55) tracks moving auth logic into pluggable strategies so future work doesn't need core changes. - **It has two autoloaders, both running:** - `core/c/auto.php` — Nibiru's native class autoloader, loads framework + module classes via a module registry - `core/l/autoload.php` — Composer PSR-4 autoloader, loads third-party packages from `core/l/` (the composer `vendor-dir`) - Both are bootstrapped automatically. **Never create a separate `vendor/autoload.php`** anywhere in the project. If a new PHP process (worker, cron) needs PHP packages, use the framework's existing autoloader. - **The CLI is `./nibiru`** (a compiled binary at the project root inside the container). Common commands: ```bash docker compose exec fpm ./nibiru -c # scaffold a new controller + template docker compose exec fpm ./nibiru -m # scaffold a new module (4 files) docker compose exec fpm ./nibiru -mi local # run pending migrations docker compose exec fpm ./nibiru -cache-clear # clear Smarty + framework caches docker compose exec fpm ./nibiru -model-rebuild # regenerate model files from DB ``` - **Configuration is INI-based.** `app/src/application/settings/config/settings.local.ini` (and env-specific variants) contain everything: routes, module registration, autoloader paths, database connection, security keys. ### 8.2 The module registry — gotcha The framework's registry (`core/c/registry.php`) scans every module directory recursively looking for INI configuration files. It uses **string matching** (`strstr($path, 'settings')`) to find them. This means: > **Any file path under `application/module/*/` that contains the literal string `settings` will be passed to `parse_ini_file()`.** If it's actually an INI file: fine. If it's a PHP file with "settings" in its name (e.g., `settingsForm.php`): the INI parser tries to parse PHP syntax, hits an unexpected token, and **the entire platform returns HTTP 500**. This bit us hard recently. **Never use the word "settings" in a filename under `application/module/`.** Use "prefs", "config", "options", "account" — anything else. ## 9. Module-Model-View-Controller (MMVC) Standard Nibiru module structure: ``` app/src/application/module// ├── .php # main module class, extends Module ├── interfaces/ │ └── .php # interface defining the public contract ├── plugins/ │ ├── .php # primary plugin — most of the logic lives here │ └── .php # additional plugins as needed ├── traits/ │ └── Form.php # form builders, controller-side helpers └── settings/ └── .ini # module config (database, behavior) ``` **Controllers stay thin.** They authenticate, validate input, call the module, assign view data, render. All real logic lives in module plugins. Example: `app/src/application/module/finetune/` contains: - `plugins/Finetune.php` — central DB-access plugin, all CRUD for jobs/datasets/models - `plugins/Ollama.php` — Ollama HTTP client + deployment - `plugins/ContainerManager.php` — talks to the container-manager microservice - `plugins/TrainingManager.php` — orchestrates training jobs end-to-end - `plugins/TestRunner.php` — manages chat-test sessions against the shared Ollama - `plugins/HuggingFace.php` — HF Hub API client, GGUF download, model import - `traits/apiKeyForm.php` — form builder for the API keys page Pattern for adding a new feature: 1. `./nibiru -m newfeature` — scaffolds the 4-file module 2. **Register all 4 files in `settings.local.ini`** under `[AUTOLOADER]`. If you forget, the class isn't loaded. 3. Implement plugin logic 4. Add a controller (`./nibiru -c newfeature`) 5. Wire the route in `settings.local.ini` under `route[...]` 6. Build the template, link from navigation ## 10. Database ### 10.1 Engine choice **MariaDB 10.11, not MySQL, not PostgreSQL.** Syntax differs in subtle ways — JSON functions, window functions, `EXPLAIN` output. Test your queries inside the container: ```bash docker compose -f local/docker-compose.yml exec mariadb \ mariadb -u neuronetz -p"$MARIADB_PASSWORD" neuronetz_finetune ``` ### 10.2 Column naming convention **Always `tablename_fieldname`:** ``` jobs_id, jobs_user_id, jobs_status, jobs_created_at datasets_id, datasets_name, datasets_file_path user_id, user_login, user_email, user_password_hash ``` This is not aesthetic preference — Nibiru's auto-model generator (`./nibiru -model-rebuild`) and the module registry depend on this exact format. Migrations that rename columns **must also update the corresponding model file's `const TABLE` array** in `app/src/application/model/NeuronetzFinetune/.php`. ### 10.3 Migrations Location: `app/src/application/settings/config/database/NNN-.sql` Numbered sequentially. Never edit an applied migration — always add a new numbered file. Tracked in the `migrations` table. Apply manually: ```bash docker compose exec fpm ./nibiru -mi local ``` Re-apply a specific file: ```bash docker compose exec fpm ./nibiru -mi-reset-file 032-handoff.sql local ``` ### 10.4 Foreign key renames If a migration renames a column that has a foreign key: 1. **DROP** the foreign key first 2. **RENAME** the column 3. **RECREATE** the foreign key with the new column name This was the lesson from NFP-27. Skipping step 1 fails with a generic "cannot rename column" error. ### 10.5 Schema overview (30 tables, abridged) | Group | Tables | |---|---| | Identity & auth | `user`, `acl`, `user_to_acl`, `account`, `user_to_account` | | API & integration | `api_keys`, `api_registry`, `account_to_api_registry` | | Settings | `user_settings`, `user_billing` | | Core domain | `jobs`, `datasets`, `models`, `model_pulls` | | Usage tracking | `usage_logs` | | Agent system | `agent_templates`, `handoff_requests`, `handoff_messages`, `handoff_triggers` | | Email | `email_templates`, `email_queue`, `email_notification_preferences` | | Blog (NFP-9) | `blog_posts`, `blog_categories` | | Time | `timeanddate`, `timeanddate_to_user`, `timeanddate_to_account` | | Migrations | `migrations` | Full schema in `db/01-schema.sql`. ## 11. API Surface The REST API is OpenAI-compatible-ish. Auth via session cookie (web) or Bearer token (`api_keys` table). ### Web routes (HTML) ``` / landing page /auth/login login /auth/register registration /auth/logout logout /dashboard user dashboard /jobs, /jobs/create, /jobs/view /datasets, /datasets/upload /models pulled models + user's trained models /agenttemplates pre-built agent templates /apikeys /usage, /pricing /settings user prefs (note: NOT a Nibiru module — see §8.2) /support /docs API docs page /admin admin-only, billing dashboard /chat chat test interface ``` ### JSON API ``` GET /api/pricing public pricing tiers GET /api/auth auth status (requires session) GET /api/jobs list user's jobs POST /api/jobs create a job GET /api/datasets list datasets POST /api/datasets upload dataset GET /api/models user's trained models (DB only) POST /api/models?subaction=deploy deploy a model GET /api/usage usage stats GET /api/apikeys POST /api/apikeys create + revoke GET /api/wstoken issue a websocket auth token POST /api/training/start start a training job (also via job-worker) POST /api/ollama?subaction=list-pulled list models on shared Ollama POST /api/ollama?subaction=chat-pulled chat with a pulled model POST /api/ollama?subaction=pull pull a new model from registry POST /api/ollama?subaction=hf-search search HF Hub POST /api/ollama?subaction=hf-download download GGUF from HF POST /api/ollama?subaction=hf-import register downloaded GGUF with Ollama POST /api/test?subaction=start start a chat test session POST /api/test?subaction=chat send chat message in session POST /api/test?subaction=stop end session, unload model ``` Public production API (when deployed) sits at `https://api.neuronetz.ai/v1/*` — OpenAI-compatible chat completions endpoint, served by the production Ollama (or eventually the C++ inference server). The platform displays code snippets to users using this URL on the model detail page. ## 12. Authentication & Sessions ### 12.1 Password storage As of NFP-18 (merged 2026-04-13), passwords use **argon2id** via PHP's `password_hash(PASSWORD_ARGON2ID)`. The `user.user_password_hash` column holds the modern hash. For migration from the legacy AES-encrypted column (`user.user_pass`), the auth flow tries argon2id first. If that fails and a legacy AES password matches, the user is transparently rehashed into argon2id on that login. Eventually `user_pass` can be dropped — schedule for a release after most users have logged in once. NFP-55 tracks refactoring `core/c/auth.php` to make password strategies pluggable so future auth work (2FA, passkeys, etc.) doesn't require touching core. ### 12.2 Session storage Sessions are stored in **Redis** (`redis:6379`). Session ID is the standard PHP session cookie. ACL role is loaded from `user_to_acl` on login and cached in `$_SESSION['auth']`: ```php $_SESSION['auth'] = [ 'user_id' => 1, 'user_login' => 'admin@finetune.ai', 'user_email' => 'admin@finetune.ai', 'user_name' => 'Admin', 'role' => 'superuser', // from acl.acl_role ]; ``` ### 12.3 CSRF CSRF tokens are mandatory on all POST forms (NFP-19). The CSRF module generates per-session tokens; forms include them as hidden fields; the controller validates before processing. The framework's form factory (`Nibiru\Factory\Form`) automatically injects CSRF tokens. If you build a form by hand, include the token manually: ```smarty ``` ### 12.4 API key auth API endpoints accept `Authorization: Bearer ` as alternative to session auth. Keys are stored as bcrypt hashes in `api_keys` (per-user, revokable, with scopes). ## 13. Background Work ### 13.1 job-worker A Workerman-based PHP CLI process (separate container, PHP 8.2-alpine) that polls the `jobs` table every 5 seconds, picks queued training jobs, dispatches them via `ContainerManager::createContainer()` for the actual training run, and monitors progress. The worker has **no direct DB models** — it runs in its own container and uses raw `PDO` queries. It does NOT bootstrap the Nibiru framework (would be overkill for the worker's narrow job). But it does use the framework's `composer` vendor directory via the WebSocket server's autoloader at `app/src/application/server/loader/vendor/autoload.php`. If you need to debug: ```bash docker compose logs job-worker -f ``` ### 13.2 WebSocket server Workerman process on TCP 2346, served behind nginx at `local.websocket.finetune.neuronetz.ai`. Used for: - Job progress streaming (NFP-13) - Future: real-time chat, collaborative editing Auth: the platform issues short-lived (5-minute) tokens via `/api/wstoken`; the client connects with `?token=...`. The WS server validates against the database. ### 13.3 container-manager Python (Flask) microservice with the host Docker socket mounted. Exposes a small HTTP API on `:8080`: ``` GET /health GET /api/gpu GPU info GET /api/containers list managed containers GET /api/containers/{userId} POST /api/containers/{userId} create+start a user's Ollama POST /api/containers/{userId}/start POST /api/containers/{userId}/stop DELETE /api/containers/{userId} POST /api/containers/{userId}/exec { command: "ollama list" } POST /api/containers/{userId}/write { content: "...", path: "/Modelfile" } GET /api/containers/{userId}/logs?lines=100 ``` The PHP client is `Ollama\ContainerManager`. **All container operations from PHP go through it.** Never `exec("docker ...")` from PHP. ## 14. Model Serving & Inference ### 14.1 Current: Ollama (`ollama` service) The shared Ollama instance (`finetune-ollama-shared`) runs at `http://finetune-ollama-shared:11434` inside the Docker network. It currently hosts the user's trained models plus any models pulled from HF. Two concurrent models max (`OLLAMA_MAX_LOADED_MODELS=2`). For chat testing, the platform's `TestRunner` plugin: 1. Picks an endpoint — shared Ollama if loaded models < 2, otherwise spins up an overflow per-user container via ContainerManager. 2. Loads the model if not already registered (via `ollama pull` for HF models, or `ollama create -f Modelfile` for platform-trained GGUFs once NFP-52 ships). 3. Routes chat HTTP to that endpoint's `/api/chat`. 4. Unloads when the session ends. A model is "chat-capable" if its Ollama metadata reports `completion` capability AND its template contains the `.Messages` marker. Pure-completion templates like `{{ .Prompt }}` are not chat-capable; the UI filters their chat buttons (commit `1947e34`). ### 14.2 Next: C++ llama.cpp server (NFP-51, in progress) Stephan is building a multi-model C++ inference server using `llama.cpp` directly, exposed via `cpp-httplib`. Goals: - Replace Ollama (third-party Go binary) with code we fully control - Handle multi-model inference natively (no per-model container lifecycle) - Eliminate Docker from the inference path entirely - Open-source-friendly, GDPR-friendly (no upstream telemetry concerns) Status: scaffold landed (`local/llama-server/`) but currently uncompilable on develop because `main.cpp` references the removed `llama_batch_add` helper from older llama.cpp. The service is disabled via `profiles: [disabled]` in docker-compose. Stephan is writing the working version himself. ### 14.3 HuggingFace integration The `HuggingFace` plugin handles: - Search Hub for models (`/api/models?search=`) - List files in a repo (`/api/models/{repo}/tree/main`) - Download specific files (typically GGUFs) to `app/src/downloads/huggingface/` - Async downloads with progress reporting via a separate script that writes to a temp file Honors `HUGGINGFACE_API_TOKEN` env var for authenticated downloads (required for gated models like Meta Llama). ## 15. Frontend ### 15.1 Templates **Smarty 3.1** at `app/src/application/view/templates/`. Shared layout fragments under `templates/shared/v5/` (header, footer, navbar, sidebar). **No inline JS or CSS.** This is enforced by QA — any inline `