feat: batches with dry run, undo, and CSV import and export

- 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
This commit is contained in:
2026-10-27 12:35:26 +01:00
parent aa0c3bcacf
commit ab3b219fec
27 changed files with 3319 additions and 96 deletions

View File

@@ -8,6 +8,7 @@ nav:
seo: SEO
history: History
rules: URL rules
transfer: Import and export
permissions:
tab: SEO
manage: Manage SEO
@@ -184,6 +185,23 @@ history:
diff_before: Before
diff_after: After
list_empty: No SEO changes recorded yet. Every change from the admin, MCP or an import appears here and can be undone.
undo: Undo this change
undo_confirm: Undo this change? The values before it are restored, and the undo is recorded as a new change.
undone: Change undone.
undo_batch: Undo the whole batch
undo_batch_confirm: Undo every change of this batch? It is undone as a whole or not at all.
batch_undone: Batch undone.
undo_stale: "“:field” has changed since: :author changed it on :time. Undoing would overwrite that. Nothing was undone."
undo_loop: "Undoing would create a redirect loop: :path. Nothing was undone."
undo_gone: The :type this change belongs to no longer exists. Nothing was undone.
undo_done: This change is already undone.
undo_unsupported: This change cannot be undone here. Nothing was undone.
record: record
kind_rule: URL rule
kind_redirect: redirect
kind_robots: robots.txt
kind_settings: SEO settings
kind_notfound: 404 log entry
jobs:
generate_entries: 'SEO: generate entries'
effective:
@@ -298,3 +316,32 @@ api:
bot_unknown: This is not an AI bot of the robots.txt screen.
value_invalid: This value is not valid here.
type_unknown: No content type with this key is registered.
batch_too_large: "A batch takes at most :max changes."
change_not_found: The record this change names does not exist.
op_unknown: "Unknown operation “:op”."
conflict_noindex_published: "“:url” is a published page listed in the sitemaps; noindex removes it from search results."
transfer:
title: Import and export
delimiter: CSV delimiter
delimiter_comma: Comma (,)
delimiter_semicolon: Semicolon (;), as Excel uses in Polish
delimiter_comment: Files are UTF-8 with a byte order mark, so Excel opens Polish characters correctly.
widget: Files
preview: Preview import
applied:
one: "Applied :count change as one batch. You can undo it in History."
other: "Applied :count changes as one batch. You can undo them in History."
stale: The data changed after the preview. Preview the file again before applying.
unreadable: The file could not be read as CSV. Check the delimiter and that the first row holds the column names.
missing_column: "The file has no “:column” column. Download an export to see the expected columns."
dataset_unknown: Choose redirects, URL rules or SEO of records.
too_large: This file is larger than 60 KB. Split it into smaller files, or import it through MCP.
too_large_mcp: This file is larger than 900 KB. Import it in smaller parts.
export_too_large: This export is too large to download here. Export it through MCP, page by page.
refused_blocks: Fix the refused rows in the file and preview it again. Nothing is applied while a row is refused.
token_invalid: This preview is no longer valid. Preview the file again.
row_loop: "Creates a redirect loop: :path"
row_chain: "Creates a chain: :path"
row_canonical: "The canonical URL redirects to “:target”"
row_rule: "No record at this address: a URL rule is created"
row_invalid: ":field: :message"

View File

