Unverified Commit fad48ff2 authored by Minseong Choi's avatar Minseong Choi 💬
Browse files

docs(changes): establish the change ledger for functional changes

Add docs/changes/ — a durable, in-repo map of every functional change and
the commit that records it, independent of git log. INDEX.md carries the
convention (each functional change gets a dated detail doc plus a ledger
row) and the full oldest-first ledger, regenerable losslessly from git.
Seed detail docs for the two changes just landed: the break-glass halt op
(c2ee21ae) and the /felis migrate command (c1aa38ba).
parent c1aa38ba
Loading
Loading
Loading
Loading
+83 −0
Changes for docs/changes/2026-07-05-break-glass-halt.md: 83 added lines, 0 removed lines.
Original line number Diff line number Diff line
# Break-glass "halt a running server" op (#31 B4)

- **Type:** feature (addition)
- **Date:** 2026-07-05
- **Area:** `cmd/felis` — break-glass recovery console (Go, oracle-verifiable)
- **Commit:** `c2ee21a` — feat(breakglass): add halt-a-server op to the recovery console (§B4)
- **Task:** #31 Phase B4 (felis TUI break-glass ops)

## What it does

Adds a **"Halt a running server"** operation to the root-gated break-glass console.
The operator picks a server from the live fleet and the console flips that
`MinecraftServer` CRD's `spec.desiredState` to `Stopped`, letting the operator
reconcile it into a graceful shutdown. It is the emergency "stop this now" lever for
when the panel is unreachable but the box still has `root` + a kubeconfig.

## Why

The break-glass console already provisions the Owner and adds Operators, but there
was no local, panel-independent way to **stop** a misbehaving server (runaway,
compromised, resource-pinning). Halting is a reversible state nudge — the safest
possible break-glass power — so it belongs in the same root-gated recovery surface.

## Design decisions

- **CRD write, not pod kill.** The console flips `spec.desiredState=Stopped` with a
  **spec-only merge patch** (`client.MergeFrom`), never a full-object `Update`. The
  operator writes `status` on the same object continuously; a merge patch of
  `spec.desiredState` touches a disjoint field and cannot race/clobber the operator's
  status writes. A halt is therefore exactly the CRD write the operator already knows
  how to honour.
- **Authority = root + kubeconfig.** The accountable actor is the OS user who
  escalated to root (`osUser`), recorded for attribution — not proof. The root gate
  plus kubeconfig possession *is* the authority, so (unlike the owner/operator paths)
  no credential-minting auth sub-flow is needed for a reversible state change.
- **System servers allowed but named.** Halting the `login`/`lobby` system servers
  takes the shared front door down (login has no fallback). Break-glass is deliberately
  full power, so the console **warns** rather than forbids: a `⚠ system` tag in the
  picker and an explicit `WARNING` line in the post-exit summary.
- **Audit is best-effort.** `performHalt` mirrors `performBreakGlass`: the halt
  succeeds even if the audit sink is down (break-glass must work with logging broken);
  any audit error rides back in the outcome and is surfaced as a summary `WARNING`.
