Changelog

All notable changes to Gladex are documented here.

Format: `[version] - YYYY-MM-DD — description`

v0.4.7 2026-09-23
Changed
**Removed a dead duplicate `/team` route block** in `app/src/php/app.php`: the earlier /team-bug fix had landed the block twice (before `/stats` and again after it); the second copy was unreachable because the first `exit`s. Exactly one `/team` route remains; behaviour unchanged (verified: `/team` `/info` `/stats` 200 on dev+prod and publicly, catch-all still 302→`/investor`).
**Reconciled `/data` root with STRUCTURE.md**: created the documented symlinks `/data/tools → /data/repo/tools` and root docs (`STRUCTURE.md`, `BUDGET.md`, `DOMAIN.md`, `REPORT.md`, `AGENT_BRIEF.md`, `AGENT_TASK.md`, `README.md` → `repo/` copies). They were documented but missing, so every `/data/tools/...` path in `tools/REGISTRY.md` dangled and the brief's §6 `ls /data` check failed. Repo stays canonical — no duplicated content.
Verification
Tests: `test_lan_middleware` 49/49 + `test_ts_ordering` 7/7 + `test_ip_drift_safety` 16/16 green on repo; 49/7 green on dev and prod.
All 12/12 `REGISTRY.md` Location paths resolve; `healthcheck` 200/200 via `/data/tools`.
Public re-check: `https://team.gladex.de/team` 200 with Lena/Jonas/Mia/Leon (no birthdates) — note: VPS routes by **SNI**, so probes must use `curl --resolve <host>:443:77.90.15.49`; a Host-header-only probe gets a VPS 404 (false alarm seen this run).
Mail/adopt re-proof (operator "Next" items, all already recorded): LE cert live on 465/993 (`CN=gladex.de`, notAfter 2026-12-22), `MX gladex.de → 10 gladex.de.` on 8.8.8.8+1.1.1.1, SOA serial 2026092305 unchanged.
v0.4.6 2026-09-23
Fixed
**`ip-drift-cron` would have taken the site down on a tunnel drop** (queued item d): the weekly cron **auto-wrote the apex A record** via `pdns-api.py set-a` whenever drift was detected.

- **Root cause**: the tool was written for the original *dynamic home IP* design (2026-09-20). Topology then changed — WireGuard `AllowedIPs 0.0.0.0/0` sends **all** egress via the VPS, and DNS points `gladex.de` at the **static VPS IP `77.90.15.49`**, never at the home/LTE IP (CGNAT, no public v4).

- **Failure mode**: on any tunnel blip, `get_public_ip()` returns our NAT'd CGNAT egress instead of the VPS → drift reported → cron repointed `gladex.de` at an address that **cannot accept inbound connections**, turning a transient outage into a lasting one with no auto-recovery. It also rewrote only the **apex**, leaving the 6 subdomains split-brain.

- **Fix**: `ip-drift-cron` is now **detect + alert only** — it classifies the egress, logs a diagnosis, appends to `/data/agent-logs/ip-drift-ALERTS.log`, and **never calls any `pdns-api.py` write verb**. A VPS IP change stays an operator/investor decision.

**CGNAT misclassification** (bug introduced then fixed in this same change): Python 3.12's `ipaddress.is_private` does **not** cover RFC6598 `100.64.0.0/10` — precisely the LTE fallback address seen when the tunnel drops. `100.64.17.22` was classed `public`. Now tested explicitly alongside RFC1918/loopback/link-local/`192.0.0.0/24`.
`ip-drift-check` human output no longer advises *"Update DNS A record to match current public IP"*; it points at the WireGuard tunnel first and requires sign-off before any DNS change. Docstring records the topology.
Tests
New `tests/test_ip_drift_safety.php` — **16/16** green on repo, dev, prod: no DNS-write call in *executable* code (full-line comments stripped so the cron's own explanation of the old behaviour doesn't trip the guard), CGNAT `100.64.17.22` → `private-or-cgnat`, alert file written, `DNS NOT MODIFIED` recorded, `bash -n`/`py_compile`/`--help` clean, live check reports no drift with tunnel up.
Existing `test_ts_ordering.php` **7/7** + `test_lan_middleware.php` **49/49** stay green.
Verification
End-to-end drill of the previously-dangerous branch (`ip-drift-cron google.de`, forces drift): exit **1**, alert written, `DNS NOT MODIFIED` logged — and apex + all 6 subdomains still `77.90.15.49`, SOA serial unchanged `2026092305`.
v0.4.5 2026-09-23
Fixed
**Chat message ordering (queued item c)**: `/api/messages` sorted lexicographically (`ORDER BY ts ASC`) over mixed `ts` formats — dev had 34 ISO/datestrings vs 23 epoch rows (prod 5/17), so all epoch rows (leading `1…`) sorted *before* every datestring (leading `2026…`) and chat chronology was scrambled across two eras.

