# Mishka CMS Migration v0.0.1 - API Reference

## Modules

- [MishkaCmsMigration](MishkaCmsMigration.md): The migration extension's Ash domain. The extension moves a site from another CMS into a Mishka
CMS site. No source ships yet: WordPress is planned as the first, and Joomla and others come later
as plugins that reuse the same mapping, dry run, import job and report.
- [MishkaCmsMigration.Activity](MishkaCmsMigration.Activity.md): The runs' log (`MishkaCmsMigration.MigrationLog`) as a source of the core's activity log, so a
run's steps show where the CMS shows what happened, each in the sentence the bell says
(`MishkaCmsMigration.Resources.Notifiers.Steps.sentence/2`).
- [MishkaCmsMigration.Application](MishkaCmsMigration.Application.md): The migration extension's OTP application. It supervises every mishka_installer plugin the
extension ships (`plugins/0`). A plugin registers on the event it listens on once it is supervised
here.
- [MishkaCmsMigration.Events](MishkaCmsMigration.Events.md): The three `mishka_installer` events an import fires, through `MishkaInstaller.Event.Hook.call/3`.
Each carries the run it belongs to, and `meta: %{actor: actor, site_id: site_id}` for a plugin
that takes `call/2`. A dry run fires none of them: it writes nothing in the CMS and asks no plugin.
- [MishkaCmsMigration.MigrationItem](MishkaCmsMigration.MigrationItem.md): What the migration remembers about one item of a source on a site: what the last dry run would
do with it (`plan`, `plan_reason`), the CMS record the last import wrote (`target_resource`,
`target_id`), when that record last changed as the import left it (`target_updated_at`), what the
source said about the item then (`title`, `source_slug`, `source_modified_at`, `content_hash`, and
the fields the CMS has no home for, `extra`), the SHA-256 of the file an import downloaded for a
media item (`file_hash`, which finds a second attachment of the same bytes), and what the last import that met it did
(`outcome`, `reason`, `run_id`). `outcome` is `nil` for an item no import has met yet, so a run's
counts (`MishkaCmsMigration.MigrationRun`'s `created_count` and the rest) count what its import
did alone. The item's content and its other fields, such as a post's status and author, are not
kept: every run reads them from the source again.
- [MishkaCmsMigration.MigrationLog](MishkaCmsMigration.MigrationLog.md): One entry of a run's log: a step of its life (`step`), who took it (`user_id`, none for a job
that failed on its own or that Oban discarded), and what it said: the host the run reads from,
a finished step's counts and a failed step's reason. Nothing else of the run is kept, so an entry
never holds its connection or its secret.
- [MishkaCmsMigration.MigrationRun](MishkaCmsMigration.MigrationRun.md): One move of a site's content from another CMS: which source it reads (`source`, a
`MishkaCmsMigration.Source` id), which host it reads from (`source_host`, so a site may take
content from several sources, each run showing its own), how it connects
(`connection`, and the secret sealed in `connection_secret`), what the admin chose (`options`)
and how far it got.
- [MishkaCmsMigration.MigrationRun.Options](MishkaCmsMigration.MigrationRun.Options.md)
- [MishkaCmsMigration.Provision](MishkaCmsMigration.Provision.md): What setting the migration up on a site gives it: the permission names its runs are gated by,
`admin:dashboard:migration` and `admin:api:migration`, so a site's admin can be granted them. The
names are shared by every site, so the first site's install creates them and every later one finds
them; the system writes them, whoever asked, through
`MishkaCmsCore.Install.Bootstrap.ensure_permissions/1`, as
`MishkaCmsCore.Runtime.ExtensionProvision` does for every extension. The extension ships no
components, pages or snippets.
- [MishkaCmsMigration.Repo](MishkaCmsMigration.Repo.md): The Postgres repo the extension's resources write through when it runs on its own. Inside the CMS
the resources write through the CMS's repo, which `config :mishka_cms_core, repo: …` names.

