Implemented automated architecture validation to enforce design decisions: Architecture Validation System: - Created .architecture-rules.yaml with comprehensive rule definitions - Implemented validate_architecture.py script with AST-based validation - Added pre-commit hook configuration for automatic validation - Comprehensive documentation in docs/architecture/architecture-patterns.md Key Design Rules Enforced: - API-001 to API-004: API endpoint patterns (Pydantic models, no business logic, exception handling, auth) - SVC-001 to SVC-004: Service layer patterns (domain exceptions, db session params, no HTTP concerns) - MDL-001 to MDL-002: Model separation (SQLAlchemy vs Pydantic) - EXC-001 to EXC-002: Exception handling (custom exceptions, no bare except) - JS-001 to JS-003: JavaScript patterns (apiClient, logger, Alpine components) - TPL-001: Template patterns (extend base.html) Features: - Validates separation of concerns (routes vs services vs models) - Enforces proper exception handling (domain exceptions in services, HTTP in routes) - Checks database session patterns and Pydantic model usage - JavaScript and template validation - Detailed error reporting with suggestions - Integration with pre-commit hooks and CI/CD UI Fix: - Fixed icon names in content-pages.html (pencil→edit, trash→delete) Documentation: - Added architecture patterns guide with examples - Created scripts/README.md for validator usage - Updated mkdocs.yml with architecture documentation - Built and verified documentation successfully Usage: python scripts/validate_architecture.py # Validate all python scripts/validate_architecture.py --verbose # With details python scripts/validate_architecture.py --errors-only # Errors only 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
201 lines
10 KiB
HTML
201 lines
10 KiB
HTML
{# app/templates/admin/content-pages.html #}
|
|
{% extends "admin/base.html" %}
|
|
|
|
{% block title %}Content Pages{% endblock %}
|
|
|
|
{% block alpine_data %}contentPagesManager(){% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- Page Header -->
|
|
<div class="flex items-center justify-between my-6">
|
|
<div>
|
|
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
|
Content Pages
|
|
</h2>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
|
Manage platform defaults and vendor-specific content pages
|
|
</p>
|
|
</div>
|
|
<a
|
|
href="/admin/content-pages/create"
|
|
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple"
|
|
>
|
|
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
|
|
Create Page
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div x-show="loading" class="text-center py-12">
|
|
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
|
|
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading pages...</p>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div x-show="error && !loading" class="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg flex items-start">
|
|
<span x-html="$icon('exclamation', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
|
|
<div>
|
|
<p class="font-semibold">Error loading pages</p>
|
|
<p class="text-sm" x-text="error"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs and Filters -->
|
|
<div x-show="!loading" class="mb-6 bg-white dark:bg-gray-800 rounded-lg shadow-md p-4">
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<!-- Tabs -->
|
|
<div class="flex space-x-2 border-b border-gray-200 dark:border-gray-700">
|
|
<button
|
|
@click="activeTab = 'all'"
|
|
class="px-4 py-2 text-sm font-medium transition-colors"
|
|
:class="activeTab === 'all' ? 'text-purple-600 border-b-2 border-purple-600' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'"
|
|
>
|
|
All Pages
|
|
<span class="ml-2 px-2 py-0.5 text-xs rounded-full bg-gray-100 dark:bg-gray-700" x-text="allPages.length"></span>
|
|
</button>
|
|
<button
|
|
@click="activeTab = 'platform'"
|
|
class="px-4 py-2 text-sm font-medium transition-colors"
|
|
:class="activeTab === 'platform' ? 'text-purple-600 border-b-2 border-purple-600' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'"
|
|
>
|
|
Platform Defaults
|
|
<span class="ml-2 px-2 py-0.5 text-xs rounded-full bg-gray-100 dark:bg-gray-700" x-text="platformPages.length"></span>
|
|
</button>
|
|
<button
|
|
@click="activeTab = 'vendor'"
|
|
class="px-4 py-2 text-sm font-medium transition-colors"
|
|
:class="activeTab === 'vendor' ? 'text-purple-600 border-b-2 border-purple-600' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'"
|
|
>
|
|
Vendor Overrides
|
|
<span class="ml-2 px-2 py-0.5 text-xs rounded-full bg-gray-100 dark:bg-gray-700" x-text="vendorPages.length"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Search -->
|
|
<div class="relative">
|
|
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
|
|
<span x-html="$icon('search', 'w-5 h-5 text-gray-400')"></span>
|
|
</span>
|
|
<input
|
|
type="text"
|
|
x-model="searchQuery"
|
|
placeholder="Search pages..."
|
|
class="pl-10 pr-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-purple-500"
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pages Table -->
|
|
<div x-show="!loading && filteredPages.length > 0" class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full whitespace-nowrap">
|
|
<thead>
|
|
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50 dark:text-gray-400">
|
|
<th class="px-4 py-3">Page</th>
|
|
<th class="px-4 py-3">Slug</th>
|
|
<th class="px-4 py-3">Type</th>
|
|
<th class="px-4 py-3">Status</th>
|
|
<th class="px-4 py-3">Navigation</th>
|
|
<th class="px-4 py-3">Updated</th>
|
|
<th class="px-4 py-3">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white dark:bg-gray-800 divide-y dark:divide-gray-700">
|
|
<template x-for="page in filteredPages" :key="page.id">
|
|
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
<!-- Page Title -->
|
|
<td class="px-4 py-3">
|
|
<div>
|
|
<p class="font-semibold text-gray-900 dark:text-white" x-text="page.title"></p>
|
|
<p class="text-xs text-gray-600 dark:text-gray-400" x-show="page.vendor_name">
|
|
Vendor: <span x-text="page.vendor_name"></span>
|
|
</p>
|
|
</div>
|
|
</td>
|
|
|
|
<!-- Slug -->
|
|
<td class="px-4 py-3 text-sm">
|
|
<code class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 rounded" x-text="'/' + page.slug"></code>
|
|
</td>
|
|
|
|
<!-- Type -->
|
|
<td class="px-4 py-3 text-sm">
|
|
<span
|
|
class="px-2 py-1 text-xs font-semibold rounded-full"
|
|
:class="page.is_platform_default ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'"
|
|
x-text="page.is_platform_default ? 'Platform' : 'Vendor'"
|
|
></span>
|
|
</td>
|
|
|
|
<!-- Status -->
|
|
<td class="px-4 py-3 text-sm">
|
|
<span
|
|
class="px-2 py-1 text-xs font-semibold rounded-full"
|
|
:class="page.is_published ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'"
|
|
x-text="page.is_published ? 'Published' : 'Draft'"
|
|
></span>
|
|
</td>
|
|
|
|
<!-- Navigation -->
|
|
<td class="px-4 py-3 text-xs">
|
|
<div class="flex gap-1">
|
|
<span x-show="page.show_in_header" class="px-1.5 py-0.5 bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200 rounded">Header</span>
|
|
<span x-show="page.show_in_footer" class="px-1.5 py-0.5 bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-200 rounded">Footer</span>
|
|
<span x-show="!page.show_in_header && !page.show_in_footer" class="text-gray-400">—</span>
|
|
</div>
|
|
</td>
|
|
|
|
<!-- Updated -->
|
|
<td class="px-4 py-3 text-xs" x-text="formatDate(page.updated_at)"></td>
|
|
|
|
<!-- Actions -->
|
|
<td class="px-4 py-3">
|
|
<div class="flex items-center space-x-2 text-sm">
|
|
<a
|
|
:href="`/admin/content-pages/${page.id}/edit`"
|
|
class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
|
title="Edit"
|
|
>
|
|
<span x-html="$icon('edit', 'w-5 h-5')"></span>
|
|
</a>
|
|
<button
|
|
@click="deletePage(page)"
|
|
class="flex items-center justify-center p-2 text-red-600 rounded-lg hover:bg-red-50 dark:text-red-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
|
title="Delete"
|
|
>
|
|
<span x-html="$icon('delete', 'w-5 h-5')"></span>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div x-show="!loading && filteredPages.length === 0" class="text-center py-12 bg-white dark:bg-gray-800 rounded-lg shadow-md">
|
|
<span x-html="$icon('document-text', 'inline w-16 h-16 text-gray-400 mb-4')"></span>
|
|
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-2">No pages found</h3>
|
|
<p class="text-gray-500 dark:text-gray-400 mb-4" x-show="searchQuery">
|
|
No pages match your search: "<span x-text="searchQuery"></span>"
|
|
</p>
|
|
<p class="text-gray-500 dark:text-gray-400 mb-4" x-show="!searchQuery && activeTab === 'vendor'">
|
|
No vendor-specific pages have been created yet.
|
|
</p>
|
|
<a
|
|
href="/admin/content-pages/create"
|
|
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700"
|
|
>
|
|
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
|
|
Create First Page
|
|
</a>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script src="{{ url_for('static', path='admin/js/content-pages.js') }}"></script>
|
|
{% endblock %}
|