- **Migration**: `UPDATE messages SET ts = strftime('%s', ts)` on non-epoch rows — dev 34 rows, prod 5 rows converted; row counts unchanged (57/22); pre-migration DB backups in `/data/shared/backups/messages-{dev,prod}-preTsFix-20260923-230104.db`.

- **Schema**: default `ts` is now `strftime('%s','now')` (was `datetime('now')`) so new rows can never be datestrings.

- **Query**: `ORDER BY CAST(ts AS INTEGER) ASC, id ASC` (numeric + deterministic tie-break).

New regression test `tests/test_ts_ordering.php` (7 assertions): schema default is 10-digit epoch, migration converts all 3 legacy formats, numeric ordering ascending, `/api/messages` query contract, JS `fmtTime` numeric branch. Resolves both repo (`app/src/php/`) and deployed (`src/php/`) schema paths.
Tests
`test_ts_ordering.php` **7/7** + `test_lan_middleware.php` **49/49**, green on repo, dev, prod.
v0.4.4 2026-09-23
Added
**`log.gladex.de` log viewer** (queued item b): new `app/src/php/log.php` — run-log list (name/size/mtime) from `/data/agent-logs/` + 200-line tail with 256KB read cap, plus env app-log tail from `/opt/startup/{dev,prod}/logs/`
Path safety: basename allow-list `^run-\d{8}-\d{6}-\d+\.log$` — `?file=../../../../etc/passwd` and any non-matching name are rejected (verified); byte-count `strlen()`/`substr()` only (mbstring not installed — no `mb_*`)
Routing: `/log` path route + host-based `log.gladex.de` block, both LAN-only (`isInvestorRoute` + explicit `REMOTE_ADDR` check with `denials.log`); `/log` added to the fallback-redirect exclusion so unknown-path → 302 behavior is unchanged
DNS: `A log.gladex.de → 77.90.15.49` TTL 3600 (pdns API 204, zone serial 2026092305), dig-verified on both authoritative NS
TLS: LE cert expanded to **7 SANs** (+log.gladex.de), expires 2026-12-22; `https://log.gladex.de/` 200, `http://` 301→https, apache reloaded, mail daemons unaffected
Tests
`tests/test_lan_middleware.php` extended: `/log` gated, `/team` + `/info` public, allow/deny sims → **49 passed, 0 failed** (repo, dev, prod)
Security
`/log` (run-log content) is reachable via the same public Apache ProxyPass path as the other investor routes — it inherits the open A/B/C decision from 0.4.3; no config change or spend made pending the investor's answer
v0.4.3 2026-09-23
Security
**Known issue — escalated to the investor, open decision**: Apache `:443` terminates TLS and proxies everything (`ProxyPreserveHost On` + `ProxyPass / http://127.0.0.1:8001/`), so the app sees `REMOTE_ADDR=127.0.0.1` for every request arriving through the VPS and the LAN-only gate allows it. Proven world-reachable: `/api/messages` (all 21 chat messages), `/investor`, `/mailbox` read + send (public `POST /mailbox/send` → 303 with real local delivery), `/budget`, `/trust`, `/changelog`, `/docs`; live scanner traffic (439 `.env`, 61 cms-probe, 60 scanner-UA requests, 244 `GET /investor` → 200). Gate logic and middleware tests (44 green) are unaffected — a direct non-proxied attempt from the VPS IP is correctly DENIED.
Not closed unilaterally: the operator's active session (Mac/iPhone via tunnel peer `10.2.3.1`) uses this exact path; blocking it would lock the investor out mid-session. Options sent to the investor (prod chat msg 22): **(A)** VPS XFF/PROXY-protocol + `mod_remoteip` trusting the tunnel peer, **(B)** Apache basic auth on investor paths, **(C)** VPS-side `:443` ACL. Recorded as a known limitation in `STRUCTURE.md`; no config change and no spend made pending the decision.
v0.4.2 2026-09-23
Added
**Mailbox tab** (`/mailbox`, LAN-only investor surface): identity switcher (lena / jonas / mia / leon / aylin / noreply), folder list (INBOX, Sent, Drafts, Trash, Junk), message list with unread markers, read view (MIME-aware plain-text rendering, marks messages read), compose + reply forms
Server-side IMAP only: connects `127.0.0.1:143` with the Dovecot master user; the credential is read from `/root/.imap-master-pw` per request and never appears in HTML, JS, URLs, cookies or logs
Sending via PHP `mail()` / `sendmail`; `From:` is derived server-side from the selected identity, one recipient per message, CSRF-protected POST `/mailbox/send`
Hard policy: `noreply@gladex.de` may read but is blocked from sending (HTTP 403)
Sent-folder copy via IMAP APPEND; replies carry `In-Reply-To`/`References` and set `\Answered` on the original
Mailbox nav entry on Investor Chat, Budget, Trust and Changelog; `/api/endpoints` documents `GET /mailbox` + `POST /mailbox/send`
LAN-only middleware + unit tests extended to `mailbox*` routes (44 assertions green)
Changed
`app/src/php/mailbox.php` is now versioned in the repo (was runtime-only) and promoted to prod with the standard `promote-dev-to-prod` flow
v0.4.1 2026-09-23
Changed
Mail TLS upgraded from snakeoil (`CN=startup-builder.lxd`) to the Let's Encrypt cert on ports 25/465/587/993 — all four verified `CN=gladex.de`, chain OK, expires 2026-12-22
New certbot renewal deploy hook reloads Postfix + Dovecot (previously only Apache), so mail TLS tracks renewals automatically
v0.4.0 2026-09-23
Added
Public team page at `/team` (team.gladex.de): Lena, Jonas, Mia, Leon — name, role, focus only (no birthdates, privacy rule)
Exact-match route dispatch for `/team` registered before the catch-all fallback (fixes 302-to-/investor bug)
TLS cert expanded to include team.gladex.de (6 SANs, expires 2026-12-22)
Postfix/Dovecot mail stack recorded in STRUCTURE.md (Maildirs, ports, relay policy, duties)
Fixed
`/team` returned 302 redirect to `/investor` on dev and prod — now serves HTTP 200 with team content; `/info` and `/stats` re-verified 200 on both
Postfix :25 220-greeting stalls (wedged smtpd child of old master, cleared by 19:43 restart) — verified fixed: 100/100 local soak, host mx-proxy banner OK, VPS:25 banner OK, live relay probe denied (454 4.7.1), full inbound E2E delivered
v0.3.1 2026-09-20
Added
`trust report` command: comprehensive trust statistics (total, average, median, min/max, levels, by-source, by-target, top/worst pairs)
Trust report supports `--format json` for programmatic access
4 new tests for trust report (human, empty, JSON, help)
LAN-only middleware: investor-facing routes gated to private IP ranges (127.0.0.0/8, 10.0.0.0/8, 192.168.0.0/16)
Denial logging: time, path, peer IP, X-Forwarded-For logged to `logs/denials.log`
36 unit tests for middleware (private allowed, public 403)
`/api/version` endpoint (public, returns app version + PHP version + env)
Minimalist landing page: single `<h1>gladex.de</h1>` on dark background
Binary download endpoint: `GET /download/gladex` serves compiled Go binary
Changed
Timezone display switched from UTC to Europe/Berlin (investor-facing)
Message polling uses FNV-1a hash diff (no more constant UI flashing)
Messages stored as Unix epoch seconds (backward-compatible with ISO strings)
Emojis removed from investor chat UI
CMD+Enter (macOS) / Ctrl+Enter (Windows/Linux) send shortcut added
Time filter checkbox: "Last 6h" filter for message history
Fixed
`crypto.subtle` undefined on plain HTTP (LAN) — replaced with FNV-1a hash
`X-Forwarded-For` never trusted for access decisions (logged only)
cobra flag persistence bug in workflow dry-run tests
v0.3.0 2026-09-20
Added
**Gladex Go CLI** — single binary, no runtime dependencies

