−
100%
+
fit
scroll
# Portal API Reference (Day 382) Working notes
on the `/api/things` surface, grounded in what
was actually exercised during tonight's Color
Chain migration — not a full endpoint
catalog. File:line refs are
`Portal/rust/src/server/at_routes.rs` unless
noted. ## Core Things endpoints | Endpoint |
Handler | Notes | |---|---|---| | `GET
/api/things?sapphire=X&ring=Y&being=Z&limit=N&cursor=...`
| `api_things_list` (3519) | Per
[[project_things_api_filtering]] — only
`sapphire`/`ring`/`being`/`limit`/`cursor`
actually filter; `onyx`/`ruby.*` query params
are silently ignored, filter those
client-side. | | `GET /api/things/:id` |
`api_things_get` (3535) | Full Thing incl.
`gem_values`. This is what `xo read <uuid>`
resolves to under the hood. | | `GET
/api/things/by-name/:name` |
`api_things_by_name` (3567) | Lookup by
`onyx`. | | `GET /api/things/by-type/:type` |
`api_things_by_type` (3622) | Lookup by
`sapphire`. | | `GET /api/things/by-gem` |
`api_things_by_gem` (4220) | Generic gem-value
query — what `△ gem` and `△ thing` MCP
tools likely call through. | | `PATCH
/api/things/:id/gems` |
`api_things_patch_gems` (4290) | **The edit
path.** See below — this is the one that
actually moved tonight's migration. | | `POST
/api/things/ingest` | `api_things_ingest`
(4370) | Always INSERTs a new, unrelated Thing
— no upsert, no dedup (per
[[project_thing_box_thing|Thing Box]] notes
— caused a real duplicate-asset incident
previously). Don't use this to edit; use the
PATCH path. | | `GET
/api/things/:id/snapshots` |
`api_things_snapshots` (4343) | Version
history — every `PATCH .../gems` call
auto-snapshots the prior `gem_values` first
(4302-4310), so edits carry real lineage. | ##
`PATCH /api/things/:id/gems` — the
mechanism, and its real gotcha ```json {
"gem_values": { "ruby": {...}, "topaz": {...}
}, "merge": true } ``` - **`merge: true`**
(default) runs `gem_values = gem_values || $1`
(4314-4319) — **Postgres JSONB `||` is a
shallow, top-level merge.** Any gem-slot key
you include in the payload (`ruby`, `topaz`,
`diamond`, etc.) **replaces that slot's entire
value**, not just the sub-fields you mention.
Tonight's migration learned this the hard way:
patching `diamond: {swatches: [...]}` without
first including the existing
`crown`/`ring`/`summoner`/`amulet` keys
silently wiped them on 24 Color Things — had
to re-fetch current state and re-patch with
everything included. **Always fetch the
current `gem_values` for a slot before
patching it, and send the full slot back with
your changes merged in, not just the new
keys.** - **`merge: false`** replaces
`gem_values` wholesale (4320-4324) — even
more destructive, full overwrite. -
Auto-snapshot happens regardless (4302-4310)
via `db::snapshot_thing`, so a botched patch
is always recoverable from history — but
recovery isn't automatic, you still have to
notice and re-fetch/re-apply. ## Auth —
softer than expected `api_things_patch_gems`
has **no Authorization/Bearer check in the
handler itself** (confirmed by reading
4290-4336 — no token validation, no role
check). The 403s hit during tonight's
migration were **Cloudflare's bot-management
(error code 1010)**, not application auth —
triggered by Python's default `urllib`
User-Agent. Switching the write loop from
`python urllib` to plain `curl` resolved it
with **zero auth header needed**; adding
`Authorization: Bearer <token>` (via `△
my_token`) made no observable difference once
the UA issue was fixed. This is worth treating
as a real finding, not just a workaround: the
live PATCH-gems route currently has **no
app-level write authorization** — anyone who
can get past Cloudflare's bot check can mutate
any Thing's `gem_values`. Cross-reference
[[project_fid_priority_knob]]'s sibling
segment `token-hardening` (FID, already on the
spiral, currently 5/5 gaps open) — this
finding belongs there, not just in this note.
## Palette-specific endpoints (tonight's
actual subject) | Endpoint | Handler | Notes |
|---|---|---| | `GET /api/palette` |
`api_palette_active` (7100) | Active palette
name from `config::get_str("active-palette")`,
defaults to `hexcraft-dark`, delegates to
`api_palette_by_name`. | | `GET
/api/palette/:name` | `api_palette_by_name`
(7109) | Resolves Color→Swatch chain. **Bug,
not fixed tonight:** the swatch query
(7146-7148) has no palette-membership filter
— it returns every `sapphire=swatch` Thing
regardless of `:name`. Two Palette Things
exist (`hexcraft-dark`, `timepiece-palette`)
but only the metadata block differs per name;
the swatch set is identical either way. This
is gap `ccr-004` on the `color-chain-rebirth`
segment. | | | | **Fixed tonight:** the
resolver was reading `hex`/`rgb` from `ruby`
(`Gem::Mark`) (7129-7137), which broke when
the Color migration moved those fields to
`topaz` (`Gem::Data`) per the locked schema.
Patched to read `topaz` instead — rebuilt,
restarted `portal-hexlet` via pm2, verified
live. | ## Source pointers -
`Portal/rust/src/server/at_routes.rs:3519-4520`
(core Things CRUD), `:4290-4336`
(`api_things_patch_gems`), `:7099-7193`
(palette). -
`Portal/rust/src/things/theme.rs:18-86` —
the TUI-side equivalent resolver, same
`ruby`→`topaz` fix applied there too (Day
382, alongside the API fix, in
[[Color-Chain-Rebirth.00382]]).
△