Files
finetuning-plattform-setup-…/MANUAL.md
m17hr1l 8932534623 Initial delta: full developer manual + DB bootstrap
Generated from finetuning-plattform develop @ 70b203c on 2026-05-14.

Contents:
- MANUAL.md       — full developer manual, setup at front (28 sections)
- bootstrap-db.sh — one-command DB initialization
- db/01-schema.sql       — MariaDB schema, no data
- db/02-seed.sql         — reference data (ACL, email templates, API registry)
- db/03-default-users.sql — admin + test user, argon2id hashes

Drop-in package for new developers joining the platform.
2026-05-14 12:44:47 +02:00

1104 lines
50 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <username>! 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 510 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 1030s 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 <name> # scaffold a new controller + template
docker compose exec fpm ./nibiru -m <name> # 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/<name>/
├── <name>.php # main module class, extends Module
├── interfaces/
│ └── <name>.php # interface defining the public contract
├── plugins/
│ ├── <name>.php # primary plugin — most of the logic lives here
│ └── <OtherPlugin>.php # additional plugins as needed
├── traits/
│ └── <name>Form.php # form builders, controller-side helpers
└── settings/
└── <name>.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/<name>.php`.
### 10.3 Migrations
Location: `app/src/application/settings/config/database/NNN-<description>.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
<input type="hidden" name="csrf_token" value="{$csrf_token}">
```
### 12.4 API key auth
API endpoints accept `Authorization: Bearer <key>` 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 `<script>` or `<style>` block in a template is an auto-reject. Use external files under `app/src/public/{js,css}/v5/`.
### 15.2 CSS
**Bootstrap 5.3** loaded from CDN in `header.tpl`. Custom CSS in `app/src/public/css/v5/` per feature (e.g. `admin-billing.css`, `chat.css`, `agent-templates.css`).
### 15.3 JavaScript
**Vanilla JS + HTMX** for interactivity. Custom helpers in `app/src/public/js/v5/`:
- `finetune-api.js` — central API client (handles auth, CSRF, fetch)
- `theme-switcher.js` — dark/light mode
- `onboarding.js` — first-time user tour
- Feature-specific JS per module
HTMX is loaded globally. Use it for partial page updates, modal loads, dynamic dropdowns. No React, no Vue, no SPA framework. The platform is intentionally classic.
### 15.4 Forms
Use the framework's `Nibiru\Factory\Form` builder:
```php
use Nibiru\Factory\Form;
Form::create('myForm');
Form::addInputTypeText('name', $value, ['placeholder' => 'Name', 'required' => true]);
Form::addInputTypeEmail('email', $value);
Form::addSelectOption(['Option 1', 'value1']);
Form::addSelectOption(['Option 2', 'value2']);
Form::addSelect('country', ['id' => 'country-select']);
Form::addTypeButton('Submit');
$html = Form::addForm(['action' => '/my-endpoint', 'method' => 'POST']);
```
**Known framework bug:** `addOpenAny`/`addCloseAny` are broken (missing `FORM_ATTRIBUTE_ROLE` constant). Use `addOpenDiv` with raw HTML in `value` as a workaround until the core refactor.
### 15.5 i18n
Translation files in `app/src/application/settings/lang/{en,de,es,fr,it,ja,nl,pl,pt}.json`. Loaded by the `I18n` module. Pass to templates as `$t`:
```smarty
{$t.settings.title|default:'Settings'}
```
Default-to-English fallback in case a translation is missing.
---
# Part III — Working in the Codebase
## 16. Hard Rules
Read these once. Internalize them. They will save you from re-doing work.
| # | Rule | Why |
|---|---|---|
| 1 | **`app/src/core/` is FROZEN.** No edits, no exceptions. | The Nibiru Framework is 8+ years of stable base code. Core changes destabilize every project that uses it. NFP-18 (argon2id) was the final exception, explicitly approved. |
| 2 | **Use `docker compose` (space), never `docker-compose` (hyphen).** | The legacy script behaves differently and is explicitly blocked. |
| 3 | **MariaDB syntax only.** No PostgreSQL idioms. | Test queries against the actual DB before committing. |
| 4 | **No inline `<script>` or `<style>` in templates.** | QA auto-rejects. Style in `public/css/v5/`, JS in `public/js/v5/`. |
| 5 | **Column naming: `tablename_fieldname` always.** | The auto-model generator and registry depend on it. Migrations renaming columns must update model `const TABLE` arrays. |
| 6 | **No file with "settings" in its name under `application/module/`.** | The registry's INI parser will eat it and the platform 500s. Use "prefs", "config", "options". |
| 7 | **No `exec("docker ...")` calls from PHP.** | fpm has no docker CLI. Route through `ContainerManager::*` methods. |
| 8 | **No separate `vendor/autoload.php`** anywhere. | Use the framework's existing autoloader. Adding parallel autoloaders breaks classloading. |
| 9 | **Never modify the legacy `user_pass` column.** | It's the bridge for users not yet migrated to argon2id. Read it for fallback auth only. |
| 10 | **Branch off `develop`, PR to `develop`.** Never merge directly into `main`. | Gitflow. `main` is for release cuts. |
## 17. Git Workflow
### 17.1 Branch lifecycle
```bash
git checkout develop && git pull origin develop
git checkout -b NFP-<num>/<short-title-with-dashes>
# ... do work ...
git add <specific-files> # never `git add -A` — picks up secrets and runtime junk
git commit -m "NFP-<num>: <description>"
git push -u origin NFP-<num>/<short-title-with-dashes>
# open PR in Gitea targeting develop
```
### 17.2 Commit messages
```
NFP-<num>: <imperative description>
[optional body with details, why-not-what, references to other tickets]
```
Examples from recent history:
- `NFP-1: Convert login form to Nibiru Factory\Form`
- `NFP-27: Add column renaming migrations for consistent naming`
- `NFP-50: Replace exec('docker') calls with ContainerManager in Ollama plugin`
### 17.3 Pull request
- **Title:** the commit subject. Keep under 70 chars.
- **Body:** what changed and why, test plan, screenshots if UI.
- **Base:** `develop`.
- **Reviewer:** QA agent (Marie) reviews automatically if the orchestrator is running. Otherwise tag Stephan.
### 17.4 Rebase, don't merge develop into your branch
If develop has moved since you branched:
```bash
git fetch origin
git rebase origin/develop
git push --force-with-lease # only after rebase
```
## 18. Code Style
```php
<?php
namespace Nibiru\Module\Finetune\Plugins;
use Nibiru\Module\Finetune\Interfaces\Finetune as IFinetune;
class MyPlugin implements IFinetune
{
private static ?MyPlugin $instance = null;
private function __construct()
{
// …
}
public static function init(): MyPlugin
{
if (self::$instance === null)
{
self::$instance = new self();
}
return self::$instance;
}
public function doThing(int $userId, array $options = []): array
{
if ($options['fast'] ?? false)
{ // opening brace on NEW line for if/foreach/while/etc
return $this->fastPath($userId);
} else { // else on SAME line as closing brace
return $this->slowPath($userId);
}
}
}
```
- **Type hints on every method signature** (params + return).
- **Opening brace on new line for control flow** (`if`, `foreach`, `while`, function declarations).
- **`} else {` and `} catch {` on the same line.**
- **Condition stays on one line.** Break long conditions across multiple lines with `&&` / `||` at the start of continuation.
- **No commented-out code in commits.** Delete it; git remembers.
- **Default to no comments.** Add one only when the WHY is non-obvious. Don't explain WHAT — names should do that.
## 19. Naming Conventions
### Files
- Controllers: `<name>Controller.php` (lowercase first letter even for class). E.g., `jobsController.php`.
- Module main: `<name>.php` (lowercase). E.g., `finetune.php`.
- Plugins: `<Name>.php` (PascalCase). E.g., `Ollama.php`, `ContainerManager.php`.
- Traits: `<name>Form.php` or `<name>Trait.php`. E.g., `authForm.php`, `apiKeyForm.php`.
- Migrations: `NNN-<description>.sql`. E.g., `029-handoff_requests.sql`.
- Templates: `<route>.tpl`. E.g., `dashboard.tpl`. Shared: under `templates/shared/v5/`.
### Database
- Tables: `lowercase_snake_case`. Singular for single-row tables (`user`, `account`), plural for collections (`jobs`, `datasets`).
- Columns: `tablename_fieldname` (e.g. `user.user_id`, `jobs.jobs_status`).
- Foreign keys: `<table>_<other_table>_id` (e.g. `jobs.jobs_user_id`).
- Indexes: `idx_<table>_<columns>`.
- Constraints: `fk_<table>_<other_table>`.
### Classes
- Modules: `Nibiru\Module\<Name>\<Name>` for the main, plugins under `Nibiru\Module\<Name>\Plugins\<Plugin>`.
- Models: `Nibiru\Model\NeuronetzFinetune\<Table>`.
- Controllers: `Nibiru\<name>Controller` (note: no module namespace).
### Routes
In `settings.local.ini`:
```
route[my-feature] = "/my-feature"
route[my-feature/create] = "/my-feature/create"
route[api/my-feature] = "/api/my-feature"
```
Maps to `myfeatureController::pageAction()`, `myfeatureController::createAction()`, etc. The framework strips `-` and lowercases when locating the controller.
## 20. Testing
### 20.1 Stack
**PHPUnit 11.** Tests under `app/src/tests/`:
- `tests/Unit/` — pure unit tests, no DB
- `tests/Integration/` — DB-backed, currently skipped because the test bootstrap doesn't initialize the Nibiru DB connection (separate ticket)
- `tests/Fixtures/DatabaseSeeder.php` — fixture data for integration tests
### 20.2 Run
```bash
docker compose exec fpm vendor/bin/phpunit
# or the bundled phar if composer.lock is unresolved:
docker compose exec fpm ./phpunit-11.phar
```
Expected baseline (from 2026-04-13): **192 tests, 160 passing, 0 failing, 32 skipped.** The skips are all in `FinetunePluginIntegrationTest` waiting on the bootstrap fix.
### 20.3 Write tests
For new modules, add `tests/Unit/Module/<Module>/<Plugin>Test.php`. Pattern:
```php
<?php
declare(strict_types=1);
namespace Tests\Unit\Module\MyModule;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;
class MyPluginTest extends TestCase
{
#[Test]
public function it_does_the_thing(): void
{
$result = MyPlugin::init()->doThing(1, ['fast' => true]);
$this->assertTrue($result['success']);
}
}
```
### 20.4 Coverage philosophy
We don't enforce a coverage percentage. **Test what's hard to verify by clicking.** Pure functions, edge cases, regression fixes. Don't test framework code; don't test trivial getters.
## 21. Common Gotchas
| Symptom | Cause | Fix |
|---|---|---|
| Trait class not found | New file not in autoloader cache | `docker compose exec fpm composer dump-autoload` |
| Module not loading | Forgot to register all 4 files in `settings.local.ini` `[AUTOLOADER]` | Add `class.pos[]`, `iface.pos[]`, `trait.pos[]`, `class.plugin.pos[]` entries |
| 500 on every page after merge | Probably a file with "settings" in its name | grep `application/module/*/` for `settings` |
| `Failed to load model` in chat | Model has no chat template (completion-only) | UI should filter; if you see it, the filter is broken |
| `docker: not found` | Someone called `exec("docker ...")` from PHP | Replace with ContainerManager method |
| `mkdir(): Permission denied` | Named docker volume created as root | `docker exec -u root <container> chown -R www-data:www-data <path>` |
| `column does not exist` | Forgot to update model `const TABLE` after rename | Update the model file, run `composer dump-autoload` |
| PR #N is `mergeable: false` | Conflicts with develop | Rebase your branch onto develop, force-push |
| Login form posts but returns 200 (login page again) | CSRF token missing or mismatched | Inspect the form HTML for `name="csrf_token"`, check session is being created |
## 22. Tools & Dashboards
| Tool | URL | Purpose |
|---|---|---|
| Platform | `http://local.finetune.neuronetz.ai/` | The app |
| Graylog | `http://local.graylog.finetune.neuronetz.ai/` | Centralized log search |
| Kibana | `http://local.kibana.finetune.neuronetz.ai/` | ES log visualization |
| Redis Commander | `http://local.redis-commander.neuronetz.ai/` | Redis inspector |
| Ollama API | `http://finetune-ollama-shared:11434/api/tags` (internal only) | List loaded models |
| Container Manager | `http://container-manager:8080/health` (internal only) | Docker proxy used by PHP |
| Gitea | `https://gitea.neuronetz.ai/Neuronetz/finetuning-plattform` | Source, PRs |
| YouTrack | `https://yt.neuronetz.ai/projects/NFP` | Tickets, board |
# Part IV — Operations
## 23. Logging & Observability
### 23.1 Structured logging via Graylog
The `graylog` module exposes traits any plugin can use:
```php
use Nibiru\Module\Graylog\Traits\Log;
class MyPlugin
{
use Log;
public function doThing()
{
$this->logInfo('Did the thing', ['user_id' => 42, 'duration_ms' => 123]);
$this->logWarning('Something off', ['detail' => 'x']);
$this->logError('It broke', ['exception' => $e->getMessage()]);
}
}
```
Logs ship to Graylog over GELF. Search at `local.graylog.finetune.neuronetz.ai/search`.
### 23.2 Application logs (Smarty errors, PHP warnings)
```bash
docker compose logs fpm -f
docker compose logs nginx -f
docker compose logs job-worker -f
```
PHP errors are at LOG level NOTICE/WARNING/ERROR by default.
### 23.3 Database queries
If you need to inspect what queries are running:
```sql
SET GLOBAL general_log = 'ON';
SET GLOBAL general_log_file = '/var/lib/mysql/general.log';
-- ... do the thing ...
-- then:
SET GLOBAL general_log = 'OFF';
```
Then `docker exec -it neuro-finetuning-platform-mariadb-1 cat /var/lib/mysql/general.log`. **Don't leave it on** — it's a perf hit and a privacy concern.
## 24. Troubleshooting Runbooks
### 24.1 Platform returns 500 on every page
Most common cause: a PHP file with "settings" in its name under `application/module/`, being eaten by the registry's INI parser.
```bash
docker compose logs fpm | tail -50 | grep -A 5 "syntax error"
# look for: "syntax error, unexpected '...' in /var/www/html/application/module/.../settings*.php"
```
Fix: rename the file.
### 24.2 `/admin` returns 500 after merge
Likely a stale column reference. The `usage_logs_type` → `usage_logs_resource_type` rename in NFP-27 bit us; check for any code that references columns by their pre-rename name.
```bash
grep -rn "usage_logs_type" app/src/application/
```
Replace with the current column name; commit; deploy.
### 24.3 Stack went down mid-session
Usually someone (or an agent) ran `docker compose down`. To resume:
```bash
cd local
docker compose up -d
```
Wait ~30s, then verify with:
```bash
curl -sS -o /dev/null -w "%{http_code}\n" http://local.finetune.neuronetz.ai/
```
If anything fails to start, check the per-service logs.
### 24.4 Chat with a model fails silently
The model probably has no chat template (completion-only). Per the UI filter, only models with `template` containing `.Messages` should show a chat button. If you see one that shouldn't be there, the filter is broken (commit `1947e34` added it; regression means a recent merge undid the filter).
To check directly:
```bash
docker exec neuro-finetuning-platform-fpm-1 sh -c \
'curl -sS http://finetune-ollama-shared:11434/api/show -d "{\"name\":\"<model-name>\"}" | jq .template'
```
Empty or `"{{ .Prompt }}"` → not chat-capable.
### 24.5 Composer install fails on PHP 8.3
```
laminas/laminas-diactoros requires php (~8.0.0 || ~8.1.0 || ~8.2.0) failed
```
The lock file has a stale constraint. Workaround:
```bash
docker compose exec fpm composer update laminas/laminas-diactoros --with-dependencies
docker compose exec fpm composer install
```
Proper fix is in flight (NFP-22 area).
## 25. Regenerating This Delta
When the schema drifts, regenerate from a live develop instance:
```bash
cd ~/PhpstormProjects/finetuning-plattform-setup-delta
# 1. Schema
docker exec neuro-finetuning-platform-mariadb-1 sh -c \
'mariadb-dump -u neuronetz -p"$MARIADB_PASSWORD" --no-data --skip-add-drop-table --skip-comments neuronetz_finetune' \
> db/01-schema.sql
# 2. Seed data
docker exec neuro-finetuning-platform-mariadb-1 sh -c \
'mariadb-dump -u neuronetz -p"$MARIADB_PASSWORD" --no-create-info --skip-comments --complete-insert neuronetz_finetune acl email_templates api_registry' \
> db/02-seed.sql
# 3. Default users — passwords are argon2id, so regenerate hashes
docker compose exec fpm php -r 'echo password_hash("admin123", PASSWORD_ARGON2ID).PHP_EOL;'
docker compose exec fpm php -r 'echo password_hash("test123", PASSWORD_ARGON2ID).PHP_EOL;'
# Paste into db/03-default-users.sql.
# 4. Update the generated-at marker at the top of this MANUAL.md.
git commit -am "Regenerate delta from develop @ $(cd ../finetuning-plattform && git rev-parse --short HEAD)"
git push
```
The hashes change on every regeneration because argon2id uses a random salt — that's expected and correct.
---
# Part V — Context & Culture
## 26. Regulatory Stance (Why No GDPR Theater)
The platform takes an explicit activist position: **regulatory capture by big tech is real, and most compliance ritual exists to make startups uncompetitive rather than to protect users.** Concretely:
- **Production hosting is outside the EU.** No GDPR jurisdictional hook. No mandatory cookie banner, no Art. 30 records of processing, no DPA boilerplate.
- **The user-facing privacy notice is honest.** Roughly: "We don't do compliance theater. Server's not in the EU. We don't log what we don't need, we don't sell what we know, GDPR is a rubber stamp big tech pays for and we don't. If you're building something real, you get it. Don't steal, don't abuse, everything else is between you and your model."
- **What does NOT change:** German Impressum (Telemediengesetz §5) for the company is a separate legal requirement and stays real. Don't conflate Impressum with GDPR. Tax & accounting compliance — same.
- **What this means for you as a developer:** Don't propose "just in case" cookie consent flows, consent banners, GDPR-style audit logs, or DPA templates as part of feature work. If a customer explicitly asks for one, escalate to Stephan — there may be a business reason (e.g. enterprise contract). Otherwise default to honest, minimal, no theater.
NFP-36 (GDPR compliance implementation) is permanently Blocked. The notice is being drafted; placement (footer, dedicated page, banner) is a pending product decision.
## 27. The Multi-Agent Orchestrator (Optional)
There's a sibling project at `~/PhpstormProjects/orchstrator-agent-setup/` that runs 6 named AI agents (Bruno, Luna, Marie, Otto, Klara, Felix) in parallel via a tmux session and a workflow engine. They pick tickets from YouTrack, work in isolated git worktrees, open PRs in Gitea, review each other's code, and merge.
You probably won't use it — you're the experienced devs. But:
- Dashboard at `http://127.0.0.1:7400` (slide-up iframe in the platform footer when running)
- Marie (QA agent) runs on Claude Opus and auto-rejects any PR that touches `app/src/core/`. If you see her reject one of yours, it's the core rule (§16#1).
- Start: `cd ~/PhpstormProjects/orchstrator-agent-setup/orchestrator && ./orchestrator.sh start`
- Stop: `./orchestrator.sh stop`
- Status: `./orchestrator.sh status`
The orchestrator will eventually be replaced by platform-hosted fine-tuned models (the "fire test") — at which point this whole subsystem gets deleted.
## 28. Glossary
| Term | Meaning |
|---|---|
| **NFP** | Neuronetz Finetuning Platform. Also the YouTrack project key. |
| **Nibiru** | The PHP framework underneath the platform. Stephan's. Frozen. |
| **MMVC** | Module-Model-View-Controller — Nibiru's variant of MVC with explicit module separation. |
| **Module** | A self-contained feature area: main class + interface + traits + plugins + INI settings. Lives under `application/module/<name>/`. |
| **Plugin** | A concrete implementation class within a module. Plugins are where logic lives. |
| **Trait** | PHP trait used by controllers, typically for form construction. |
| **Modelfile** | An Ollama configuration that defines a model's template, parameters, base. Like a Dockerfile for LLMs. |
| **GGUF** | The file format llama.cpp / Ollama uses for quantized models. Single binary file, runs on CPU or GPU. |
| **LoRA** | Low-Rank Adaptation. Fine-tuning method that produces small adapter weights rather than retraining the whole model. The platform uses this. |
| **Fire test** | The milestone where the platform's own fine-tuned models replace the Claude agents and the multi-agent orchestrator becomes self-hosted. |
| **Develop** | The integration branch. All feature work merges here. |
| **Main** | The release branch. `develop` → `main` happens periodically; never push direct. |
| **Container Manager** | The Python microservice that holds the Docker socket so PHP doesn't need to. |
| **Workerman** | PHP library for long-running daemons. Used for the websocket and job-worker. |
| **OpenAI-compatible** | Refers to the production inference API at `api.neuronetz.ai/v1/*` that mimics OpenAI's chat completions shape. |
---
*Welcome aboard. If anything in here is wrong, file an issue — or just fix it and PR the manual.*