-- allium: 1 -- bDS Miscellaneous Editor Views -- Scope: UI content area — dashboard, menu editor, metadata diff, git diff, -- documentation, validation, find duplicates, import analysis -- Distilled from: Editor.tsx (Dashboard), MenuEditorView.tsx, -- MetadataDiffPanel.tsx, ImportAnalysisView.tsx -- Describes the layout and behaviour of smaller editor views that don't -- warrant their own spec file. use "./tabs.allium" as tabs -- ─── External surfaces ────────────────────────────────────── surface User { provides: ImportAnalyzeRequested(definition_id, file_path) provides: ImportExecuteRequested(definition_id) provides: SiteValidationScanRequested() provides: SiteValidationApplyRequested(report) provides: TranslationValidationScanRequested() provides: TranslationValidationFixRequested(report) provides: DuplicateSearchRequested() provides: DuplicatePairDismissed(post_id_a, post_id_b) provides: DuplicatePairsBatchDismissed(pair_ids) provides: MenuSaveRequested() provides: MenuItemAdded(kind, data) provides: MenuItemDeleted(item_id) provides: MenuItemMoved(item_id, direction) } -- ─── Dashboard (no tab active) ─────────────────────────────── -- Shown as default/welcome view when no entity tab is active. -- Single centered column. value Dashboard { title: String subtitle: String stats: DashboardStats timeline: DashboardTimeline tag_cloud: DashboardTagCloud category_cloud: DashboardCategoryCloud recent_posts: List } value DashboardStats { -- Three stat cards side by side: total_posts: Integer published_count: Integer draft_count: Integer archived_count: Integer -- shown only if > 0 media_count: Integer image_count: Integer total_media_size: String -- formatted B/KB/MB/GB tag_count: Integer category_count: Integer } value DashboardTimeline { -- Bar chart of posts over time -- Last 12 months that have data -- Each bar: count label on top, month abbreviation + year below -- Bar height proportional to max count months: List } value DashboardTimelineMonth { label: String -- month abbreviation year: Integer count: Integer } value DashboardTagCloud { -- Up to 40 tags, sorted alphabetically -- Font size scaled 11px-22px based on post count -- Tags with colours get coloured background with contrast text -- Hover title shows post count -- "and N more" text if > 40 tags tags: List overflow_count: Integer? } value DashboardTag { name: String count: Integer color: String? } value DashboardCategoryCloud { -- All categories as badge-like tags -- Each shows category name + count categories: List } value DashboardCategory { name: String count: Integer } value DashboardRecentPost { -- Last 5 posts by updatedAt descending -- Each row: title (or "Untitled"), status badge (draft/published), date -- Single-click: preview tab, double-click: pin tab post_id: String title: String status: String date: String } config { dashboard_max_tags: Integer = 40 dashboard_tag_min_font: Integer = 11 dashboard_tag_max_font: Integer = 22 dashboard_recent_count: Integer = 5 dashboard_timeline_months: Integer = 12 } -- ─── Menu editor view ──────────────────────────────────────── -- Visual editor for the OPML navigation menu (meta/menu.opml). -- See menu.allium for data model. -- Layout: toolbar at top, tree view of menu items below. -- Each item row: drag handle, icon, label, type badge, action buttons. -- Nested items indented to show hierarchy. -- ─── Menu editor actions ──────────────────────────────────── rule MenuAddItem { when: MenuItemAdded(kind, data) -- kind = page: opens lazy-loaded page picker (posts with "page" category) -- kind = category_archive: opens lazy-loaded category picker -- kind = submenu: creates empty container node for nesting children -- kind = home: always available, maximum one allowed ensures: MenuTreeUpdated() } rule MenuSave { when: MenuSaveRequested() -- Serializes tree to OPML 2.0, writes meta/menu.opml ensures: MenuFileWritten() } rule MenuMoveItem { when: MenuItemMoved(item_id, direction) -- direction = up | down | indent | unindent -- Up/Down: reorder within same nesting level -- Indent: nest under previous sibling (becomes child) -- Unindent: move to parent's level (becomes next sibling of parent) requires: not is_home_item(item_id) -- Home item is protected: cannot be moved or deleted ensures: MenuTreeUpdated() } rule MenuDeleteItem { when: MenuItemDeleted(item_id) requires: not is_home_item(item_id) -- Removes item and all children, no confirmation dialog ensures: MenuTreeUpdated() } -- Drag-and-drop reorder: -- Drag handle on each item -- Auto-expand collapsed submenus on hover (450ms delay) -- Drop indicators show target position and nesting level config { menu_drag_expand_delay: Integer = 450 } -- ─── Metadata diff view ────────────────────────────────────── -- Shows DB vs filesystem differences for all entity types. -- See metadata_diff.allium for diff field definitions. -- Layout: scan button at top, results grouped by entity type below. -- Each diff entry: entity name, field name, DB value, file value, -- two sync buttons (DB->File, File->DB). -- ─── Metadata diff actions ────────────────────────────────── -- Scan: runs 4 parallel scans (posts, media, scripts, templates) -- Compares DB records against filesystem files field by field -- Results grouped by entity type, sorted by entity name -- Shows count of differences per entity type in section header -- Per-field sync: -- DB -> File button: overwrites file field with DB value -- File -> DB button: overwrites DB field with file value -- No confirmation dialog for individual field syncs -- Button disappears after successful sync (field now matches) -- Orphan file import: -- Files on disk with no matching DB record shown separately -- "Import" action creates DB record from file metadata -- Appears at bottom of each entity type section -- ─── Git diff view ──────────────────────────────────────────── -- Renders diff for a file (working tree vs HEAD) or a commit. -- File diff: id = "git-diff:{filePath}" -- Commit diff: id = "git-diff:commit:{commitHash}" -- Supports inline and side-by-side diff display modes (from editor settings). -- No actions beyond viewing — changes are managed via git sidebar. -- ─── Documentation views ───────────────────────────────────── -- documentation: renders DOCUMENTATION.md as styled HTML -- api_documentation: renders API.md as styled HTML -- ─── Site validation view ─────────────────────────────────── -- Compares sitemap.xml to generated HTML files on disk. -- Detects missing pages, orphan HTML, and stale content. value SiteValidationReport { expected_url_count: Integer existing_html_count: Integer missing_url_paths: List -- in sitemap, no HTML on disk extra_url_paths: List -- HTML on disk, not in sitemap updated_post_url_paths: List -- source .md newer than HTML } -- Layout: scan button, summary line, three URL list sections. -- Summary: "Expected URLs: N — Existing HTML URLs: N — Missing: N — Extra: N — Updated: N" -- Each section: heading, list of URL paths, or "None found". rule SiteValidationScan { when: SiteValidationScanRequested() -- Parses entries from sitemap.xml into expected URL set -- Scans HTML output dir for index.html files (zero-byte = missing) -- Compares source .md mtime against generated HTML mtime ensures: SiteValidationReport } rule SiteValidationApply { when: SiteValidationApplyRequested(report) -- Classifies affected paths into generation sections (core, single, category, tag, date) -- Renders only affected sections in parallel -- Deletes extra HTML files, removes empty directories -- Regenerates calendar if anything changed -- Rebuilds search index if anything rendered or deleted -- Toast: "Validation applied: N rendered, N deleted" ensures: GenerateSiteRequested(sections: report.affected_sections) } -- ─── Translation validation view ─────────────────────────── -- Checks translation integrity across DB rows and filesystem files. -- NOT a matrix view — it is a list of issues found. value TranslationValidationReport { checked_database_row_count: Integer checked_filesystem_file_count: Integer invalid_database_rows: List invalid_filesystem_files: List } value TranslationValidationIssue { issue: String -- Issue kinds: -- missing_source_post: translationFor points to nonexistent post -- same_language_as_canonical: translation language matches source post language -- do_not_translate_has_translations: source post is doNotTranslate but has translations -- content_in_database: published translation still has content in DB (should be on disk) translation_id: String? translation_for: String canonical_language: String? translation_language: String title: String? file_path: String? } -- Layout: Revalidate + Fix Issues buttons, summary line, two issue sections. -- Summary: "Checked DB rows: N — Checked files: N — Invalid DB rows: N — Invalid files: N" -- Each issue rendered as card with coloured left border. -- Card shows: issue label, source post ID, translation ID, title, languages, file path. rule TranslationValidationScan { when: TranslationValidationScanRequested() -- Database pass: checks all translation rows for integrity issues -- Filesystem pass: scans posts/ for translation .md files, checks frontmatter ensures: TranslationValidationReport } rule TranslationValidationFix { when: TranslationValidationFixRequested(report) -- content_in_database: flushes content to .md file, sets content = null in DB -- missing_source_post | same_language_as_canonical | do_not_translate: -- DB issues: deletes translation row -- Filesystem issues: deletes the .md file -- After fix: automatically re-validates -- Toast: "Deleted N DB rows and N files, flushed N translations to disk" ensures: TranslationValidationScan } -- ─── Find duplicates view ────────────────────────────────── -- Uses embedding vectors to find semantically similar posts. -- Gate: requires semanticSimilarityEnabled in project metadata. -- If disabled: shows "Semantic similarity is not enabled" message. value DuplicateSearchResult { pairs: List } value DuplicatePair { post_id_a: String title_a: String post_id_b: String title_b: String similarity: Decimal -- 0.0 to 1.0 exact_match: Boolean -- true if titles + content identical } -- Layout: header, count, paginated pair list (500 per page). -- Each pair row: checkbox, post A title (clickable), arrow, post B title (clickable), -- similarity badge ("Exact duplicate" or "N% similar"), Dismiss button. -- Exact matches styled distinctly. Batch controls: Check All, Uncheck All, -- Dismiss Checked (N). Refresh button. rule DuplicateSearch { when: DuplicateSearchRequested() requires: semantic_similarity_enabled -- Loads USearch vector index for project -- For each indexed post: search 21 nearest neighbors -- similarity = max(0, 1 - distance) -- Filter: similarity >= threshold, exclude dismissed pairs -- For 100% embedding similarity: load post bodies, compare title+content -- If identical: exact_match = true -- Sort: exact matches first, then descending similarity ensures: DuplicateSearchResult } rule DuplicateDismiss { when: DuplicatePairDismissed(post_id_a, post_id_b) -- Inserts into dismissed_duplicate_pairs with canonical ID ordering -- Excluded from future searches -- See schema.allium DismissedDuplicatePair ensures: PairDismissed(post_id_a, post_id_b) } rule DuplicateBatchDismiss { when: DuplicatePairsBatchDismissed(pair_ids) -- Batch insert in chunks of 100 ensures: for pair in pair_ids: PairDismissed(pair) } config { duplicate_similarity_threshold: Decimal = 0.92 duplicate_page_size: Integer = 500 duplicate_neighbor_count: Integer = 21 } -- ─── Import analysis view ─────────────────────────────────── -- Editor for WXR (WordPress eXtended RSS) import definitions. -- Keyed by import definition ID. Opened as always-pinned tab. -- Workflow: Select File -> Analyze -> Resolve Conflicts -> Execute -- Layout: header (definition name), file selector, analysis results, -- conflict resolution panel, taxonomy mapping, execute button. -- ─── Import analysis actions ──────────────────────────────── rule ImportSelectAndAnalyze { when: ImportAnalyzeRequested(definition_id, file_path) -- Parses WXR XML file -- Extracts: posts, pages, media, tags, categories, authors -- Shows summary counts per entity type -- Identifies conflicts: duplicate slugs, existing categories/tags } -- Conflict resolution: -- Per-item dropdown: Import (create new), Skip (ignore), Merge (combine) -- Default: Import for new items, Skip for existing matches -- Taxonomy mapping: -- Source categories/tags mapped to existing project categories/tags -- Dropdown per source taxonomy term -> target term or "Create New" -- AI taxonomy analysis (optional): -- Gate: airplane mode check -- Suggests mappings based on name similarity via title model rule ImportExecute { when: ImportExecuteRequested(definition_id) -- No confirmation dialog — executes immediately -- Processes items per conflict resolution settings -- Creates posts, media, tags, categories as needed -- Summary with counts shown in import view on completion -- Created entities appear in sidebar immediately (store updated) }