#!/usr/bin/env python3 """Enforce Core ownership of repeated WebUI layout and dialog anatomy.""" from __future__ import annotations import pathlib import re import sys META_ROOT = pathlib.Path(__file__).resolve().parents[2] REPOS_ROOT = META_ROOT.parent CORE_INDEX = pathlib.Path("govoplan-core/webui/src/index.ts") DIALOG_WIDTH_EXCEPTIONS = META_ROOT / "tools/checks/shared-webui-dialog-width-exceptions.txt" RAW_ELEMENT = re.compile( r'<(?Pdiv|span|header|section|form)\b(?P[^>]*?)' r'\bclassName="(?P[^"]+)"', re.DOTALL, ) LEGACY_LAYOUT_TOKENS = { "form-grid", "admin-form-grid", "dashboard-grid", "settings-grid", "admin-dialog", "admin-dialog-wide", "admin-details-grid", "detail-list", "metric-grid", } CENTRAL_COMPONENTS = { "PageActionBar": pathlib.Path( "govoplan-core/webui/src/components/PageActionBar.tsx" ), "ActionToolbar": pathlib.Path( "govoplan-core/webui/src/components/ActionToolbar.tsx" ), "ToolbarGroup": pathlib.Path( "govoplan-core/webui/src/components/ActionToolbar.tsx" ), "ToolbarSpacer": pathlib.Path( "govoplan-core/webui/src/components/ActionToolbar.tsx" ), "ContentGrid": pathlib.Path( "govoplan-core/webui/src/components/ContentGrid.tsx" ), "ContentSection": pathlib.Path( "govoplan-core/webui/src/components/ContentSection.tsx" ), "FormGrid": pathlib.Path("govoplan-core/webui/src/components/ContentGrid.tsx"), "FormLayout": pathlib.Path( "govoplan-core/webui/src/components/ContentGrid.tsx" ), "GridItem": pathlib.Path("govoplan-core/webui/src/components/ContentGrid.tsx"), "FormSection": pathlib.Path( "govoplan-core/webui/src/components/FormSection.tsx" ), "DialogActions": pathlib.Path( "govoplan-core/webui/src/components/DialogAnatomy.tsx" ), "DialogForm": pathlib.Path( "govoplan-core/webui/src/components/DialogAnatomy.tsx" ), "DialogSection": pathlib.Path( "govoplan-core/webui/src/components/DialogAnatomy.tsx" ), "DescriptionList": pathlib.Path( "govoplan-core/webui/src/components/DescriptionList.tsx" ), "DescriptionItem": pathlib.Path( "govoplan-core/webui/src/components/DescriptionList.tsx" ), "MetricGrid": pathlib.Path( "govoplan-core/webui/src/components/MetricGrid.tsx" ), "MetricCard": pathlib.Path( "govoplan-core/webui/src/components/MetricCard.tsx" ), "FilterBar": pathlib.Path( "govoplan-core/webui/src/components/FilterBar.tsx" ), "StatePanel": pathlib.Path( "govoplan-core/webui/src/components/StatePanel.tsx" ), "CountBadge": pathlib.Path( "govoplan-core/webui/src/components/CountBadge.tsx" ), "MultiSelectFilter": pathlib.Path( "govoplan-core/webui/src/components/MultiSelectFilter.tsx" ), "ListSelectionFilter": pathlib.Path( "govoplan-core/webui/src/components/ListSelectionFilter.tsx" ), "SelectionList": pathlib.Path( "govoplan-core/webui/src/components/SelectionList.tsx" ), "SelectionListItem": pathlib.Path( "govoplan-core/webui/src/components/SelectionList.tsx" ), "SelectionListItemContent": pathlib.Path( "govoplan-core/webui/src/components/SelectionList.tsx" ), "WorkspaceLayout": pathlib.Path( "govoplan-core/webui/src/components/WorkspaceLayout.tsx" ), "WorkspaceFrame": pathlib.Path( "govoplan-core/webui/src/components/WorkspaceFrame.tsx" ), "DefinitionPalette": pathlib.Path( "govoplan-core/webui/src/components/DefinitionPalette.tsx" ), "DefinitionPaletteGroup": pathlib.Path( "govoplan-core/webui/src/components/DefinitionPalette.tsx" ), "DefinitionPaletteItem": pathlib.Path( "govoplan-core/webui/src/components/DefinitionPalette.tsx" ), "DefinitionNodeIcon": pathlib.Path( "govoplan-core/webui/src/components/DefinitionNodeIcon.tsx" ), "FloatingStatus": pathlib.Path( "govoplan-core/webui/src/components/FloatingStatus.tsx" ), } REQUIRED_CONSUMERS = { "PageActionBar": ( pathlib.Path("govoplan-payments/webui/src/features/payments/PaymentsPage.tsx"), ), "ActionToolbar": ( pathlib.Path("govoplan-core/webui/src/components/WysiwygEditor.tsx"), pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarPage.tsx"), pathlib.Path("govoplan-files/webui/src/features/files/FilesPage.tsx"), pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"), ), "ContentGrid": ( pathlib.Path("govoplan-core/webui/src/features/settings/SettingsPage.tsx"), pathlib.Path("govoplan-campaign/webui/src/features/campaigns/GlobalSettingsPage.tsx"), pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationSettingsPanel.tsx"), ), "ContentSection": ( pathlib.Path("govoplan-datasources/webui/src/features/datasources/DatasourcesPage.tsx"), pathlib.Path("govoplan-dist-lists/webui/src/features/distributionLists/DistributionListsPage.tsx"), pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"), ), "FormGrid": ( pathlib.Path("govoplan-core/webui/src/components/mail/MailServerSettingsPanel.tsx"), pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarEventDialog.tsx"), pathlib.Path("govoplan-forms/webui/src/features/forms/FormDefinitionDialog.tsx"), pathlib.Path("govoplan-postbox/webui/src/features/postbox/PostboxAdminPanel.tsx"), ), "FormSection": ( pathlib.Path("govoplan-addresses/webui/src/features/addressbook/AddressBookPage.tsx"), pathlib.Path("govoplan-quick-access/webui/src/features/settings/QuickAccessSettingsPanel.tsx"), ), "DialogForm": ( pathlib.Path("govoplan-addresses/webui/src/features/addressbook/AddressBookPage.tsx"), pathlib.Path("govoplan-calendar/webui/src/features/calendar/CalendarEventDialog.tsx"), pathlib.Path("govoplan-records/webui/src/features/records/RecordsPage.tsx"), ), "DialogSection": ( pathlib.Path("govoplan-datasources/webui/src/features/datasources/DatasourcesPage.tsx"), pathlib.Path("govoplan-files/webui/src/features/files/components/FileShareDialog.tsx"), pathlib.Path("govoplan-templates/webui/src/features/templates/TemplatesPage.tsx"), ), "DescriptionList": ( pathlib.Path("govoplan-access/webui/src/features/admin/UsersPanel.tsx"), pathlib.Path("govoplan-campaign/webui/src/features/campaigns/CampaignReportPage.tsx"), pathlib.Path("govoplan-docs/webui/src/features/docs/DocsPage.tsx"), pathlib.Path("govoplan-policy/webui/src/features/policy/ViewPoliciesPanel.tsx"), ), "MetricGrid": ( pathlib.Path("govoplan-core/webui/src/features/dashboard/DashboardPage.tsx"), pathlib.Path("govoplan-admin/webui/src/features/admin/ModuleManagementPanel.tsx"), pathlib.Path("govoplan-campaign/webui/src/features/operator/OperatorQueuePage.tsx"), pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationSummaryWidget.tsx"), ), "MetricCard": ( pathlib.Path("govoplan-admin/webui/src/features/admin/AdminOverviewPanel.tsx"), pathlib.Path("govoplan-approvals/webui/src/features/approvals/ApprovalsPage.tsx"), pathlib.Path("govoplan-campaign/webui/src/features/campaigns/ReviewSendPage.tsx"), pathlib.Path("govoplan-voting/webui/src/features/voting/VotingPage.tsx"), ), "FilterBar": ( pathlib.Path("govoplan-cases/webui/src/features/cases/CasesPage.tsx"), pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowPage.tsx"), pathlib.Path("govoplan-records/webui/src/features/records/RecordsPage.tsx"), pathlib.Path("govoplan-tasks/webui/src/features/tasks/TasksPage.tsx"), ), "StatePanel": ( pathlib.Path("govoplan-committee/webui/src/features/committee/CommitteePage.tsx"), pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationCenterPage.tsx"), pathlib.Path("govoplan-postbox/webui/src/features/postbox/PostboxPage.tsx"), pathlib.Path("govoplan-risk-compliance/webui/src/features/riskCompliance/RiskCompliancePage.tsx"), ), "CountBadge": ( pathlib.Path("govoplan-core/webui/src/layout/Titlebar.tsx"), pathlib.Path("govoplan-mail/webui/src/features/mail/MailboxPage.tsx"), ), # Search's old count badge was part of a retired module-local filter menu. # Both surfaces must now compose the owning facet adapter and Core dropdown. "SearchFilters": ( pathlib.Path("govoplan-search/webui/src/features/search/SearchPage.tsx"), pathlib.Path("govoplan-search/webui/src/components/GlobalSearch.tsx"), ), "MultiSelectFilter": ( pathlib.Path("govoplan-search/webui/src/components/SearchFilters.tsx"), pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationCenterPage.tsx"), pathlib.Path("govoplan-docs/webui/src/features/docs/DocsPage.tsx"), ), "ListSelectionFilter": ( pathlib.Path("govoplan-core/webui/src/components/MultiSelectFilter.tsx"), pathlib.Path("govoplan-core/webui/src/components/table/DataGrid.tsx"), ), "SelectionListItemContent": ( pathlib.Path("govoplan-approvals/webui/src/features/approvals/ApprovalsPage.tsx"), pathlib.Path("govoplan-datasources/webui/src/features/datasources/DatasourcesPage.tsx"), pathlib.Path("govoplan-voting/webui/src/features/voting/VotingPage.tsx"), ), "WorkspaceLayout": ( pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowPage.tsx"), pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationCenterPage.tsx"), pathlib.Path("govoplan-workflow/webui/src/features/workflow/WorkflowPage.tsx"), ), "WorkspaceFrame": ( pathlib.Path("govoplan-cases/webui/src/features/cases/CasesPage.tsx"), pathlib.Path("govoplan-portal/webui/src/features/portal/PortalPage.tsx"), pathlib.Path("govoplan-records/webui/src/features/records/RecordsPage.tsx"), ), "DefinitionPalette": ( pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowPage.tsx"), pathlib.Path("govoplan-workflow/webui/src/features/workflow/WorkflowPage.tsx"), ), "DefinitionNodeIcon": ( pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowNode.tsx"), pathlib.Path("govoplan-workflow/webui/src/features/workflow/WorkflowNode.tsx"), ), "FloatingStatus": ( pathlib.Path("govoplan-dataflow/webui/src/features/dataflow/DataflowPage.tsx"), pathlib.Path("govoplan-workflow/webui/src/features/workflow/WorkflowPage.tsx"), ), } LEGACY_CSS_SELECTOR = re.compile( r"(? list[pathlib.Path]: paths: list[pathlib.Path] = [] for repository in sorted(REPOS_ROOT.glob("govoplan*")): source_root = repository / "webui" / "src" if source_root.is_dir(): paths.extend(sorted(source_root.rglob("*.tsx"))) return paths def css_paths() -> list[pathlib.Path]: paths: list[pathlib.Path] = [] for repository in sorted(REPOS_ROOT.glob("govoplan*")): source_root = repository / "webui" / "src" if source_root.is_dir(): paths.extend(sorted(source_root.rglob("*.css"))) return paths def relative(path: pathlib.Path) -> pathlib.Path: return path.relative_to(REPOS_ROOT) def raw_reason(classes: str) -> str | None: tokens = classes.split() legacy = sorted(set(tokens) & LEGACY_LAYOUT_TOKENS) if legacy: return f"legacy shared layout class {', '.join(legacy)}" toolbars = [ token for token in tokens if token == "admin-toolbar-row" or token.endswith("-toolbar") ] if toolbars: return f"raw toolbar class {', '.join(toolbars)}" dialog_forms = [token for token in tokens if token.endswith("dialog-form")] if dialog_forms: return f"raw dialog form class {', '.join(dialog_forms)}" return None def normalized_css_selector(selector: str) -> str: return " ".join(selector.split()) def dialog_width_exceptions(styles: dict[pathlib.Path, str]) -> dict[str, str]: exceptions: dict[str, str] = {} central_dialog_styles = pathlib.Path("govoplan-core/webui/src/styles/dialogs.css") for path, content in styles.items(): if path == central_dialog_styles: continue without_comments = CSS_COMMENT.sub("", content) for match in CSS_BLOCK.finditer(without_comments): selector = normalized_css_selector(match.group(1)) declarations = match.group(2) if not DIALOG_WIDTH.search(declarations): continue dialog_classes = DIALOG_CLASS.findall(selector) if not dialog_classes: continue if all(name.lower().endswith(DIALOG_INTERNAL_SUFFIXES) for name in dialog_classes): continue signature = f"{path}|{selector}" exceptions[signature] = declarations return exceptions def exception_baseline() -> set[str]: if not DIALOG_WIDTH_EXCEPTIONS.exists(): return set() return { line.strip() for line in DIALOG_WIDTH_EXCEPTIONS.read_text(encoding="utf-8").splitlines() if line.strip() and not line.lstrip().startswith("#") } def main() -> int: sources = source_paths() source_text = {relative(path): path.read_text(encoding="utf-8") for path in sources} styles = css_paths() style_text = {relative(path): path.read_text(encoding="utf-8") for path in styles} errors: list[str] = [] for path, content in source_text.items(): for match in RAW_ELEMENT.finditer(content): reason = raw_reason(match.group("classes")) if reason is None: continue line = content.count("\n", 0, match.start()) + 1 errors.append( f"Raw {match.group('tag')} repeats shared anatomy ({reason}): {path}:{line}" ) for path, content in style_text.items(): uncommented = CSS_COMMENT.sub("", content) legacy_match = LEGACY_CSS_SELECTOR.search(uncommented) if legacy_match: line = content.count("\n", 0, legacy_match.start()) + 1 errors.append(f"Legacy shared layout selector is not allowed: {path}:{line}") for class_name in sorted(RETIRED_LOCAL_CSS_CLASSES): retired = re.search(rf"(?