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.
50 KiB
Neuronetz Finetuning Platform — Developer Manual
Repository:
ssh://git@gitea.neuronetz.ai:222/Neuronetz/finetuning-plattform.gitThis delta:ssh://git@gitea.neuronetz.ai:222/m17hr1l/finetuning-plattform-setup-delta.gitGenerated from:develop@70b203c, 2026-05-14 Audience: Senior backend and frontend developers joining the project.
Table of Contents
Part I — Get Running
Part II — The Platform
- What This Platform Does
- Architecture Overview
- The Service Stack
- The Nibiru Framework
- Module-Model-View-Controller (MMVC)
- Database
- API Surface
- Authentication & Sessions
- Background Work
- Model Serving & Inference
- Frontend
Part III — Working in the Codebase
Part IV — Operations
Part V — Context & Culture
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 versionmust succeed (the legacydocker-composescript 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 theNeuronetzorg. - NVIDIA GPU + driver + nvidia-container-toolkit if you want to actually run inference (Ollama or llama-server). Without one, set
OLLAMA_GPU_LAYERS=0inlocal/.envand 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 5–10 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:
- Sources
local/.envfrom the platform repo to get DB credentials. - Waits for MariaDB to accept connections.
- Loads schema, seed, default users in order.
- 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
/dashboardloads → ✓/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 10–30s to be ready after up -d |
Wait longer, then re-run |
| Platform returns 502 from nginx | fpm crashed |
docker compose restart fpm; if it crashes again, capture logs and grep for Fatal error |
llama-server fails to build |
Known: NFP-51 vs current llama.cpp API | Ignore — the service is profile-disabled. Other services work without it. |
composer install fails with PHP 8.3 platform check error |
Stale composer.lock (NFP-22 work in progress) |
Use the bundled phpunit-11.phar for tests; ignore composer until the lock is regenerated. |
Part II — The Platform
5. What This Platform Does
The Neuronetz Finetuning Platform is a self-hosted alternative to OpenAI fine-tuning APIs. End users:
- Upload datasets (JSONL with prompt/completion or instruction format)
- Pick a base model (anything on HuggingFace as GGUF, or already pulled into the local Ollama)
- Configure a training job (epochs, batch size, LoRA rank, learning rate — or use presets like
quick/standard/thorough) - Run the job — the platform spawns a training container, streams progress via WebSocket
- Test the result — chat with the fine-tuned model in-browser
- Deploy — serve the model on an Ollama instance accessible from a stable URL (
api.neuronetz.aiin 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:
- PHP never calls Docker directly. The fpm container has no
dockerCLI by design. All container operations route through thecontainer-managermicroservice (Python) which has the Docker socket mounted. This is the most important invariant — violating it produces silent failures or 500s. - 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 registrycore/l/autoload.php— Composer PSR-4 autoloader, loads third-party packages fromcore/l/(the composervendor-dir)- Both are bootstrapped automatically. Never create a separate
vendor/autoload.phpanywhere 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 stringsettingswill be passed toparse_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/modelsplugins/Ollama.php— Ollama HTTP client + deploymentplugins/ContainerManager.php— talks to the container-manager microserviceplugins/TrainingManager.php— orchestrates training jobs end-to-endplugins/TestRunner.php— manages chat-test sessions against the shared Ollamaplugins/HuggingFace.php— HF Hub API client, GGUF download, model importtraits/apiKeyForm.php— form builder for the API keys page
Pattern for adding a new feature:
./nibiru -m newfeature— scaffolds the 4-file module- Register all 4 files in
settings.local.iniunder[AUTOLOADER]. If you forget, the class isn't loaded. - Implement plugin logic
- Add a controller (
./nibiru -c newfeature) - Wire the route in
settings.local.iniunderroute[...] - 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:
- DROP the foreign key first
- RENAME the column
- 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_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:
- Picks an endpoint — shared Ollama if loaded models < 2, otherwise spins up an overflow per-user container via ContainerManager.
- Loads the model if not already registered (via
ollama pullfor HF models, orollama create -f Modelfilefor platform-trained GGUFs once NFP-52 ships). - Routes chat HTTP to that endpoint's
/api/chat. - 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 modeonboarding.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\FormNFP-27: Add column renaming migrations for consistent namingNFP-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.phpor<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: undertemplates/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 underNibiru\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 DBtests/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_type → usage_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. 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.