Two concurrent undos of one entry both passed the already-undone check and
both restored. Undo and UndoBatch now read the entries FOR UPDATE, and the
final mark is conditional on undone_at IS NULL, refusing with
already_undone when another undo got there first.
The save-time simulation read the rules at READ COMMITTED without a lock,
so A -> B and B -> A written concurrently both passed. CheckRedirect,
AddressChanged and every batch now take a transaction-level advisory lock
before they read the rules.
AddressChanged wrote its 301 and re-pointed rules without Problems or the
save-time simulation, and removed only an exact rule whose source equals
the new address literally. It now removes every exact rule that matches
the new address with its options, simulates the 301 and the re-pointing
with the stored rules and writes nothing when that loops, stores an
absolute old address on the site as its path, and skips an address that
is not a path on the site.
The resolver fetched exact rules with lower(source) IN keys, while the keys
drop one trailing slash and keep the query as written, so a source stored
as /old/ or /list?Page=2 was never read. A new migration adds the stored
generated column source_key (the SQL form of exactKey) with an index for
exact rules, and the fetch compares it.
- POST /_seo/api/v1/batch runs changes in one transaction: dry_run
answers the diff and the conflicts (loops, chains, noindex on a
published record, canonicals to redirected or noindex addresses) and
rolls back; a refused change refuses the batch (D-12)
- undo of a change or a whole batch from History and through MCP, with
the undo_stale, undo_loop and undo_gone refusals (D-13)
- CSV export and import of redirects, rules and records by URL: BOM,
comma or semicolon, formula cells escaped, a dry-run preview, all or
nothing with via import; the screen's apply token is HMAC-SHA256 over
the file and a fingerprint of the data (transfer.stale)
- Import and export singleton with the seo_transfer widget action,
History record actions undo and undo_batch, navigation entry
- MCP tools undo_change, undo_batch, apply_batch, export_csv, import_csv
- TestBatch, TestUndo, TestCSVTransfer, TestLoopRefused mcp and import
- the RecordSaved listener re-describes an updated record of a content
type after commit and applies AddressChanged per changed locale, so
Journal's API, MCP and imports get the 301 too; an admin save that
already applied it changes nothing more
- POST /_seo/api/v1/notfound with the golem15.seo.notfound_token bearer
(constant-time, off when unset), URL and referer caps, a row cap with
an hourly warning, throttle bucket seo-notfound
- golem15.seo.notfound list by hits with Resolved rows, a preview with the
resolved hint, a prompted 301 and a 410 record action, a bulk 410 that
refuses a selection with a covered address
- every /_seo/api/v1 answer carries X-Robots-Tag: noindex
- golem15.seo.redirects list and form (UI-SPEC C8): chain rows marked,
status, match, origin and chain filters, the navigation item lands here
- a field-less extension on the controller refuses a loop with
redirects.loop on target, answers the chain notice and records history
- the status dropdown posts text, read through RedirectStatus
- rule set: exact sources by key, patterns by literal prefix, regex by id
(stdlib RE2, compiled once per source); ignore case, trailing slash,
query ignore/pass, a source with ? matches its query exactly
- targets: captures escaped, a relative target never leaves the site, an
absolute one keeps its host
- SaveRedirect validates, simulates the rule in place (exact, pattern and
regex probes, 10 hops) and refuses a loop; chains answer the notice
- resolve follows the chain to its final target, 410 answers gone, a
runtime cycle answers not_found and is logged once
- golem15_seo_redirects table and Redirect model
- AddressChanged: old -> new 301 in the save transaction, earlier
redirects pointed at the new address, self-loops dropped
- the SEO tab save compares descriptor URLs with the entry URLs and
answers the redirects.auto_created notice
- resolve answers redirect with the absolute target for an exact source