- [MishkaCmsMigration.Report](MishkaCmsMigration.Report.md): What a dry run or an import did with each item, saved on the run
(`MishkaCmsMigration.MigrationRun`'s `report`) and handed to `migration:import:finished`.
- [MishkaCmsMigration.Resources.Calculations.Ancestors](MishkaCmsMigration.Resources.Calculations.Ancestors.md): The items above each item at the source, for the item's `:ancestors` calculation: its parent's
item (`source_parent`, of its kind and source host on its site), that one's parent, and so on,
the top one first, as the Items page draws an item's place (Where it sits) and the Map view
places an item under its parents. `nil` for an item with no parent.
- [MishkaCmsMigration.Resources.Calculations.Decisions](MishkaCmsMigration.Resources.Calculations.Decisions.md): What waits for a decision now, and what an admin decided, among the items of each run's source
host, for the run's `:decisions` calculation: `%{waiting: waiting, decided: decided}`.
- [MishkaCmsMigration.Resources.Calculations.RecordPaths](MishkaCmsMigration.Resources.Calculations.RecordPaths.md): Where the CMS record of each item is, for the item's `:record_paths` calculation: the answer of
`MishkaCmsMigration.MigrationRun.record_paths/2` for its kind and the record it was written to,
with the record's `name` (its name, or its title), or `nil` for an item with no record, a kind
with no pages, and a record that is gone or that the reader may not read.
- [MishkaCmsMigration.Resources.Changes.CancelJobs](MishkaCmsMigration.Resources.Changes.CancelJobs.md): Cancels a run's jobs that are not done — waiting, scheduled, waiting to be tried again or
running — with `Oban.cancel_all_jobs/1`, in the same transaction as the action, once the run is
written. A job AshOban queued for one of the run's triggers would find the run out of the
trigger's `where` when it ran and stop there; cancelling it now leaves nothing waiting in the
queue for a run that is over.
- [MishkaCmsMigration.Resources.Changes.CheckConnection](MishkaCmsMigration.Resources.Changes.CheckConnection.md): Connects to a new run's source before the run is saved
(`MishkaCmsMigration.Source.connect/4`, with the run's `connection` and its sealed secret
opened), so a run is only kept for a source it can reach; a refusal is also told on telemetry.
What the source says of itself
(`c:MishkaCmsMigration.Source.extra/1`) becomes the run's `extra`.
- [MishkaCmsMigration.Resources.Changes.Choices](MishkaCmsMigration.Resources.Changes.Choices.md): Folds what the New import window's steps ask apart from the run's Advanced options into the run's
`options`: the `bring` argument (the switches of what to bring, such as `"bring_posts"` and
`"comments"`) and the `language` argument (the one language to put every item of an unknown or
unpublished language in, blank to keep the source's languages), as the run's `"one_language"`.
- [MishkaCmsMigration.Resources.Changes.Decided](MishkaCmsMigration.Resources.Changes.Decided.md): Keeps an admin's decision on the conflict an item waits on
(`MishkaCmsMigration.MigrationItem.conflict/1`): the choice is put in the item's `decisions`
under that conflict and brings the item back, so every later dry run and import follows it;
`"leave_out"` leaves the item out instead.
- [MishkaCmsMigration.Resources.Changes.EnqueueJob](MishkaCmsMigration.Resources.Changes.EnqueueJob.md): Queues the job of a run's AshOban trigger (`:trigger`) with `AshOban.run_trigger/3`, in the same
transaction as the action, once the run is written, as the actor who asked.
- [MishkaCmsMigration.Resources.Changes.PickedItems](MishkaCmsMigration.Resources.Changes.PickedItems.md): Turns the `items` argument of a run's `:dry_run_items` or `:import_items`, the ids of the items an
admin picked on the Items page (`MishkaCmsMigration.MigrationItem`), into what the run's job is
handed (`MishkaCmsMigration.Resources.Changes.EnqueueJob`): their source ids by kind, and the
status the run goes back to once the job is done, the one it is in now.
- [MishkaCmsMigration.Resources.Changes.Reason](MishkaCmsMigration.Resources.Changes.Reason.md): Keeps why a run failed: the action's `error` argument in words
(`MishkaCmsMigration.Report.reason/1`) as the run's `reason`. The error is what AshOban hands its
trigger's `on_error` action after the last attempt, or what Oban says of a job it discarded.
- [MishkaCmsMigration.Resources.Changes.SourceFile](MishkaCmsMigration.Resources.Changes.SourceFile.md): The file a run of a source that reads one (`c:MishkaCmsMigration.Source.file?/0`) is handed,
kept from the run's creation until it ends (see "The source's file" in
`MishkaCmsMigration.MigrationRun`).
- [MishkaCmsMigration.Resources.Notifiers.Steps](MishkaCmsMigration.Resources.Notifiers.Steps.md): What a run's steps tell the admins, an Ash notifier on `MishkaCmsMigration.MigrationRun`, run once
a step is saved.
- [MishkaCmsMigration.Resources.Validations.Connectable](MishkaCmsMigration.Resources.Validations.Connectable.md): Holds a step that queues a job, Run again (`:start_dry_run`) and the jobs of picked items
(`:dry_run_items`, `:import_items`), on a run that still holds what its source connects with
(`MishkaCmsMigration.MigrationRun.connectable?/1`): its sealed password, or the file it was
handed. A run an earlier release ended cleared both, and its job could only fail on the source's
refusal, so it is refused before anything is queued, in words: "This run no longer holds its
WordPress password. Start a New import.", or its file, for a source that reads one.
- [MishkaCmsMigration.Resources.Validations.MappedRecord](MishkaCmsMigration.Resources.Validations.MappedRecord.md): Holds an item's `mapped_id` (`MishkaCmsMigration.MigrationItem`'s `:choose`) to a record the item
may be mapped to: one of the CMS resource its kind is written to (`MishkaCmsMigration.Target`),
on the item's site, that the admin may read. `nil`, importing the item as its own, is always
taken.
- [MishkaCmsMigration.Resources.Validations.SiteIdle](MishkaCmsMigration.Resources.Validations.SiteIdle.md): Holds an admin's choice on an item (`MishkaCmsMigration.MigrationItem`'s `:leave_out`,
`:bring_back`, `:choose`, `:decide_keep` and `:decide_import`, and their master actions) while
one of the item's site's runs is dry running or
importing: the job reads the choices as it walks, so one made halfway would hold for part of the
run alone.
- [MishkaCmsMigration.Resources.Validations.SourceInstalled](MishkaCmsMigration.Resources.Validations.SourceInstalled.md): Refuses a run whose `source` names no installed source plugin
(`MishkaCmsMigration.Source.fetch/1`), on its `source` field, so a run is never created for a
source no job could read.
- [MishkaCmsMigration.Resources.Validations.Status](MishkaCmsMigration.Resources.Validations.Status.md): Holds a run's move to the statuses it may come from: the status the run is in before the action,
not the one the action sets.
- [MishkaCmsMigration.Source](MishkaCmsMigration.Source.md): A CMS a site is moved from: one module that is a `mishka_installer` plugin on the
`"migration:sources"` event and implements the callbacks below. WordPress is the first
(`MishkaCmsMigration.Sources.WordPress`); Joomla and others come the same way and reuse the
mapping, the dry run, the import job and the report.
- [MishkaCmsMigration.Source.Item](MishkaCmsMigration.Source.Item.md): One item of a source, as every source hands it over: the normalized model the dry run decides on
and a target writes from. A source's `c:MishkaCmsMigration.Source.normalize/3` returns the map
`build/1` turns into this struct; the walk puts `:kind` in, so a source never names it.
- [MishkaCmsMigration.Source.Item.Content](MishkaCmsMigration.Source.Item.Content.md)
- [MishkaCmsMigration.Sources.WordPress](MishkaCmsMigration.Sources.WordPress.md): The WordPress source (`"wordpress"`): a site read over its REST API, signed in to with a user
name and an application password (WordPress 5.6+, Users ▸ Profile ▸ Application Passwords).
WordPress offers application passwords over HTTPS only, or on a site whose
`WP_ENVIRONMENT_TYPE` is `local`.
- [MishkaCmsMigration.Sources.WordPress.Content](MishkaCmsMigration.Sources.WordPress.Content.md): A WordPress post's or page's `content.raw` as its item's content pieces
(`MishkaCmsMigration.Source.Item`): `%{html: ...}` for what is converted here, and
`%{name: ..., attrs: ..., raw: ...}` for a block or shortcode left to `migration:content:convert`
(`MishkaCmsMigration.Events`), in the order they come. `call/2` reads the content with LazyHTML,
an HTML5 parser, so its markup is never edited as text; only a shortcode, which is text, is read
with a pattern.
- [MishkaCmsMigration.Sources.WordPress.Elementor](MishkaCmsMigration.Sources.WordPress.Elementor.md): A WordPress page or post laid out with Elementor as its item's content pieces
(`MishkaCmsMigration.Source.Item`), and the widgets in it the CMS has no equivalent for.
- [MishkaCmsMigration.Sources.WordPress.ElementorKit](MishkaCmsMigration.Sources.WordPress.ElementorKit.md): Elementor's Site Settings, the kit a site's Elementor pages are drawn with: its global colours
and fonts, which a widget names by id (`__globals__`), and the theme tokens of the CMS they
become.
- [MishkaCmsMigration.Sources.WordPress.Export](MishkaCmsMigration.Sources.WordPress.Export.md): A WordPress export file (WXR, what Tools ▸ Export makes) read as it streams by
(`MishkaCmsMigration.Sources.WordPressFile`): `fold/3` hands each child of its `<channel>`, one at
a time, to a function, and never holds more of the file than the child it is reading.
- [MishkaCmsMigration.Sources.WordPress.Navigation](MishkaCmsMigration.Sources.WordPress.Navigation.md): A block theme's menus as the rows of a menu (`MishkaCmsMigration.Sources.WordPress.menus/4`).
- [MishkaCmsMigration.Sources.WordPressFile](MishkaCmsMigration.Sources.WordPressFile.md): The WordPress export file source (`"wordpress_file"`): a site read from the file WordPress makes
under Tools ▸ Export (WXR), for a site whose REST API cannot be reached or signed in to. It gives
the same items as the WordPress source (`MishkaCmsMigration.Sources.WordPress`), so a site moved
from its export lands in the CMS as it does from its REST API, but for what an export does not
carry (below), and a run of either finds what a run of the other wrote (both read from the same
host).
- [MishkaCmsMigration.Target](MishkaCmsMigration.Target.md): Where one kind of item goes in the CMS: one module that is a `mishka_installer` plugin on the
`"migration:targets"` event and names an existing CMS resource and the create and update actions
that write it. The extension adds no write path of its own: every record is written by that
action, as the admin who pressed Import and under the run's site, so the resource's policies,
validations and plugins decide as they do for anything else.
- [MishkaCmsMigration.Targets.BlogBlogger](MishkaCmsMigration.Targets.BlogBlogger.md): The `"user"` items of a source, its authors, written as the Blog's bloggers
(`MishkaBlog.Blogger`), each the profile of a user of the run's site, through the `:create` and
`:update` an admin's own blogger form runs, on the run's site and as the admin who pressed
Import: the admin needs the Blog's `admin:dashboard:blog:post`, and the Blog's own rules decide
the rest, such as one slug and one profile per user on a site, and that a profile names a user
of its own site.
- [MishkaCmsMigration.Targets.BlogCollection](MishkaCmsMigration.Targets.BlogCollection.md): The `"category"` items of a source written as the Blog's categories (`MishkaBlog.Collection`),
through the `:create` and `:update` an admin's own category form runs, on the run's site and
as the admin who pressed Import: the admin needs the Blog's `admin:dashboard:blog:post`, and
the Blog's own rules decide the rest, such as one slug per site and how a slug is written (its
`slugify`).
- [MishkaCmsMigration.Targets.BlogComment](MishkaCmsMigration.Targets.BlogComment.md): The `"comment"` items of a source written as the Blog's comments (`MishkaBlog.Comment`),
through the `:create` and `:update` an admin's own comment form runs, on the run's site and as
the admin who pressed Import: the admin needs the Blog's `admin:dashboard:blog:comment` to write
them and `admin:dashboard:blog:post` to find the posts and bloggers they belong to, and the
Blog's own rules decide the rest, such as how a comment's text is cleaned (it is plain text).
The Blog's `:create` keeps the author it is given (`author_id`), or a guest's name, instead of
making the acting admin the author.
- [MishkaCmsMigration.Targets.BlogPost](MishkaCmsMigration.Targets.BlogPost.md): The `"post"` items of a source written as the Blog's posts (`MishkaBlog.BlogPost`), through the
`:create` and `:update` an admin's own post form runs, on the run's site and as the admin who
pressed Import: the admin needs the Blog's `admin:dashboard:blog:post`, and the Blog's own rules
decide the rest, such as how a slug is written (its `slugify`), how its content is sanitized and
its code blocks highlighted.
- [MishkaCmsMigration.Targets.BlogTag](MishkaCmsMigration.Targets.BlogTag.md): The `"tag"` items of a source written as the Blog's tags (`MishkaBlog.Tag`), through the
`:create` and `:update` an admin's own tag form runs, on the run's site and as the admin who
pressed Import: the admin needs the Blog's `admin:dashboard:blog:post`, and the Blog's own
rules decide the rest, such as one slug per site and how a slug is written (its `slugify`).
- [MishkaCmsMigration.Targets.CmsLayout](MishkaCmsMigration.Targets.CmsLayout.md): The `"menu"` items of a source: every menu of the site, classic and block
(`MishkaCmsMigration.Sources.WordPress.Navigation`), written where an admin says it goes. A menu
becomes the links of the site's layout, the core's own navigation, through the `:update` an
admin's layout form runs (`MishkaCmsCore.Runtime.Layout`), on the run's site and as the admin who
pressed Import: the admin needs the core's `admin:dashboard:runtime:layout`, and the core's own
rules decide the rest, such as that the layout's template must compile.
- [MishkaCmsMigration.Targets.CmsMedia](MishkaCmsMigration.Targets.CmsMedia.md): The `"media"` items of a source, its files, written to the CMS's own media library
(`MishkaCmsCore.Runtime.Media`), on the run's site and as the admin who pressed Import: a file is
stored and its record made by the core's `:upload`, the action the media chooser of an admin's
forms runs, and written again by the `:update` an admin's own media form runs. The admin needs the
core's `admin:dashboard:runtime:media`, and the core's own rules decide the rest: which types it
keeps and how big, that a file's first bytes are the type its name says
(`MishkaCmsCore.Runtime.FileSignature`), where the file is stored, and that a name is unique
within a site.
- [MishkaCmsMigration.Targets.CmsPage](MishkaCmsMigration.Targets.CmsPage.md): The `"page"` items of a source written as the CMS's own pages (`MishkaCmsCore.Runtime.Page`),
through the `:create` and `:update` an admin's own page form runs, on the run's site and as the
admin who pressed Import: the admin needs the core's `admin:dashboard:runtime:page`, and the
core's own rules decide the rest, such as that a path is unique within a site and that a page's
template must compile.
- [MishkaCmsMigration.Targets.CmsSetting](MishkaCmsMigration.Targets.CmsSetting.md): The `"setting"` items of a source, the site's own name and pictures, written to the run's site's
settings (`MishkaCmsCore.Runtime.Setting`) through the `:set` the Settings page's save runs
(`MishkaCmsCore.Settings.put_as/4`), as the admin who pressed Import: the admin needs what
writing the setting takes (`MishkaCmsCore.Settings.write_permission/1`,
`admin:dashboard:setting` for these), and the core's own rules decide the rest.
- [MishkaCmsMigration.Targets.CmsStylesheet](MishkaCmsMigration.Targets.CmsStylesheet.md): The `"stylesheet"` items of a source, such as Elementor's global colours and fonts as theme tokens
(`MishkaCmsMigration.Sources.WordPress.ElementorKit`), written as a stylesheet of the run's site
(`MishkaCmsCore.Runtime.Stylesheet`) through the `:create` and `:update` the Stylesheets page
runs, as the admin who pressed Import: the admin needs `admin:dashboard:runtime:stylesheet` (or
the code permission), and the core's own rules decide the rest. A stylesheet Tailwind will not
build is refused (`MishkaCmsCore.Runtime.Resources.Changes.StylesheetCompiles`), and each write
builds the site's CSS again.
- [MishkaCmsMigration.Targets.Helpers.BySlug](MishkaCmsMigration.Targets.Helpers.BySlug.md): The `existing/2` (`MishkaCmsMigration.Target`) of a target whose resource keeps one record per
slug on a site, such as the Blog's tags and categories: an item whose slug the run's site already
holds a record with, in any language, as the Blog keeps one slug per site, waits for an admin's
decision as `"slug_taken"` (`MishkaCmsMigration.MigrationItem`'s Decide…). Connect to the existing
record connects the item to it (`"keep"`), and Import as new with another slug (`"import"`) writes
it as its own, with the slug `wanted/2` makes and the report saying so (`note/2`,
`"another_slug"`).
- [MishkaCmsMigration.Targets.Helpers.ContentChecks](MishkaCmsMigration.Targets.Helpers.ContentChecks.md): What a post or a page may wait on before it is written, for the targets that write it
(`MishkaCmsMigration.Targets.CmsPage`, `MishkaCmsMigration.Targets.BlogPost`), asked in this
order, the first that waits answering: its language, which the site may not publish in or the
source may not say (`MishkaCmsMigration.Targets.Helpers.Editions`), the widgets of a page builder
the CMS has no equivalent for (`MishkaCmsMigration.Targets.Helpers.Widgets`), its scripts and
inline event handlers
(`MishkaCmsMigration.Targets.Helpers.Scripts`), and the video players the site does not allow
yet (`MishkaCmsMigration.Targets.Helpers.Frames`). An admin decides each in turn.

- [MishkaCmsMigration.Targets.Helpers.Editions](MishkaCmsMigration.Targets.Helpers.Editions.md): An item's language and the other editions of it, for the targets whose resource keeps one record
per language, joined by `language_group_id` (`MishkaCmsCore.LanguageEditions`): the core's pages
(`MishkaCmsMigration.Targets.CmsPage`) and the Blog's posts, categories and tags
(`MishkaCmsMigration.Targets.BlogPost`, `MishkaCmsMigration.Targets.BlogCollection`,
`MishkaCmsMigration.Targets.BlogTag`). A source names them in the item's `"language"` and
`"translations"` (`%{"en" => "121"}`, the source id of each other edition by language), as the
WordPress source reads them from Polylang or WPML (`MishkaCmsMigration.Sources.WordPress`,
"Languages").
- [MishkaCmsMigration.Targets.Helpers.Frames](MishkaCmsMigration.Targets.Helpers.Frames.md): The video players a post's or a page's content frames, allowed on the run's site, for the
targets that write the content (`MishkaCmsMigration.Targets.BlogPost`,
`MishkaCmsMigration.Targets.CmsPage`). The CMS's Content-Security-Policy lets a page frame only
the origins of the site's `security.frame_origins` setting (Sites pages may embed), so an
imported YouTube or Vimeo player would show nothing without its origin there.
- [MishkaCmsMigration.Targets.Helpers.LanguageUrls](MishkaCmsMigration.Targets.Helpers.LanguageUrls.md): Whether the links an import writes may not match the site's addresses, for the targets that write
links (`MishkaCmsMigration.Targets.CmsLayout`).
- [MishkaCmsMigration.Targets.Helpers.Read](MishkaCmsMigration.Targets.Helpers.Read.md): The records a query finds for a target's `existing/2` (`MishkaCmsMigration.Target`), read as the
admin running the job under the run's site and the CMS's own policies.
- [MishkaCmsMigration.Targets.Helpers.Scripts](MishkaCmsMigration.Targets.Helpers.Scripts.md): The scripts and inline event handlers a post's or a page's content holds, for the targets that
write the content (`MishkaCmsMigration.Targets.CmsPage`, `MishkaCmsMigration.Targets.BlogPost`):
each `<script>` element and each `on…` attribute (`onclick`, `onerror`, ...), whatever the source
and the editor that made the content.
- [MishkaCmsMigration.Targets.Helpers.SeoTags](MishkaCmsMigration.Targets.Helpers.SeoTags.md): An item's `"seo"` (`MishkaCmsMigration.Sources.WordPress` reads it from Yoast SEO and Rank Math)
as the `seo_tags` a CMS page or a Blog post stores, for the targets that write them
(`MishkaCmsMigration.Targets.CmsPage`, `MishkaCmsMigration.Targets.BlogPost`). Each tag is
written as the core's head reads a stored tag (`MishkaCmsCore.Runtime.Seo.stored_tag/3`), and
replaces the default the head would draw for it
- [MishkaCmsMigration.Targets.Helpers.Status](MishkaCmsMigration.Targets.Helpers.Status.md): The status of a source item as a target that publishes it reads it
(`MishkaCmsMigration.Targets.BlogPost`, `MishkaCmsMigration.Targets.CmsPage`): the item's
`"status"`, except that an item behind a password (a non-empty `"password"` in its `extra`) is
`"password"` whatever its status says, since the CMS has no password-protected record to write it
as.

- [MishkaCmsMigration.Targets.Helpers.Widgets](MishkaCmsMigration.Targets.Helpers.Widgets.md): The widgets of a page builder an item was laid out with that the CMS has no equivalent for, such
as an Elementor form, slider or map, for the targets that write the content
(`MishkaCmsMigration.Targets.CmsPage`, `MishkaCmsMigration.Targets.BlogPost`). The source names
them in the item's `"unsupported_widgets"`, and puts what the builder drew for each in its place
in the content, its fallback HTML (`MishkaCmsMigration.Sources.WordPress.Elementor`).
- [MishkaCmsMigration.Targets.Helpers.Written](MishkaCmsMigration.Targets.Helpers.Written.md): The CMS records the same run, or an earlier one, wrote for a source's items of another kind, for
a target whose record points at them, such as a post's redirect at its post or a post's image at
its file: the items are read by the run's source host, the kind and their source ids
(`MishkaCmsMigration.MigrationItem`), and the records they were written to by their ids, as the
admin running the job (`MishkaCmsMigration.Targets.Helpers.Read`).
- [MishkaCmsMigration.Targets.PostRedirect](MishkaCmsMigration.Targets.PostRedirect.md): The `"post_redirect"` items of a source, each post's old address, written as the CMS's own
redirect rules (`MishkaCmsCore.Runtime.Redirect`), through the `:create` and `:update` an
admin's own redirect form runs, on the run's site and as the admin who pressed Import: the admin
needs the core's `admin:dashboard:runtime:redirect`, and the core's own rules decide the rest,
such as how an address is stored and that one site redirects an address once.
- [MishkaCmsMigration.Workers](MishkaCmsMigration.Workers.md): The `migration` jobs: AshOban's triggers on `MishkaCmsMigration.MigrationRun` generate the two
workers, `MishkaCmsMigration.Workers.DryRunWorker` (`:run_dry_run`) and
`MishkaCmsMigration.Workers.ImportWorker` (`:run_import`), each three attempts of an hour, run
as the admin who started the run (`MishkaCmsMigration.Workers.Helpers.Actor`). What a job does is
`MishkaCmsMigration.Workers.Helpers.Pipeline`. One run of a site is dry running or importing at a
time, held by the run's `:one_active_per_site` identity. Oban stops an attempt that runs longer
than its hour and tries the job again; an import's next attempt skips what the last one wrote and
recorded, so a large site goes on where it stopped.
- [MishkaCmsMigration.Workers.DryRunWorker](MishkaCmsMigration.Workers.DryRunWorker.md)
- [MishkaCmsMigration.Workers.Helpers.Actor](MishkaCmsMigration.Workers.Helpers.Actor.md): Who a migration job runs as: the admin who started its run. The `AshOban.ActorPersister` of
`MishkaCmsMigration.MigrationRun`'s triggers keeps the user's id in the job (`store/1`) and
loads the user with their permissions when the job runs (`lookup/1`), so a target writes as that
admin under the CMS's own policies, with the permissions they hold then. A user who is gone
fails the attempt.
- [MishkaCmsMigration.Workers.Helpers.Archived](MishkaCmsMigration.Workers.Helpers.Archived.md): The CMS records an admin archived. A resource with `AshArchival.Resource` hides an archived
record from its reads, so an item that remembers one (`MishkaCmsMigration.MigrationItem`'s
`target_id` or `mapped_id`) reads as though it were deleted, and a write that points at it, such
as a post's tags, is refused as a record "not found".
- [MishkaCmsMigration.Workers.Helpers.Decision](MishkaCmsMigration.Workers.Helpers.Decision.md): What a dry run or an import does with each item of a page (`call/3`), against what earlier runs
recorded for the site (`MishkaCmsMigration.MigrationItem`), found by the run's source host, the
item's kind and its source id. An admin's choice on the item comes first, before anything the
source or the CMS says
- [MishkaCmsMigration.Workers.Helpers.Discarded](MishkaCmsMigration.Workers.Helpers.Discarded.md): Fails the run of a migration job Oban discarded — its last attempt timed out or crashed past
every rescue, so AshOban never ran the trigger's `on_error` — with Oban's reason
(`MishkaCmsMigration.MigrationRun`'s `:fail`), which an admin can Run again. It runs outside any job,
so it acts as the system (`authorize?: false`), and hands `:fail` the job as AshOban hands it its
trigger's `on_error` (`context.ash_oban.job`), so what the run's steps tell says which step failed
(`MishkaCmsMigration.Resources.Notifiers.Steps`). A run no longer dry running or importing is left
as it is. Attached to Oban's `[:oban, :job, :exception]` telemetry by
`MishkaCmsMigration.Workers.attach/0`.
- [MishkaCmsMigration.Workers.Helpers.Needs](MishkaCmsMigration.Workers.Helpers.Needs.md): What the items of a page of a job of picked items need that the job was not asked for
(`MishkaCmsMigration.Workers.Helpers.Pipeline`): each item's parent and the items of other kinds
its record points at (`MishkaCmsMigration.Target`'s `needs/1`). A full run walks a parent before
its children and a kind before the kinds that point at it; a job of picked items brings nothing
else, so what it needs comes from what earlier imports recorded
(`MishkaCmsMigration.MigrationItem`), read in one query for the page, by the run's source host,
the kinds and the source ids. What the job was asked for too is never waited on: the walk brings
it first.
- [MishkaCmsMigration.Workers.Helpers.Pipeline](MishkaCmsMigration.Workers.Helpers.Pipeline.md): What a `migration` job does with its run: the manual `update/3` behind
`MishkaCmsMigration.MigrationRun`'s `:run_dry_run` and `:run_import`, the actions AshOban's
triggers of the same names run (`MishkaCmsMigration.Workers.DryRunWorker`,
`MishkaCmsMigration.Workers.ImportWorker`). It walks the run's source as the admin who started
the run and saves what came of it on the run.
- [MishkaCmsMigration.Workers.Helpers.Writer](MishkaCmsMigration.Workers.Helpers.Writer.md): What happens to one item once it is decided (`MishkaCmsMigration.Workers.Helpers.Decision`), and
its entry in the report (`call/3`, answering `MishkaCmsMigration.Report.entry/5`).
- [MishkaCmsMigration.Workers.ImportWorker](MishkaCmsMigration.Workers.ImportWorker.md)
- [MishkaCmsMigrationWeb.BlogAction](MishkaCmsMigrationWeb.BlogAction.md): The Import button on the Blog dashboard's header, a `"blog:dashboard:actions"` plugin
(`MishkaBlog.Dashboard.Actions`). It opens the migration page
(`MishkaCmsMigrationWeb.Live.Admin.Dashboard.Home.Index`), whose New import form offers the
Blog's choices; the form is not limited to the Blog's kinds.
- [MishkaCmsMigrationWeb.Flash](MishkaCmsMigrationWeb.Flash.md): The flashes of the migration pages, said by what they mean and drawn with the kinds the core has.
- [MishkaCmsMigrationWeb.Live.Admin.Dashboard.Home.Index](MishkaCmsMigrationWeb.Live.Admin.Dashboard.Home.Index.md): The migration's admin page: a card for each installed source (`MishkaCmsMigration.Source.all/0`),
and under them the runs of the admin's site, every site's for a master, as a Gervaz table (see
`MishkaCmsMigration.MigrationRun` for its columns and row actions). The table follows the runs'
PubSub notifications, which this page hands on to it.
- [MishkaCmsMigrationWeb.Live.Admin.Dashboard.Home.RunDetails](MishkaCmsMigrationWeb.Live.Admin.Dashboard.Home.RunDetails.md): A run's expanded row on the migration page (`MishkaCmsMigrationWeb.Live.Admin.Dashboard.Home.Index`),
drawn as the admin dashboard's cards are drawn (`MishkaCmsCore.Dashboard.Widget`). `panel/1` takes
the run as the page reads it.
- [MishkaCmsMigrationWeb.Live.Admin.Dashboard.ImportSteps](MishkaCmsMigrationWeb.Live.Admin.Dashboard.ImportSteps.md): The steps of the New import window (`MishkaCmsMigration.MigrationRun`'s form), for an admin who
wants few choices. The form is Gervaz's `:wizard` layout: every step is in the form and only the
current one shows, so an answer stays while the admin moves between them; Next checks the step
before it moves on, Enter in a field is Next, and only the review starts the run
- [MishkaCmsMigrationWeb.Live.Admin.Dashboard.Item.Branch](MishkaCmsMigrationWeb.Live.Admin.Dashboard.Item.Branch.md): Where an item sits at the source, drawn in its expanded row on the Items page
(`MishkaCmsMigrationWeb.Live.Admin.Dashboard.Item.Index`): the item under its parents
(`MishkaCmsMigration.MigrationItem`'s `:ancestors`), the top one first, each with where it stands
(`MishkaCmsMigration.MigrationItem.standing/1`): Needs your decision, Import failed (with why,
from its last import), Decided: ready to import, Not imported yet, Imported, Connected, Left out
by you or Not in the source. A decided item whose last import failed says why under it too. An
item with no parent draws nothing.
- [MishkaCmsMigrationWeb.Live.Admin.Dashboard.Item.Decisions](MishkaCmsMigrationWeb.Live.Admin.Dashboard.Item.Decisions.md): The Items page's Decisions panel (`MishkaCmsMigrationWeb.Live.Admin.Dashboard.Item.Index`): what
waits for a decision no admin has made among the items of the run's source host, grouped, so a
whole group is decided in one step instead of item by item. It reads the groups in one query
(`MishkaCmsMigration.MigrationItem.waiting_groups/3`) once the page is connected, and again once
after any change to the items it is told of (`refresh: true`, which the page sends for every
item's broadcast): a refresh asked for while one is read, or while a decision is applied, is read
once that is done, so a decision over a thousand items reads the groups once more, not a
thousand times. Nothing waits, nothing is drawn.
- [MishkaCmsMigrationWeb.Live.Admin.Dashboard.Item.Index](MishkaCmsMigrationWeb.Live.Admin.Dashboard.Item.Index.md): A run's Items page, `/admin/dashboard/migration/runs/:run/items`: the items the migration keeps
of the run's source host on the run's site (`MishkaCmsMigration.MigrationItem`), as the Gervaz
table that resource defines. It is every item of that host, whichever run met it last, each with
what the last dry run would do with it and what the last import did, and its "Imported by this
run" filter narrows them to the ones this run's import met last.
- [MishkaCmsMigrationWeb.Live.Admin.Dashboard.Item.Summary](MishkaCmsMigrationWeb.Live.Admin.Dashboard.Item.Summary.md): What came of an import of several items, a branch imported parents first or the rows picked
together, in the one flash the Items page shows once its job settled them
(`MishkaCmsMigrationWeb.Live.Admin.Dashboard.Item.Index`), each item named with its outcome in
its row's words (`MishkaCmsMigration.MigrationRun.reason_label/2`), what failed first
- [MishkaCmsMigrationWeb.Live.Admin.Dashboard.Item.Tree](MishkaCmsMigrationWeb.Live.Admin.Dashboard.Item.Tree.md): The Items table's Map view (`MishkaCmsMigration.MigrationItem`'s `switchable_templates`): a Gervaz
table template that draws the items as the source nests them, for deciding where each part of a
site goes before it is imported. The List view is the stock `MishkaGervaz.Table.Templates.Table`,
and the table's template switcher moves between them.
- [MishkaCmsMigrationWeb.Live.Admin.Dashboard.LanguageNotice](MishkaCmsMigrationWeb.Live.Admin.Dashboard.LanguageNotice.md): The warning a run shows when the links an import writes may not match the site's language
addresses (`MishkaCmsMigration.Targets.Helpers.LanguageUrls.cause/2`): the source runs Polylang or
WPML, or the site gives every language a prefix. `warning/1` draws it as the admin's own warning
(Gervaz's `:warning` alert, `MishkaGervaz.UIAdapters.Tailwind.alert/1`) in a run's expanded row
(`MishkaCmsMigrationWeb.Live.Admin.Dashboard.Home.RunDetails`) and on the Items page's head
(`MishkaCmsMigrationWeb.Live.Admin.Dashboard.Item.Index`): what the cause is, what is affected, a
tip and a suggestion. A run with no cause draws nothing. The cause is read from the run's `extra`
and the site's setting, which the settings cache answers, so it adds no query.
- [MishkaCmsMigrationWeb.Live.Admin.Dashboard.PasswordNotice](MishkaCmsMigrationWeb.Live.Admin.Dashboard.PasswordNotice.md): The warning for the users an import created (`MishkaCmsMigration.Targets.BlogBlogger`): a
source's password hashes cannot be carried over, so such a user signs in with Forgot password or
a social sign-in. Both parts draw it as the admin's own warning (Gervaz's `:warning` alert,
`MishkaGervaz.UIAdapters.Tailwind.alert/1`), as `MishkaCmsMigrationWeb.Live.Admin.Dashboard.LanguageNotice`
does
- [MishkaCmsMigrationWeb.Routes](MishkaCmsMigrationWeb.Routes.md): The migration's routes: its admin page, `/admin/dashboard/migration`, and a run's Items page,
`/admin/dashboard/migration/runs/:run/items`.

- [MishkaCmsMigrationWeb.SidebarMenu](MishkaCmsMigrationWeb.SidebarMenu.md): The migration's admin page and its sidebar entry, a `"sidebar:menus"` plugin
(`MishkaCmsCore.Sidebar`). It lands in the core's **Plugins** section, and the core never names
it: removing this extension removes the entry.

