M8-08: Import overview in the console¶
Status: ready | Depends on: M6-04, M8-03 | Milestone: M8
Goal¶
After this ticket the web console has the page "Import" of 01 5.5 (feature row "Import from Discord", web only): a list of import sources with kind, state and progress; a source page with the counters of the server and of the tool (stats.server.*, stats.tool.*), the items in state failed, and one row per mapped channel with its Discord ID, its Bothy channel, the stored lead (mirror or "Bothy leads"), the read-only state and whether the channel's operator equals the planned operator; and every owner step of import.manage that the command-line tool cannot do because a cli session never provides step-up (08 6.4, 22 8): create a source by uploading the mapping file (YAML, converted to JSON in the browser, server errors shown at their JSON pointers), start, pause, cancel and complete (each with confirmation and step-up), "Bothy leads" for a channel (confirmation that names the Discord step and the switch command, then setImportLead bothy, then the guided setChannelOperator to the planned operator with the I1 check) and "Discord leads" (revert: setImportLead discord, then remove the operator). The page refreshes after every action, on a refresh control and when the browser tab becomes visible; it reads the same ImportSource model that import.progress carries. patrol web tests run these flows in make console-e2e. Why: without these screens no source can be created or started and no channel can switch over, because those steps need step-up (22 8, 13.3, 13.4, D-W-124).
Read first¶
docs/spec/01-product.md: 3.7 (row "Import from Discord": web,import.manage), 5.2 (confirmation with step-up), 5.5 (console navigation with "Import"), 8, 9.docs/spec/22-import-migration.md: 5.2 (mapping file), 8 (operations, thecliparagraph), 11 (report columns), 12.1 (ownership list), 13 (flow per channel; steps done in the console), 14.docs/spec/05-actions.md(core contract, read only): 4.12import.read,import.manage, rule 1.8,channel.operator.set,channel.read_only.set.docs/spec/10-backend-policy.md(I1 and the guided switch).docs/spec/15-client-architecture.md3.1 rule 3 (console without gateway connection), 20 (row E2E:patrolfor the console).docs/api/openapi.yaml: tagImport,ImportSourceDiscordConfig(M8-03),setChannelOperator,getChannel,listAgents.docs/DECISIONS.md: D-W-100, D-W-120, D-W-123, D-W-124; rowsD-M6-04-<n>,D-M8-03-<n>; section 10 format.- Briefs (merged names win, D-W-100):
M6-04-console-admin.md(hostingadmin_uiin the console, the step-up adapter,patrolweb suites inconsole-e2e),M6-02-console-app.md(routes, HTTP layer, CSRF),M6-03-console-csp.md(console-e2e, the seed job, the CSP violation collector),M4-25-settings-devices.md(adding a device by pairing code, which the owner uses to approvebothy import login),M4-28-admin-agents.md(the guided operator switch with I1),M8-03-import-server.md(states,409reasons, lead semantics). - Code on the base branch:
packages/admin_ui,packages/api_client,packages/l10n,apps/console,tools/console-e2e,infra/compose/test.
This ticket touches no API operation, no table, no action, no event and no server configuration key.
Fixed requirements that apply¶
Owner's fixed build rules (apply verbatim):
- No MVP. New from scratch and directly in the final state; stages are only the order.
- Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
- Documentation in code: every public module, class, function and interface with a docstring/doc comment (purpose, parameters, return value, errors, example where useful); comments explain the why, not the what. API documentation is generated from code (OpenAPI from the routes, dartdoc, Python API docs in MkDocs).
- Documentation in the repo: README per module/service, ADR for every architecture decision, CHANGELOG per release, keep the documentation site current. A change without matching documentation is not done.
- Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
Rules for every brief:
- Test first: write the failing test, run it and keep the red output for the report, then make it green, then clean up.
- No en dash and no em dash in any file you write; the
text-rulespre-commit hook rejects them, and the ARB test of M4-06 rejects them in interface texts. Use a hyphen or rephrase. - White label: examples and test data use
example.comand the people Alex, Sam and Robin; no company, host or personal names. Interface texts come frompackages/l10n. - No configuration key is added.
- Specification first: a change to behaviour updates
docs/spec/in the same change. The core contract is not edited by this ticket.
Product requirements implemented here:
- Every
import.managestep (create, start, pause, cancel, complete, lead change) andchannel.operator.setshows its confirmation with the plain-language effect and then the step-up (01 5.2; 05 4.12). - The page shows counters only, never message content, file names or names of Discord users (22 11, 14); channel names (Discord and Bothy) are structure and may be shown.
- "Bothy leads" names the order of 22 13.3 in its confirmation: Discord locked by the owner,
bothy import discord switch ... --discord-lockedsucceeded; then the lead, then the operator, which must be the planned operator (otherwise the confirmation warns that imported threads keep the planned operator, R-IM-08). - "Discord leads" sets the lead (which sets read-only and cancels active runs, M8-03) and then offers removing the operator (22 13.4 step 1).
- Completion is offered only when every imported channel has
lead = bothy; the server's409 conflictwithreasonlead_not_bothyis shown in plain language; the confirmation of completion warns that the mapping inimport_itemsstays until the retention classimportis deleted and that after that deletion a repeated import is no longer idempotent (22 13.5). - The console has no gateway connection (15 3.1 rule 3); the page refreshes through
getImportafter actions, on a refresh control and on visibility; no timer polling without a configuration key.
Scope¶
Create:
packages/admin_ui/lib/src/import/:ImportRepository(REST throughapi_client),ImportController(state fromImportSourcesnapshots, the same payload asimport.progress), screensImportListPage,ImportSourcePage,ImportCreateDialog(file picker, YAML to JSON with theyamlpackage already in the workspace or the one the mergedadmin_uiuses, size limited by the server's request limit, errors at pointers),ImportChannelTable, the confirmation dialogs of requirements 11, 13 to 15 (Design notes 1 to 4).apps/console/lib/admin/import/: the routeimportand its page frame hosting theadmin_uiscreens with the console's step-up adapter (M6-04 note 2).apps/console/integration_test/admin/import_test.dart:patrolweb flows (note 5).- Tests named under "Tests".
Change:
packages/admin_uinavigation model (entry "Import" for owners only),packages/l10nARB files (English and German texts of every new string),apps/consoleroutes and README,tools/console-e2eseed (note 5), rootCHANGELOG.mdunder[Unreleased];docs/spec/22-import-migration.md8 (the console page names the steps; no new rule) only if wording needs it;docs/DECISIONS.mdsection 10 rowsD-M8-08-1toD-M8-08-4.
Out of scope¶
- The import tool, its login and the server (M8-03, M8-04, M8-10).
- A desktop or mobile import screen (01 3.7: web only).
- Editing a stored mapping (there is no operation; a changed mapping means a new source before the first
--apply, 22 13.2). - Deleting
import_itemsby retention (M7). - Changes to the core contract.
Boundaries¶
- Work in a dedicated git worktree on the branch
feat/M8-08-import-console; one writer per worktree. - Conventional Commits with the package or component as scope (for example
feat(admin-ui): ...,feat(console): ...) and a DCO sign-off on every commit (git commit -s); squash into meaningful commits before the merge (docs/quality/push-discipline.md). - Never push to
main, never tag and never create remote repositories; the only remote steps are the branch push and the pull request ofAGENTS.md. - Do not modify files outside the scope above, except the CHANGELOG, README and
docs/DECISIONS.mdentries named in this brief. - Follow the host rules supplied by the launcher (ports, machine rules); the stack publishes its ports only on
127.0.0.1. - Never contact Discord or any other real service: the e2e seed writes import data through the API with an owner device session, never through SQL and never through a Discord call.
- Stop only processes and containers you started, by PID or container ID. Temporary directories only via
mktemp -d, removed at the end. - Never read or print secrets.
- Docker, the pinned Flutter and the pinned browsers must be available for
make console-e2e; the ticket must run it once on the build host or stop withVerdict: BLOCKED <tool> missing. - Never lower a threshold, disable a gate, delete a test or relax the CSP.
- No permission logic in the client (15 1 principle 1): the entry and the buttons follow the server's answers (
403hides nothing that the server did not refuse). - If the core contract or
docs/api/lacks something this ticket needs, stop withVerdict: BLOCKED <reason>instead of building a deviation (P-11). - This brief was written before milestone M1 closed (D-W-100, extended to M8 by D-W-124). Where the merged code of an earlier ticket names a package, type, function, file, provider or make target differently from this brief, follow the code and list each difference in the report; where it behaves differently from the specification, stop with
Verdict: BLOCKED <reason>.
Design notes¶
-
Model and refresh (15 3.1 rule 3; Ruling for docs/DECISIONS.md).
ImportControllerholds the latestImportSourceper ID and replaces it with every snapshot it receives, neweststatswins; a snapshot comes fromgetImport(after an action, the refresh control,visibilitychangeto visible) or, in a future client with a gateway, fromimport.progress, whose payload is the sameImportSource. No periodic timer. -
Create (22 5.2, 8). The dialog takes a name and a mapping file; the YAML is parsed strictly (duplicate keys are an error) and converted to JSON; nothing is stored in browser storage;
createImportwithkind = discordafter confirmation and step-up;400 validation_failederrors are listed with their pointer translated to the YAML path (/channels/2/planned_operatorbecomeschannels[2].planned_operator). After success the page shows the next steps: run a dry run, then start the source here, thenbothy import discord --apply. -
Channel table and lead actions (22 12.1, 13.3, 13.4; Ruling for docs/DECISIONS.md). Rows from the stored mapping's
channelswith a target other thanskip; Bothy channel name, read-only and operator fromgetChannelof the posted channel item's target (found throughlistImportItemswith kindchannel); not yet imported channels show "not imported" and no lead action (the server would answer409 conflictwithreasonchannel_not_imported). "Bothy leads": confirmation of requirement 13, step-up,setImportLeadbothy; then the operator dialog of M4-28 preselected with the planned operator (I1 check and guided switch as there); if the owner cancels it, the row shows "Bothy leads, no operator". "Discord leads": confirmation (read-only for everyone, active runs cancelled, messages written in Bothy stay in Bothy and are counted bybothy import discord revert), step-up,setImportLeaddiscord, then the offer to remove the operator. -
State actions (M8-03 note 3). Buttons follow the state:
draftstart, cancel;runningpause, cancel, complete;pausedstart, cancel, complete (complete only while every imported channel haslead = bothy, requirement 15;409 conflictwithreasonlead_not_bothyis still handled when the state changed after loading);completedandfailednone. Each with confirmation, step-up and the server's answer;409 conflictwithreasonimport_stateand409 conflictwithreasonlead_not_bothyin plain language. The step-up adapter of M6-04 repeats the request once with the sameIdempotency-Key. -
patrolweb tests (15 20; SEC-T05). The seed job ofconsole-e2ecreates, through the API with an owner device session, nothing import-specific; the test itself: (a) Robin opens Import, uploads a white-label mapping (two channels, one forum), sees a pointer error for a wrong handle, fixes it, creates the source; (b) starts it with the virtual authenticator; (c) the test's helper (an owner device session in the test runner, the waybothy importwould act) posts a structure batch for both channels and sets the cursor counters; (d) after refresh the counters and two rows "mirror, read-only" appear; (e) "Bothy leads" on the first channel with step-up, then the operator dialog sets Lotse; the row shows "Bothy leads, operator ok"; (f) "Discord leads" on it again; the row is read-only; (g) complete is refused while one channel is mirrored; after "Bothy leads" on both, the completion confirmation shows the retention warning of requirement 15 and complete succeeds; (h) pause and cancel of a second source, each with step-up. Every flow runs with the CSP violation collector of M6-03 active; any violation fails the suite. -
Rulings for
docs/DECISIONS.md(D-M8-08-<n>): refresh without polling (note 1); YAML conversion in the browser and pointer translation (note 2); the lead actions and the guided operator step (note 3); the state buttons (note 4).
Tests (write them first)¶
packages/admin_ui/test/import/: controller tests with sequences ofImportSourcesnapshots (state changes, counters, a snapshot with an olderstatsthan the shown one is still applied because the server is the source); widget and golden tests of the list, the source page, the channel table, the create dialog and every confirmation (light and dark, text scaling 1 and 2, de and en); a test that no message content, file name or Discord user name appears even whenstatscarries unknown members.apps/console/integration_test/admin/import_test.dart: the flows of Design note 5, red first.- The route test of M6-02 still finds no content route.
Commands that must pass, from the repository root:
cd packages/admin_ui && flutter analyze --fatal-infos && flutter test --coveragecd apps/console && flutter analyze --fatal-infos && flutter test --coveragemake console-e2eon the build hostmake arch,make lint,make fmt-check,make coveragemake check
Acceptance criteria¶
- Widget tests cover the snapshots of
import.progress(ImportSource) and every screen. -
patrolweb tests create, start, change leads, refuse and then perform completion, each with step-up, under the enforced CSP. - No content, file name or person name of the source appears in the console.
- ARB files, README, CHANGELOG and rulings are updated.
-
make checkis green.
Definition of done¶
- Code and tests as specified; doc comments on every public Dart symbol; comments explain why.
apps/console/README.mdand theadmin_uiREADME updated in English.- Rulings of Design note 6 recorded in
docs/DECISIONS.mdsection 10. - Root
CHANGELOG.mdentry under[Unreleased]. - An ADR only if you take an architecture decision beyond this brief (MADR 4 as in
docs/adr/0000-template-madr-4.md, next free number,make adr-index). make checkgreen.- An independent code review over the diff, then the red team gate (
docs/quality/red-team-gate.md, verdictJAorNEIN); onNEINfix with a test per finding and have the delta checked again. Record both verdicts. - Land the change as
AGENTS.md("How work lands onmain") describes: a pull request from this branch, squash-merged only when review, red team andmake check,ci-okand the review threads are green; thengit worktree remove.
Report¶
Write to {{REPORT_PATH}}:
- First line exactly
Verdict: DONEorVerdict: BLOCKED <reason>. - Commits (SHA and subject), changed files with line counts, test results with counts (the red run before the change and the green run after), the summary of
make checkand ofmake console-e2e. - Review and red team verdicts and the findings fixed.
- What is not proven.
- Follow-ups that could not be done in this scope, each with its reason, every difference to the names of this brief (D-W-100), and spec gaps found.
Then write {{DONE_PATH}} with exit=0 sha=<sha> or exit=1 <reason>.