@@ -8,6 +8,7 @@ nav:
seo: SEO
history: Historia zmian
rules: Reguły adresów
transfer: Import i eksport
permissions:
tab: SEO
manage: Zarządzanie SEO
@@ -190,6 +191,23 @@ history:
diff_before: Przed
diff_after: Po
list_empty: Nie zarejestrowano jeszcze zmian SEO. Każda zmiana z panelu, MCP lub importu pojawi się tutaj i będzie można ją cofnąć.
undo: Cofnij tę zmianę
undo_confirm: Cofnąć tę zmianę? Przywrócone zostaną wcześniejsze wartości, a cofnięcie zostanie zapisane jako nowa zmiana.
undone: Cofnięto zmianę.
undo_batch: Cofnij całą partię
undo_batch_confirm: Cofnąć wszystkie zmiany z tej partii? Cofa się całość albo nic.
batch_undone: Cofnięto partię.
undo_stale: "„:field” zmieniło się od tego czasu: :author zmienił je :time. Cofnięcie nadpisałoby tę zmianę. Nic nie cofnięto."
undo_loop: "Cofnięcie utworzyłoby pętlę przekierowań: :path. Nic nie cofnięto."
undo_gone: ":type, którego dotyczy ta zmiana, już nie istnieje. Nic nie cofnięto."
undo_done: Ta zmiana jest już cofnięta.
undo_unsupported: Tej zmiany nie można tu cofnąć. Nic nie cofnięto.
record: Rekord
kind_rule: Reguła adresu
kind_redirect: Przekierowanie
kind_robots: robots.txt
kind_settings: Ustawienia SEO
kind_notfound: Wpis dziennika 404
jobs:
generate_entries: 'SEO: generowanie wpisów'
effective:
@@ -308,3 +326,34 @@ api:
bot_unknown: To nie jest bot AI z ekranu robots.txt.
value_invalid: Ta wartość jest tu nieprawidłowa.
type_unknown: Nie zarejestrowano typu treści o tym kluczu.
batch_too_large: "Partia może mieć najwyżej :max zmian."
change_not_found: Rekord wskazany w tej zmianie nie istnieje.
op_unknown: "Nieznana operacja „:op”."
conflict_noindex_published: "„:url” to opublikowana strona z map witryny; noindex usunie ją z wyników wyszukiwania."
transfer:
title: Import i eksport
delimiter: Separator CSV
delimiter_comma: Przecinek (,)
delimiter_semicolon: Średnik (;), jak w polskim Excelu
delimiter_comment: Pliki są w UTF-8 ze znacznikiem BOM, więc Excel poprawnie otwiera polskie znaki.
widget: Pliki
preview: Podgląd importu
applied:
one: "Zastosowano :count zmianę jako jedną partię. Możesz ją cofnąć w Historii zmian."
few: "Zastosowano :count zmiany jako jedną partię. Możesz je cofnąć w Historii zmian."
many: "Zastosowano :count zmian jako jedną partię. Możesz je cofnąć w Historii zmian."
other: "Zastosowano :count zmiany jako jedną partię. Możesz je cofnąć w Historii zmian."
stale: Dane zmieniły się po podglądzie. Wyświetl podgląd pliku ponownie przed zastosowaniem.
unreadable: Nie udało się odczytać pliku jako CSV. Sprawdź separator i czy pierwszy wiersz zawiera nazwy kolumn.
missing_column: "W pliku brakuje kolumny „:column”. Pobierz eksport, aby zobaczyć oczekiwane kolumny."
dataset_unknown: Wybierz przekierowania, reguły adresów lub SEO rekordów.
too_large: Ten plik jest większy niż 60 KB. Podziel go na mniejsze pliki lub zaimportuj przez MCP.
too_large_mcp: Ten plik jest większy niż 900 KB. Zaimportuj go w mniejszych częściach.
export_too_large: Ten eksport jest za duży, by pobrać go tutaj. Wyeksportuj go przez MCP, strona po stronie.
refused_blocks: Popraw odrzucone wiersze w pliku i ponownie wyświetl podgląd. Nic nie zostanie zastosowane, dopóki jakiś wiersz jest odrzucony.
token_invalid: Ten podgląd jest już nieważny. Wyświetl podgląd pliku ponownie.
row_loop: "Tworzy pętlę przekierowań: :path"
row_chain: "Tworzy łańcuch: :path"
row_canonical: "Adres kanoniczny przekierowuje na „:target”"
row_rule: "Pod tym adresem nie ma rekordu: zostanie utworzona reguła adresu"
row_invalid: ":field: :message"