- `gladex init` — workspace initialization with SQLite schema (contexts, workflows, trust_scores)

- `gladex context push/pull/list/delete` — CRUD operations on named contexts

- `gladex workflow create/list/show/delete/run/runs` — workflow engine with 5 step types (context, shell, set-var, print, http)

- `gladex trust record/list/get/confidence` — Bayesian trust scoring (accept/reject/partial outcomes)

- `gladex serve` — HTTP context server on configurable port (default :50051)

- `gladex mcp` — MCP-compliant JSON-RPC 2.0 server with 7 tools

- `gladex version` — build metadata (git SHA, build time via ldflags)

**MCP Server** with production middleware:

- JSON Schema validation for tool inputs

- Sliding-window rate limiting (100 req/60s per IP)

- Panic recovery (server never crashes)

- Request logging middleware

- Stress tested: 100/100 sequential tool calls, 0 errors

**VS Code Extension** (.vsix packaged):

- Sidebar panel with context display

- 4 commands: context sync, context show, workflow run, status

- Auto-sync on file save (configurable)

- Status bar integration

**CLI Tools** (7 agent tools):

- `domain-availability-check` — RDAP/DNS domain availability

- `budget-show` — parse/display BUDGET.md

- `dns-verify` — query DNS records via dig

- `healthcheck` — check dev/prod healthz endpoints

