Files
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

50 KiB
Raw Permalink Blame History

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)
  2. Full Setup
  3. First Login & Sanity Checks
  4. Troubleshooting Setup

Part II — The Platform

  1. What This Platform Does
  2. Architecture Overview
  3. The Service Stack
  4. The Nibiru Framework
  5. Module-Model-View-Controller (MMVC)
  6. Database
  7. API Surface
  8. Authentication & Sessions
  9. Background Work
  10. Model Serving & Inference
  11. Frontend

Part III — Working in the Codebase

  1. Hard Rules
  2. Git Workflow
  3. Code Style
  4. Naming Conventions
  5. Testing
  6. Common Gotchas

Part IV — Operations

  1. Tools & Dashboards
  2. Logging & Observability
  3. Troubleshooting Runbooks
  4. Regenerating This Delta

Part V — Context & Culture

  1. Regulatory Stance (Why No GDPR Theater)
  2. The Multi-Agent Orchestrator (Optional)
  3. 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:

# 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 or §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

ssh-keygen -t ed25519 -C "your@email.example"   # if you don't have one
cat ~/.ssh/id_ed25519.pub                       # send this to Stephan

Test:

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):

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

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:

./bootstrap-db.sh /custom/path/to/finetuning-plattform

2.5 Branch

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

cd local
docker compose up -d

First run pulls ~10 GB of images and takes 510 minutes. Subsequent starts are seconds.

Verify:

docker compose ps

Expect ~14 services running. The llama-server service is currently disabled via profiles: [disabled] — that's intentional, see §14.

2.8 Bootstrap the database

From this delta repo:

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):

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

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

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:

docker compose exec fpm ./nibiru -mi local

Re-apply a specific file:

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']:

$_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:

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

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:

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:

{$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

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:

git fetch origin
git rebase origin/develop
git push --force-with-lease       # only after rebase

18. Code Style

<?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

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

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)

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:

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.

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_typeusage_logs_resource_type rename in NFP-27 bit us; check for any code that references columns by their pre-rename name.

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:

cd local
docker compose up -d

Wait ~30s, then verify with:

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:

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:

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:

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. developmain 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.