-- 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 use "./i18n.allium" as i18n use "./generation.allium" as generation -- ─── Dashboard (no tab active) ─────────────────────────────── -- Shown as default/welcome view when no entity tab is active. value Dashboard { title: String subtitle: String stats: DashboardStats timeline: DashboardTimeline tag_cloud: DashboardTagCloud category_cloud: DashboardCategoryCloud recent_posts: List } value DashboardStats { 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 { months: List } value DashboardTimelineMonth { label: String -- month abbreviation year: Integer count: Integer } value DashboardTagCloud { tags: List overflow_count: Integer? -- "and N more" when > config.dashboard_max_tags } value DashboardTag { name: String count: Integer color: String? } value DashboardCategoryCloud { categories: List } value DashboardCategory { name: String count: Integer } value DashboardRecentPost { post_id: String title: String -- "Untitled" as fallback status: String -- draft | published date: String -- locale-formatted } 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 } surface DashboardSurface { context dash: Dashboard exposes: dash.title dash.subtitle dash.stats.total_posts dash.stats.published_count dash.stats.draft_count dash.stats.archived_count when dash.stats.archived_count > 0 dash.stats.media_count dash.stats.image_count dash.stats.total_media_size dash.stats.tag_count dash.stats.category_count for m in dash.timeline.months: m.label m.year m.count for t in dash.tag_cloud.tags: t.name t.count t.color dash.tag_cloud.overflow_count when dash.tag_cloud.overflow_count != null for c in dash.category_cloud.categories: c.name c.count for rp in dash.recent_posts: rp.title rp.status rp.date provides: DashboardRecentPostClicked(post_id, single) DashboardRecentPostClicked(post_id, double) @guarantee StatCards -- Three stat cards side by side. -- Posts card: total number, breakdown tags (published/drafts/archived if > 0). -- Media card: count, images count, total size (formatted bytes). -- Tags card: count, categories count. @guarantee TimelineChart -- Bar chart of posts over last config.dashboard_timeline_months months that have data. -- Each bar: count label on top, month abbreviation + year below. -- Bar height proportional to max count. @guarantee TagCloud -- Up to config.dashboard_max_tags tags, sorted alphabetically. -- Font size scaled config.dashboard_tag_min_font to config.dashboard_tag_max_font px -- based on post count. -- Tags with colours get coloured background with contrast text. -- Hover title shows post count. -- "and N more" text when overflow_count > 0. @guarantee CategoryCloud -- All categories as badge-like tags. -- Each shows category name + count. @guarantee RecentPosts -- Last config.dashboard_recent_count posts by updatedAt descending. -- Each row: title (or "Untitled"), status badge (draft/published), date. -- Single-click: preview tab. Double-click: pin tab. } -- ─── Menu editor view ──────────────────────────────────────── -- Visual editor for the OPML navigation menu (meta/menu.opml). -- See menu.allium for data model. value MenuEditorView { items: List } value MenuTreeItem { item_id: String kind: String -- home | page | category_archive | submenu label: String children: List is_home: Boolean -- true for the home item (protected) } surface MenuEditorSurface { context menu: MenuEditorView exposes: for item in menu.items: item.kind item.label item.children item.is_home provides: MenuItemAdded(kind, data) MenuSaveRequested() MenuItemDeleted(item_id) when not item.is_home MenuItemMoved(item_id, direction) when not item.is_home @guarantee HeaderLayout -- Title + description text. @guarantee Toolbar -- 8 icon buttons with tooltips: -- Add Entry (+), Save (floppy), Add Category Archive, -- Move Up, Move Down, Indent, Unindent, Delete. @guarantee TreeView -- Drag-and-drop tree with items showing: -- Drag handle, kind icon (home/page/category-archive/submenu SVGs), -- title, selected row highlighting. -- Nested items indented to show hierarchy. @guarantee InlineEditing -- When creating page item: inline PageInput with search -- (filters posts in "page" category). -- When creating category archive: inline CategoryInput with search/create. -- Escape cancels, selection confirms. @guarantee HomeItemProtection -- Home item cannot be moved, reordered, or deleted. -- Maximum one home item allowed. @guarantee DragDrop -- Drag handle on each item. -- Auto-expand collapsed submenus on hover (config.menu_drag_expand_delay ms delay). -- Drop indicators show target position and nesting level. @guarantee MoveDirections -- 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). } config { menu_drag_expand_delay: Integer = 450 } 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 requires: not is_home_item(item_id) 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() } -- ─── Metadata diff view ────────────────────────────────────── -- Shows DB vs filesystem differences for all entity types. -- See metadata_diff.allium for diff field definitions. value MetadataDiffView { is_scanning: Boolean active_entity_tab: String -- posts | media | scripts | templates diff_stats: MetadataDiffStats field_summaries: List items: List orphan_files: List } value MetadataDiffStats { total_posts: Integer published_posts: Integer draft_posts: Integer media_files: Integer scripts: Integer templates: Integer } value MetadataDiffFieldSummary { field_name: String diff_count: Integer } value MetadataDiffItem { entity_name: String entity_type: String file_missing: Boolean -- badge shown when file not found field_diffs: List } value MetadataDiffField { field_name: String db_value: String? file_value: String? } value MetadataDiffOrphanFile { file_path: String entity_type: String } surface MetadataDiffSurface { context diff: MetadataDiffView exposes: diff.is_scanning diff.active_entity_tab diff.diff_stats for fs in diff.field_summaries: fs.field_name fs.diff_count for item in diff.items: item.entity_name item.file_missing for fd in item.field_diffs: fd.field_name fd.db_value fd.file_value for orphan in diff.orphan_files: orphan.file_path provides: MetadataDiffScanRequested() MetadataDiffSyncFieldToFile(entity_name, field_name) MetadataDiffSyncFieldToDb(entity_name, field_name) MetadataDiffSyncAllFieldToFile(field_name) MetadataDiffSyncAllFieldToDb(field_name) MetadataDiffImportOrphan(file_path) @guarantee HeaderLayout -- Title + description text. -- Stats row: 6 stat items (total posts, published, drafts, media, scripts, templates). @guarantee ScanAction -- Scan/Rescan button at top. -- Progress bar + message during scan. @guarantee EntityTabs -- Tabs: Posts, Media, Scripts, Templates — each with badge count of diffs. @guarantee FieldSummaryPills -- Clickable filter pills per field, each with count. -- Two bulk sync buttons per pill: DB→File and File→DB (syncs all items for that field). @guarantee DiffItemCards -- Per-item card: header (entity label + file-missing badge), -- field rows (field name, DB value, file value), -- two sync buttons per field (DB→File, File→DB). -- Button disappears after successful sync (field now matches). @guarantee OrphanFilesSection -- Files on disk with no matching DB record shown at bottom of each entity tab. -- "Import" button creates DB record from file metadata. @guarantee NoConfirmation -- Individual field syncs require no confirmation dialog. } -- ─── 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}" value GitDiffView { diff_id: String diff_type: String -- file | commit display_mode: String -- inline | side_by_side (from editor settings) } surface GitDiffSurface { context diff: GitDiffView exposes: diff.diff_id diff.diff_type diff.display_mode @guarantee DiffDisplayModes -- Supports inline and side-by-side diff display modes. -- Mode comes from editor settings (Diff View Style). @guarantee ReadOnly -- 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 surface DocumentationSurface { @guarantee MarkdownRendering -- Renders markdown file as styled HTML. -- No edit actions. Read-only view. } -- ─── Site validation view ─────────────────────────────────── value SiteValidationReport { project_id: String 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 affected_sections: Set } surface SiteValidationSurface { context report: SiteValidationReport exposes: report.project_id report.expected_url_count report.existing_html_count report.missing_url_paths report.extra_url_paths report.updated_post_url_paths report.affected_sections provides: SiteValidationScanRequested() SiteValidationApplyRequested(report) when report.missing_url_paths.count > 0 or report.extra_url_paths.count > 0 or report.updated_post_url_paths.count > 0 @guarantee SummaryLine -- "Expected URLs: N — Existing HTML URLs: N — Missing: N — Extra: N — Updated: N" @guarantee UrlSections -- Three sections: Missing URLs, Extra URLs, Updated URLs. -- Each section: heading + list of URL paths, or "None found". @guarantee ApplyAction -- Apply button disabled when nothing to fix. -- On apply: renders missing, deletes extra, re-renders updated. -- Toast: "Validation applied: N rendered, N deleted". } 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 ensures: ApplyValidationRequested(report.project_id, report.affected_sections) } -- ─── Translation validation view ─────────────────────────── 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? } surface TranslationValidationSurface { context report: TranslationValidationReport exposes: report.checked_database_row_count report.checked_filesystem_file_count for issue in report.invalid_database_rows: issue.issue issue.translation_id issue.translation_for issue.canonical_language issue.translation_language issue.title issue.file_path for issue in report.invalid_filesystem_files: issue.issue issue.file_path provides: TranslationValidationScanRequested() TranslationValidationFixRequested(report) when report.invalid_database_rows.count > 0 or report.invalid_filesystem_files.count > 0 @guarantee SummaryLine -- "Checked DB rows: N — Checked files: N — Invalid DB rows: N — Invalid files: N" @guarantee IssueSections -- Two sections: Database Issues, Filesystem Issues. -- Each issue rendered as card with coloured left border. -- Card shows: issue label, source post ID, translation ID, title, languages, file path. @guarantee IssueTypes -- same_language_as_canonical: translation language matches source. -- do_not_translate_has_translations: source is doNotTranslate. -- content_in_database: published translation has content in DB. -- missing_source_post: translationFor references nonexistent post. @guarantee FixAction -- Revalidate button + Fix button (disabled when no issues). -- Fix: content_in_database -> flush to .md file, set content null. -- Other issues -> delete DB row or .md file. -- After fix: automatically re-validates. -- Toast: "Deleted N DB rows and N files, flushed N translations to disk". } 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 ensures: TranslationValidationScan } -- ─── Find duplicates view ────────────────────────────────── value DuplicateSearchResult { pairs: List has_more: Boolean -- pagination with config.duplicate_page_size per page } 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 } config { duplicate_similarity_threshold: Decimal = 0.92 duplicate_page_size: Integer = 500 duplicate_neighbor_count: Integer = 21 } surface DuplicatesSurface { context result: DuplicateSearchResult exposes: for pair in result.pairs: pair.title_a pair.title_b pair.similarity pair.exact_match result.has_more provides: DuplicateSearchRequested() DuplicatePairDismissed(post_id_a, post_id_b) DuplicatePairsBatchDismissed(pair_ids) DuplicatePostClicked(post_id) DuplicateShowMoreRequested() when result.has_more @guarantee SemanticSimilarityGate -- Requires semanticSimilarityEnabled in project metadata. -- If disabled: shows "Semantic similarity is not enabled" message. -- No search functionality available when disabled. @guarantee ActionsBar -- Refresh button, Check All, Uncheck All, -- Dismiss Checked (with count), disabled when none checked. @guarantee PairRows -- Each row: checkbox, post A title (clickable -> opens tab), -- arrow, post B title (clickable -> opens tab), -- similarity badge (percentage or "Exact Match"), -- Dismiss button. -- Exact matches styled distinctly from similarity matches. @guarantee Pagination -- "Show More" button when has_more is true. -- config.duplicate_page_size pairs per page. @guarantee BatchDismiss -- Batch insert in chunks of 100. -- Dismissed pairs excluded from future searches. } rule DuplicateSearch { when: DuplicateSearchRequested() requires: semantic_similarity_enabled -- Loads USearch vector index for project -- For each indexed post: search config.duplicate_neighbor_count nearest neighbors -- similarity = max(0, 1 - distance) -- Filter: similarity >= config.duplicate_similarity_threshold, exclude dismissed -- 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 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) } -- ─── Import analysis view ─────────────────────────────────── -- Editor for WXR (WordPress eXtended RSS) import definitions. -- Keyed by import definition ID. Opened as always-pinned tab. value ImportAnalysisView { definition_id: String definition_name: String -- editable name input uploads_folder_path: String? -- path display + Browse button wxr_file_path: String? -- path display + Select & Analyze button is_loading: Boolean report: ImportAnalysisReport? } value ImportAnalysisReport { site_info: ImportSiteInfo post_stats: ImportEntityStats page_stats: ImportEntityStats media_stats: ImportMediaStats category_stats: ImportTaxonomyStats tag_stats: ImportTaxonomyStats date_distribution: List conflicts: List macros: List } value ImportSiteInfo { title: String url: String language: String source_file: String } value ImportEntityStats { new_count: Integer update_count: Integer conflict_count: Integer duplicate_count: Integer } value ImportMediaStats { new_count: Integer update_count: Integer conflict_count: Integer duplicate_count: Integer missing_count: Integer } value ImportTaxonomyStats { existing_count: Integer mapped_count: Integer new_count: Integer } value ImportYearDistribution { year: Integer post_count: Integer media_count: Integer } value ImportConflict { item_type: String -- post | page | media item_name: String resolution: String -- ignore | overwrite | import } value ImportMacro { name: String usage_count: Integer parameters: List validation_status: String -- valid | invalid | unknown } surface ImportAnalysisSurface { context analysis: ImportAnalysisView exposes: analysis.definition_name analysis.uploads_folder_path analysis.wxr_file_path analysis.is_loading analysis.report when analysis.report != null provides: ImportAnalyzeRequested(analysis.definition_id, file_path) when analysis.wxr_file_path != null ImportExecuteRequested(analysis.definition_id) when analysis.report != null ImportConflictResolutionChanged(item_name, resolution) ImportTaxonomyMappingChanged(source_term, target_term) ImportAITaxonomyAnalysisRequested(analysis.definition_id) @guarantee FileSelectors -- Uploads folder: path display + Browse button (native folder dialog). -- WXR file: path display + Select & Analyze button (native file dialog). @guarantee LoadingState -- Spinner + progress step + detail text during analysis. @guarantee SiteInfoCard -- Shows: site title, URL, language, source file path. @guarantee StatCards -- Posts (new/update/conflict/duplicate), Pages (same), -- Media (new/update/conflict/duplicate/missing), -- Categories (existing/mapped/new), Tags (existing/mapped/new). @guarantee DateDistribution -- Year-by-year bar charts for posts + media. @guarantee ConflictsSection -- Collapsible. Per-item dropdown: Ignore/Overwrite/Import. -- Default: Import for new items, Ignore for existing matches. @guarantee TaxonomySection -- Collapsible. Category + tag pills. -- Click pill to map to existing term. Inline edit with suggestion dropdown. -- AI analyze button (with model selector dropdown). -- Gate: airplane mode check for AI taxonomy analysis. @guarantee MacrosSection -- Collapsible. Discovered macros with usage counts, parameters, -- validation status (valid/invalid/unknown badges). @guarantee ExecuteAction -- Execute button shows importable counts (tags, posts, media, pages). -- Disabled if nothing to import. -- Progress bar during execution (current/total, phase, detail, ETA). -- No confirmation dialog — executes immediately. @guarantee CreatedEntitiesRefresh -- Created entities appear in sidebar immediately after execution. } 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 } 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) }