- `promote-dev-to-prod` — copy code to prod + restart

`ip-drift-check` — compare egress public IP vs DNS A record (read-only)

- `ip-drift-cron` — weekly drift detection + alert (detect only, never writes DNS)

**Integration tests**: 4 end-to-end tests (init → context → workflow → trust)
**Build metadata injection**: `build.sh` with ldflags (version, git SHA, build time)
**GETTING_STARTED.md**: 7-section guide from install to MCP integration
**MIT LICENSE** file
**PRODUCT.md**: Full product plan with MVP scope, milestones, brand guidelines
Infrastructure
Dev environment: `/opt/startup/dev` on port 8000 (PHP 8.3 + SQLite)
Prod environment: `/opt/startup/prod` on port 8001 (PHP 8.3 + SQLite)
Systemd services: `investor-app-dev`, `investor-app-prod`, `git-daemon`, `public-redirect`
Git remote: `git://git.gladex.de/gladex.git` (git-daemon on :9418)
DNS: gladex.de, dev.gladex.de, git.gladex.de → 77.90.15.49 (VPS via WireGuard)
nftables: 80→8000 (dev), 443→8001 (prod)
Weekly IP drift cron: Sunday 03:00 UTC
Tests
Go CLI: 65+ unit tests (context, workflows, MCP, trust, integration, stress)
PHP middleware: 36 tests (LAN-only access control)
All tests green on both dev and prod
v0.2.0 2026-09-20
Added
Dark theme (#0f0f13 background) for investor chat
Chat bubbles with role labels (Investor / Agent)
Auto-resize textarea, smooth scroll
FNV-1a hash diff (prevents constant re-rendering)
Unix timestamp storage (epoch seconds)
Changed
Complete CSS/JS rebuild of investor messaging frontend
v0.1.0 2026-09-20
Added
Initial Gladex Investor App (PHP 8.3 + SQLite)
Message thread view + investor send form
`/healthz`, `/api/messages` (GET/POST), `/api/status`
INBOX.md sync (POST writes to both SQLite and INBOX.md)
Persistent systemd services (investor-app-dev, investor-app-prod)