- **Already-stopped is a no-op** reported distinctly ("was already stopped" vs "is now
  stopping"), so the console never claims a stop it didn't perform.
- **Namespace from config.** The target namespace is `cfg.K8s.Namespace`, threaded
  through the console constructors — never hardcoded.

## Files

| File | Change |
|---|---|
| `cmd/felis/halt.go` | **new** — pure core (no bubbletea): `listServersForHalt`, `haltServer` (merge patch), `isSystemServer`, `performHalt`, `auditHalt` |
| `cmd/felis/halt_test.go` | **new** — table tests against a controller-runtime **fake client** (applies patches for real): running→stopped persists, already-stopped no-op, missing→error, system flag, list projection + desired-state fallback, audit success, audit-failure-still-halts |
| `cmd/felis/tui_halt.go` | **new** — bubbletea/huh shell mirroring `ownerModel` (load → pick → work → done), empty-fleet guard, `⚠ system` picker labels, outcome card |
| `cmd/felis/tui_menu.go` | `bgHaltServer` enum + "Halt a running server" menu option |
| `cmd/felis/tui_root.go` | `namespace` field; `bgHaltServer` dispatch to `newHaltModel`; `haltResultMsg` terminal handling |
| `cmd/felis/breakglass.go` | halt fields on `breakGlassResult`; `namespace` threaded through `runBreakGlassTUI`/`runSetupTUI`/`runConsoleTUI`; post-exit halt summary (stopping / already-stopped, system + audit warnings, restart hint) |
| `cmd/felis/setup.go` | pass `cfg.K8s.Namespace` into `runSetupTUI` |
| `cmd/felis/tui_root_test.go` | pass `"minecraft"` namespace into `newRootModel` test call |

## Verification

WSL oracle (go1.26.4, FedoraLinux-44), authoritative for Go:

```
go build ./...            → BUILD_OK
go vet ./cmd/felis/...    → VET_OK
go test ./...             → all 20 packages ok, ALL_GREEN
```

The core (`halt.go`) is fully unit-tested against a real `fake.Client`, which applies
the merge patch, so the test asserts the **persisted** `spec.desiredState`, not merely
that `Patch` was called. `tui_halt.go` is thin bubbletea glue (untested by house
convention, mirrors the existing `tui_owner.go`).

## Self-review outcome

- **ponytail (over-engineering):** lean — no one-impl interface, every field consumed,
  audit seam justified. Nothing cut.
- **correctness:** caught and fixed a misleading restart hint — the summary originally
  pointed at `felis apply`, but that command is **create-only** (errors "already
  exists" on an existing server); corrected to "restart from the panel, or set
  `spec.desiredState` back to Running."
+80 −0
Changes for docs/changes/2026-07-05-felis-migrate-command.md: 80 added lines, 0 removed lines.
Original line number Diff line number Diff line
# `/felis migrate` in-game command (§B3 inherit, Velocity side)

- **Type:** feature (addition)
- **Date:** 2026-07-05
- **Area:** `plugins/velocity` + `plugins/shared` — Velocity proxy plugin (Java, compile-verified)
- **Commit:** `c1aa38b` — feat(velocity): add /felis migrate to open an account migration (§B3 inherit)
- **Task:** completes the code-only gap named in `internal/api/handlers_account_migrate.go`

## What it does

Adds the in-game `/felis migrate` command that a player runs to **open an account
migration** — the first step of handing their owned servers to another account (spec
§B3 "inherit", scenario A). The command posts the player's Mojang-verified UUID to the
backend, which puts that account into migrate mode (`state=initiated`). The player then
finishes the migration on the web console (prove it's them, name the receiving account,
redeem a one-time code).

The Go backend (`handleMigrateStart` and the web-driven steps 2–4) already existed and
was tested; its header comment explicitly named **"the `/felis migrate` command that
calls handleMigrateStart"** as the code-only gap. This change closes that gap.

## Why

Without the in-game command, the migration flow had no entry point — the backend
handler was reachable only in theory. `/felis migrate` is the trustworthy initiator:
Velocity has already established the caller's online-mode UUID, so the sensitive proof
can be deferred to the web step-up while the in-game command just opens the migration.

## Design decisions

- **Mirrors the existing command suite verbatim.** `doMigrate` follows `doClaim`;
  `migrateError` follows `claimError`; `migrateStart` follows `claim`/`opLoginApprove`.
  No new imports, types, or idioms — every construct already appears in the same files.
- **Identity-bound + out-of-limbo, but server-independent.** Like `claim`, it requires
  a real player past the login limbo (`requirePlayer` + `ensureOutOfLimbo`). Unlike
  `claim`, it acts on the caller's *account*, not the server they stand on, so there is
  **no** `registry`/current-server check.
- **Expects HTTP 201.** `migrateStart` posts to
  `/api/v1/internal/account/migrate/start` and expects **201 Created** (`handleMigrateStart`
  returns `StatusCreated`) — not 200 like the other calls. A 201 that does not affirm
  `started:true` is treated as a contract breach, not a refusal.
- **Error mapping matches the handler's refusals:** 404 `not_linked` → "Link your
  account on the web console before migrating"; 409 `account_retired` → "This account
  can't start a migration (already migrated or retired)"; transport (0) and default →
  generic retry text.
- **Points the player to the console on success.** The command only *opens* the
  migration, so on success it prints the player web console URL
  (`https://console.<root_domain>`, derived from config — never a hardcoded domain) and
  a one-line description of the remaining steps. A proxy-side `logger.info` records the
  initiating username against the UUID (the backend audit only has the UUID).

## Files

| File | Change |
|---|---|
| `plugins/shared/.../link/FelisApiClient.java` | **+`migrateStart(UUID)`** — POST mc_uuid, expect 201, affirm `started:true` |
| `plugins/velocity/.../FelisVelocityPlugin.java` | `migrate` literal in the Brigadier tree; **`doMigrate`** handler; **`migrateError`** mapper; `/felis migrate` help line |

## Verification

Java is not oracle-verifiable via the Go suite, but it **is** compile-verifiable via
the podman gradle toolchain established in #63/#65:

```
podman run --rm -v plugins:/work -w /work/velocity \
  docker.io/library/gradle:jdk17 gradle --no-daemon compileJava
→ BUILD SUCCESSFUL in 19s   (compiled against real velocity-api:3.3.0-SNAPSHOT)
```

The change compiles clean against the real Velocity API jar (including the shared
`FelisApiClient` compiled straight into the velocity module). The backend contract it
speaks to (`handleMigrateStart`) is covered by `handlers_account_migrate_test.go` on
the Go side.

## Self-review outcome

- **ponytail (over-engineering):** lean — pure mirror of three existing, compiling
  methods; no speculative abstraction. Nothing cut.
- **correctness:** the one contract divergence (201 vs 200) was verified against the Go
  handler source before writing.

docs/changes/INDEX.md

0 → 100644
+206 −0

File added.

Preview size limit exceeded, changes collapsed.