feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -1,4 +1,4 @@
<layout name="layouts/admin" />
<layout name="admin" />
<div class="admin-page">
<div class="page-header">

View File

@@ -1,4 +1,4 @@
<layout name="layouts/admin" />
<layout name="admin" />
<div class="admin-page">
<div class="page-header">

View File

@@ -0,0 +1,152 @@
<!doctype html>
<html lang="de" data-theme="{theme ?? 'auto'}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{title} | Admin Panel</title>
<meta name="description" content="{description}">
<meta property="og:type" content="website">
<!-- Theme Meta -->
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#1a1a1a" media="(prefers-color-scheme: dark)">
<!-- Admin CSS (ITCSS Architecture) - Vite Built -->
<link rel="stylesheet" href="/assets/css/admin-U1y6JHpV.css">
<!-- Main Application Assets -->
<link rel="stylesheet" href="/assets/css/main-DN7LWXEn.css">
<script src="/assets/js/main-DReViZUb.js" type="module"></script>
</head>
<body>
<!-- Skip to main content (Accessibility) -->
<a href="#main-content" class="admin-skip-link">Skip to main content</a>
<!-- Admin Layout Grid -->
<div class="admin-layout">
<!-- Sidebar Navigation -->
<x-admin-sidebar currentPath="{current_path}" />
<!-- Header with Search & User Menu -->
<x-admin-header pageTitle="{page_title}" />
<!-- Main Content Area -->
<main class="admin-content" id="main-content" role="main">
{content}
</main>
</div>
<!-- Admin JavaScript (Mobile Menu, Theme Toggle, Dropdowns) -->
<script>
// Mobile Menu Toggle
document.addEventListener('DOMContentLoaded', () => {
const mobileToggle = document.querySelector('[data-mobile-menu-toggle]');
const sidebar = document.querySelector('.admin-sidebar');
const overlay = document.querySelector('[data-mobile-menu-overlay]');
if (mobileToggle && sidebar && overlay) {
const toggleMenu = () => {
const isOpen = sidebar.dataset.mobileMenuOpen === 'true';
sidebar.dataset.mobileMenuOpen = !isOpen;
overlay.dataset.mobileMenuOpen = !isOpen;
mobileToggle.setAttribute('aria-expanded', !isOpen);
};
mobileToggle.addEventListener('click', toggleMenu);
overlay.addEventListener('click', toggleMenu);
}
// Theme Toggle with Dark Mode Detection
const themeToggle = document.querySelector('[data-theme-toggle]');
const htmlElement = document.documentElement;
const storageKey = 'admin-theme-preference';
if (themeToggle) {
// Detect system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
// Initialize theme
const initTheme = () => {
const savedTheme = localStorage.getItem(storageKey) || 'auto';
if (savedTheme === 'auto') {
// Use system preference when auto
htmlElement.dataset.theme = prefersDark.matches ? 'dark' : 'light';
} else {
// Use saved preference
htmlElement.dataset.theme = savedTheme;
}
// Update icon visibility
updateThemeIcon(savedTheme === 'auto' ? (prefersDark.matches ? 'dark' : 'light') : savedTheme);
};
// Update theme icon
const updateThemeIcon = (theme) => {
document.querySelectorAll('[data-theme-icon]').forEach(icon => {
icon.style.display = icon.dataset.themeIcon === theme ? 'block' : 'none';
});
};
// Listen for system theme changes
prefersDark.addEventListener('change', (e) => {
const savedTheme = localStorage.getItem(storageKey);
if (!savedTheme || savedTheme === 'auto') {
htmlElement.dataset.theme = e.matches ? 'dark' : 'light';
updateThemeIcon(e.matches ? 'dark' : 'light');
}
});
// Theme toggle click handler
themeToggle.addEventListener('click', () => {
const currentTheme = localStorage.getItem(storageKey) || 'auto';
const nextTheme = currentTheme === 'light' ? 'dark' :
currentTheme === 'dark' ? 'auto' : 'light';
localStorage.setItem(storageKey, nextTheme);
if (nextTheme === 'auto') {
htmlElement.dataset.theme = prefersDark.matches ? 'dark' : 'light';
updateThemeIcon(prefersDark.matches ? 'dark' : 'light');
} else {
htmlElement.dataset.theme = nextTheme;
updateThemeIcon(nextTheme);
}
});
// Initialize on load
initTheme();
}
// Dropdown Menus
document.querySelectorAll('[data-dropdown-trigger]').forEach(trigger => {
const dropdownId = trigger.dataset.dropdownTrigger;
const dropdown = trigger.closest('[data-dropdown]');
trigger.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = dropdown.dataset.open === 'true';
// Close all other dropdowns
document.querySelectorAll('[data-dropdown]').forEach(dd => {
if (dd !== dropdown) {
dd.dataset.open = 'false';
}
});
dropdown.dataset.open = !isOpen;
});
});
// Close dropdowns on outside click
document.addEventListener('click', () => {
document.querySelectorAll('[data-dropdown]').forEach(dropdown => {
dropdown.dataset.open = 'false';
});
});
});
</script>
</body>
</html>

View File

@@ -1,4 +1,4 @@
<layout name="layouts/admin" />
<layout name="admin" />
<div class="section">
<h2>Analytics Dashboard</h2>

View File

@@ -1,4 +1,4 @@
<layout name="layouts/admin" />
<layout name="admin" />
<div class="section">
<h2>Cache Metrics</h2>

View File

@@ -0,0 +1,42 @@
<!-- Admin Breadcrumbs Component -->
<!-- Uses Framework LinkCollection Value Object -->
<nav class="admin-breadcrumbs" aria-label="Breadcrumb">
<ol class="admin-breadcrumbs__list" role="list">
<!-- Home Icon Link -->
<li class="admin-breadcrumbs__item">
<a href="/admin" class="admin-breadcrumbs__link" aria-label="Home">
<svg class="admin-breadcrumbs__home-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
</svg>
<span class="admin-breadcrumbs__sr-only">Home</span>
</a>
</li>
<!-- LinkCollection Items -->
<if condition="breadcrumbs && breadcrumbs.count() > 0">
<for items="breadcrumbs" as="link" index="index">
<!-- Separator -->
<li class="admin-breadcrumbs__separator" aria-hidden="true">
<svg class="admin-breadcrumbs__separator-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</li>
<!-- Breadcrumb Item -->
<li class="admin-breadcrumbs__item">
<!-- Check if current page (AccessibleLink with aria-current) -->
<if condition="link.hasAttribute('aria-current')">
<span class="admin-breadcrumbs__current" aria-current="page">
{link.text}
</span>
<else>
<a href="{link.href}" class="admin-breadcrumbs__link">
{link.text}
</a>
</else>
</if>
</li>
</for>
</if>
</ol>
</nav>

View File

@@ -0,0 +1,118 @@
<!-- Admin Header Component -->
<header class="admin-header" role="banner">
<!-- Page Title (hidden on mobile, breadcrumbs shown instead) -->
<h1 class="admin-header__title">{page_title}</h1>
<!-- Breadcrumbs (visible on mobile) -->
<if condition="breadcrumbs">
<include template="components/breadcrumbs" data="{breadcrumbs: breadcrumbs}" />
</if>
<!-- Search Bar -->
<div class="admin-header__search">
<form class="admin-search" role="search" action="/admin/search" method="get">
<svg class="admin-search__icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<input
type="search"
name="q"
class="admin-search__input"
placeholder="Search..."
aria-label="Search"
autocomplete="off"
/>
</form>
</div>
<!-- Header Actions -->
<div class="admin-header__actions">
<!-- Notifications -->
<button
type="button"
class="admin-action-btn"
aria-label="Notifications"
data-dropdown-trigger="notifications"
>
<svg class="admin-action-btn__icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
</svg>
<if condition="notification_count > 0">
<span class="admin-action-btn__badge admin-action-btn__badge--count" aria-label="{notification_count} unread notifications">
{notification_count}
</span>
</if>
</button>
<!-- Theme Toggle -->
<button
type="button"
class="admin-theme-toggle"
aria-label="Toggle theme"
data-theme-toggle
>
<svg class="admin-theme-toggle__icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true" data-theme-icon="light">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
</svg>
<svg class="admin-theme-toggle__icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true" data-theme-icon="dark" style="display: none;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
</svg>
<span class="admin-theme-toggle__label">Theme</span>
</button>
<!-- User Menu -->
<div class="admin-user-menu" data-dropdown="user-menu">
<button
type="button"
class="admin-user-menu__trigger"
aria-label="User menu"
aria-expanded="false"
aria-haspopup="true"
data-dropdown-trigger="user-menu"
>
<img
src="{user.avatar ?? '/assets/default-avatar.png'}"
alt="{user.name}"
class="admin-user-menu__avatar"
width="32"
height="32"
/>
<span class="admin-user-menu__name">{user.name}</span>
<svg class="admin-user-menu__chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<ul class="admin-user-menu__dropdown" role="menu" aria-labelledby="user-menu" data-dropdown-content="user-menu">
<li class="admin-user-menu__item" role="none">
<a href="/admin/profile" class="admin-user-menu__link" role="menuitem">
<svg class="admin-user-menu__icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
<span>Profile</span>
</a>
</li>
<li class="admin-user-menu__item" role="none">
<a href="/admin/settings" class="admin-user-menu__link" role="menuitem">
<svg class="admin-user-menu__icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<span>Settings</span>
</a>
</li>
<li role="none">
<hr class="admin-user-menu__divider" />
</li>
<li class="admin-user-menu__item" role="none">
<a href="/logout" class="admin-user-menu__link" role="menuitem">
<svg class="admin-user-menu__icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
</svg>
<span>Logout</span>
</a>
</li>
</ul>
</div>
</div>
</header>

View File

@@ -0,0 +1,108 @@
<!-- Admin Sidebar Component -->
<aside class="admin-sidebar" role="navigation" aria-label="Main navigation">
<!-- Sidebar Header -->
<div class="admin-sidebar__header">
<svg class="admin-sidebar__logo" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<rect width="40" height="40" rx="8" fill="currentColor" opacity="0.1"/>
<path d="M20 10L28 16V24L20 30L12 24V16L20 10Z" fill="currentColor" opacity="0.8"/>
</svg>
<h1 class="admin-sidebar__title">Admin</h1>
</div>
<!-- Main Navigation -->
<nav class="admin-nav" aria-label="Primary">
<!-- Dashboard Section -->
<div class="admin-nav__section">
<ul class="admin-nav__list" role="list">
<li class="admin-nav__item">
<a href="/admin" class="admin-nav__link" aria-current="{current_path === '/admin' ? 'page' : null}">
<svg class="admin-nav__icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
</svg>
<span>Dashboard</span>
</a>
</li>
</ul>
</div>
<!-- Content Section -->
<div class="admin-nav__section">
<h2 class="admin-nav__section-title">Content</h2>
<ul class="admin-nav__list" role="list">
<li class="admin-nav__item">
<a href="/admin/pages" class="admin-nav__link" aria-current="{current_path === '/admin/pages' ? 'page' : null}">
<svg class="admin-nav__icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<span>Pages</span>
</a>
</li>
<li class="admin-nav__item">
<a href="/admin/media" class="admin-nav__link" aria-current="{current_path === '/admin/media' ? 'page' : null}">
<svg class="admin-nav__icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span>Media</span>
</a>
</li>
</ul>
</div>
<!-- System Section -->
<div class="admin-nav__section">
<h2 class="admin-nav__section-title">System</h2>
<ul class="admin-nav__list" role="list">
<li class="admin-nav__item">
<a href="/admin/users" class="admin-nav__link" aria-current="{current_path === '/admin/users' ? 'page' : null}">
<svg class="admin-nav__icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
</svg>
<span>Users</span>
</a>
</li>
<li class="admin-nav__item">
<a href="/admin/settings" class="admin-nav__link" aria-current="{current_path === '/admin/settings' ? 'page' : null}">
<svg class="admin-nav__icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<span>Settings</span>
</a>
</li>
</ul>
</div>
</nav>
<!-- Sidebar Footer -->
<div class="admin-sidebar__footer">
<a href="/admin/profile" class="admin-sidebar__user">
<img
src="{user.avatar ?? '/assets/default-avatar.png'}"
alt="{user.name}"
class="admin-sidebar__avatar"
width="32"
height="32"
/>
<div class="admin-sidebar__user-info">
<span class="admin-sidebar__user-name">{user.name}</span>
<span class="admin-sidebar__user-role">{user.role}</span>
</div>
</a>
</div>
</aside>
<!-- Mobile Menu Toggle -->
<button
type="button"
class="admin-sidebar__mobile-toggle"
aria-label="Toggle navigation menu"
aria-expanded="false"
data-mobile-menu-toggle
>
<svg class="admin-sidebar__toggle-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
<!-- Mobile Overlay -->
<div class="admin-mobile-overlay" data-mobile-menu-overlay aria-hidden="true"></div>

View File

@@ -1,51 +1,140 @@
<layout name="layouts/admin" />
<layout name="admin" />
<div class="section">
<h2>Admin Dashboard</h2>
<div class="stats-grid">
<div class="stat-card">
<h3>System Status</h3>
<p><strong>Status:</strong> <span style="color: var(--success);">Online</span></p>
<p><strong>Uptime:</strong> {{ uptime_formatted }}</p>
<p><strong>Version:</strong> {{ framework_version }}</p>
<!-- Breadcrumbs -->
<x-breadcrumbs items='[{"url": "/admin", "text": "Dashboard"}]' />
<!-- Dashboard Content -->
<div class="admin-content__header admin-content__header--with-actions">
<div class="admin-content__title-group">
<h1 class="admin-content__title">Admin Dashboard</h1>
<p class="admin-content__description">System overview and quick actions</p>
</div>
</div>
<!-- System Stats Grid -->
<div class="admin-grid admin-grid--3-col">
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">System Status</h3>
</div>
<div class="stat-card">
<h3>Performance</h3>
<p><strong>Memory Usage:</strong> {{ memory_usage_formatted }}</p>
<p><strong>Peak Memory:</strong> {{ peak_memory_formatted }}</p>
<p><strong>Load Average:</strong> {{ load_average }}</p>
</div>
<div class="stat-card">
<h3>Database</h3>
<p><strong>Connection:</strong> <span style="color: var(--success);">Connected</span></p>
<p><strong>Pool Size:</strong> {{ db_pool_size }}</p>
<p><strong>Active Connections:</strong> {{ db_active_connections }}</p>
<div class="admin-card__content">
<div class="admin-stat-list">
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Status</span>
<span class="admin-badge admin-badge--success">Online</span>
</div>
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Uptime</span>
<span class="admin-stat-list__value">{{ uptime_formatted }}</span>
</div>
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Version</span>
<span class="admin-stat-list__value">{{ $framework_version }}</span>
</div>
</div>
</div>
</div>
<div class="stats-grid">
<div class="stat-card">
<h3>Cache Performance</h3>
<p><strong>Hit Rate:</strong> {{ cache_hit_rate }}</p>
<p><strong>Total Operations:</strong> {{ cache_total_operations }}</p>
<p><strong>Status:</strong> <span style="color: var(--success);">Running</span></p>
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Performance</h3>
</div>
<div class="stat-card">
<h3>Recent Activity</h3>
<p><strong>Requests Today:</strong> {{ requests_today }}</p>
<p><strong>Errors:</strong> {{ errors_today }}</p>
<p><strong>Last Deployment:</strong> {{ last_deployment }}</p>
<div class="admin-card__content">
<div class="admin-stat-list">
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Memory Usage</span>
<span class="admin-stat-list__value">{{ memory_usage_formatted }}</span>
</div>
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Peak Memory</span>
<span class="admin-stat-list__value">{{ peak_memory_formatted }}</span>
</div>
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Load Average</span>
<span class="admin-stat-list__value">{{ load_average }}</span>
</div>
</div>
</div>
</div>
<div class="stat-card">
<h3>Quick Actions</h3>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<a href="{{ clear_cache_url }}" style="background: var(--primary); color: white; padding: 8px 16px; border-radius: 4px; text-decoration: none; font-size: 14px;">Clear Cache</a>
<a href="{{ logs_url }}" style="background: var(--secondary); color: white; padding: 8px 16px; border-radius: 4px; text-decoration: none; font-size: 14px;">View Logs</a>
<a href="{{ migrations_url }}" style="background: var(--accent); color: white; padding: 8px 16px; border-radius: 4px; text-decoration: none; font-size: 14px;">Migrations</a>
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Database</h3>
</div>
<div class="admin-card__content">
<div class="admin-stat-list">
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Connection</span>
<span class="admin-badge admin-badge--success">Connected</span>
</div>
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Pool Size</span>
<span class="admin-stat-list__value">{{ db_pool_size }}</span>
</div>
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Active Connections</span>
<span class="admin-stat-list__value">{{ db_active_connections }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Additional Stats Grid -->
<div class="admin-grid admin-grid--3-col" style="margin-top: var(--admin-spacing-xl);">
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Cache Performance</h3>
</div>
<div class="admin-card__content">
<div class="admin-stat-list">
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Hit Rate</span>
<span class="admin-stat-list__value">{{ cache_hit_rate }}</span>
</div>
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Total Operations</span>
<span class="admin-stat-list__value">{{ cache_total_operations }}</span>
</div>
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Status</span>
<span class="admin-badge admin-badge--success">Running</span>
</div>
</div>
</div>
</div>
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Recent Activity</h3>
</div>
<div class="admin-card__content">
<div class="admin-stat-list">
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Requests Today</span>
<span class="admin-stat-list__value">{{ requests_today }}</span>
</div>
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Errors</span>
<span class="admin-stat-list__value">{{ errors_today }}</span>
</div>
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Last Deployment</span>
<span class="admin-stat-list__value">{{ last_deployment }}</span>
</div>
</div>
</div>
</div>
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Quick Actions</h3>
</div>
<div class="admin-card__content">
<div class="admin-cluster">
<x-a href="{{ clear_cache_url }}" class="admin-btn admin-btn--primary">Clear Cache</x-a>
<x-a href="{{ logs_url }}" class="admin-btn admin-btn--secondary">View Logs</x-a>
<x-a href="{{ migrations_url }}" class="admin-btn admin-btn--accent">Migrations</x-a>
</div>
</div>
</div>

View File

@@ -0,0 +1,218 @@
<layout name="admin" />
<!-- Breadcrumbs -->
<x-breadcrumbs items='[{"url": "/admin", "text": "Dashboard"}, {"url": "/admin/deployment/dashboard", "text": "Deployments"}]' />
<!-- Dashboard Header -->
<div class="admin-content__header admin-content__header--with-actions">
<div class="admin-content__title-group">
<h1 class="admin-content__title">{{ $pageTitle }}</h1>
<p class="admin-content__description">{{ $pageDescription }}</p>
</div>
</div>
<!-- Overall Statistics Grid -->
<div class="admin-grid admin-grid--4-col">
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Total Deployments</h3>
</div>
<div class="admin-card__content">
<div class="admin-stat-big">
<span class="admin-stat-big__value">{{ $totalDeployments }}</span>
<span class="admin-stat-big__label">All Time</span>
</div>
</div>
</div>
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Success Rate</h3>
</div>
<div class="admin-card__content">
<div class="admin-stat-big">
<span class="admin-stat-big__value">{{ $successRate }}%</span>
<span class="admin-stat-big__label">Success</span>
</div>
</div>
</div>
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Failed Deployments</h3>
</div>
<div class="admin-card__content">
<div class="admin-stat-big">
<span class="admin-stat-big__value">{{ $failedDeployments }}</span>
<span class="admin-stat-big__label">Failures</span>
</div>
</div>
</div>
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Average Duration</h3>
</div>
<div class="admin-card__content">
<div class="admin-stat-big">
<span class="admin-stat-big__value">{{ $averageDurationFormatted }}</span>
<span class="admin-stat-big__label">Avg Time</span>
</div>
</div>
</div>
</div>
<!-- Environment-Specific Statistics -->
<div class="admin-grid admin-grid--2-col" style="margin-top: var(--admin-spacing-xl);">
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Production Environment</h3>
</div>
<div class="admin-card__content">
<div class="admin-stat-list">
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Total Deployments</span>
<span class="admin-stat-list__value">{{ $productionStats['total_deployments'] }}</span>
</div>
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Success Rate</span>
<span class="admin-stat-list__value">{{ $productionStats['success_rate'] }}%</span>
</div>
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Failed</span>
<span class="admin-stat-list__value">{{ $productionStats['failed'] }}</span>
</div>
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Rolled Back</span>
<span class="admin-stat-list__value">{{ $productionStats['rolled_back'] }}</span>
</div>
</div>
</div>
</div>
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Staging Environment</h3>
</div>
<div class="admin-card__content">
<div class="admin-stat-list">
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Total Deployments</span>
<span class="admin-stat-list__value">{{ $stagingStats['total_deployments'] }}</span>
</div>
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Success Rate</span>
<span class="admin-stat-list__value">{{ $stagingStats['success_rate'] }}%</span>
</div>
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Failed</span>
<span class="admin-stat-list__value">{{ $stagingStats['failed'] }}</span>
</div>
<div class="admin-stat-list__item">
<span class="admin-stat-list__label">Rolled Back</span>
<span class="admin-stat-list__value">{{ $stagingStats['rolled_back'] }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Deployments Table -->
<div class="admin-card" style="margin-top: var(--admin-spacing-xl);">
<div class="admin-card__header">
<h3 class="admin-card__title">Recent Deployments</h3>
</div>
<div class="admin-card__content">
<div if="count($recentDeployments) > 0">
<table class="admin-table">
<thead>
<tr>
<th>Pipeline ID</th>
<th>Environment</th>
<th>Status</th>
<th>Duration</th>
<th>Stages</th>
<th>Success Rate</th>
<th>Started At</th>
<th>Completed At</th>
</tr>
</thead>
<tbody>
<for items="recentDeployments" as="deployment">
<tr>
<td><code>{{ $deployment['pipelineId'] }}</code></td>
<td>
<span class="admin-badge admin-badge--neutral">
{{ $deployment['environment'] }}
</span>
</td>
<td>
<span class="admin-badge {{ $deployment['statusBadgeClass'] }}">
{{ $deployment['status'] }}
</span>
<span if="$deployment['wasRolledBack']" class="admin-badge admin-badge--warning">Rolled Back</span>
</td>
<td>{{ $deployment['durationFormatted'] }}</td>
<td>{{ $deployment['stageCount'] }}</td>
<td>{{ $deployment['successRate'] }}%</td>
<td>{{ $deployment['startedAt'] }}</td>
<td>{{ $deployment['completedAt'] }}</td>
</tr>
</for>
</tbody>
</table>
</div>
<div if="count($recentDeployments) === 0">
<div class="admin-empty-state">
<p class="admin-empty-state__text">No deployments found</p>
</div>
</div>
</div>
</div>
<!-- Failed Deployments Section -->
<div if="count($failedDeployments) > 0" class="admin-card" style="margin-top: var(--admin-spacing-xl);">
<div class="admin-card__header">
<h3 class="admin-card__title">Recent Failed Deployments</h3>
</div>
<div class="admin-card__content">
<table class="admin-table">
<thead>
<tr>
<th>Pipeline ID</th>
<th>Environment</th>
<th>Failed Stage</th>
<th>Error</th>
<th>Duration</th>
<th>Started At</th>
</tr>
</thead>
<tbody>
<for items="failedDeployments" as="deployment">
<tr>
<td><code>{{ $deployment['pipelineId'] }}</code></td>
<td>
<span class="admin-badge admin-badge--neutral">
{{ $deployment['environment'] }}
</span>
</td>
<td>
<span if="$deployment['failedStage'] !== null" class="admin-badge admin-badge--danger">
{{ $deployment['failedStage'] }}
</span>
<span if="$deployment['failedStage'] === null" class="admin-badge admin-badge--neutral">N/A</span>
</td>
<td>
<span if="$deployment['error'] !== null" class="admin-text-truncate" title="{{ $deployment['error'] }}">
{{ $deployment['error'] }}
</span>
<span if="$deployment['error'] === null" class="admin-text-muted">No error message</span>
</td>
<td>{{ $deployment['durationFormatted'] }}</td>
<td>{{ $deployment['startedAt'] }}</td>
</tr>
</for>
</tbody>
</table>
</div>
</div>

View File

@@ -1,4 +1,4 @@
<layout name="layouts/admin" />
<layout name="admin" />
<div class="section">
<h2>Umgebungsvariablen</h2>

View File

@@ -1,4 +1,4 @@
<layout name="layouts/admin" />
<layout name="admin" />
<!-- Cache invalidation: 2025-01-20 16:11 -->
<div class="section">
<h2>{{ title }}</h2>

View File

@@ -1,4 +1,4 @@
<layout name="layouts/admin" />
<layout name="admin" />
<div class="section">
<div class="page-header-actions">
@@ -49,29 +49,15 @@
</div>
</div>
<!-- Image Gallery Section -->
<!-- Image Gallery Section - Now using LiveComponent -->
<div class="stat-card full-width">
<h3>Image Gallery</h3>
<h3>Image Gallery (LiveComponent)</h3>
<p style="color: var(--gray-600); margin-bottom: 20px;">
Real-time image gallery with server-side filtering, sorting, and actions
</p>
<div data-module="image-manager"
data-image-gallery
data-list-endpoint="/api/images"
data-upload-endpoint="/api/images"
data-page-size="20"
data-columns="4"
data-pagination
data-search
data-sort
data-allow-delete
data-allow-edit
data-selectable>
<!-- Gallery will be rendered here by JavaScript -->
<div class="gallery-loading">
<div class="loading-spinner"></div>
<p>Loading images...</p>
</div>
</div>
<!-- LiveComponent replaces JavaScript-based gallery -->
{{image_gallery}}
</div>
<!-- Legacy Image Slots Section (for backward compatibility) -->

View File

@@ -1,4 +1,4 @@
<layout name="layouts/admin" />
<layout name="admin" />
<div class="section">
<h2>{{ title }}</h2>

View File

@@ -0,0 +1,68 @@
<layout name="admin" />
<!-- Breadcrumbs -->
<x-breadcrumbs items='[{"url": "/admin", "text": "Admin"}, {"url": "/admin/jobs/dashboard", "text": "Job Dashboard"}]' />
<!-- Dashboard Header -->
<div class="admin-content__header admin-content__header--with-actions">
<div class="admin-content__title-group">
<h1 class="admin-content__title">Background Jobs Dashboard</h1>
<p class="admin-content__description">Real-time monitoring of queue, workers, and scheduler</p>
</div>
</div>
<!-- Dashboard Grid - Top Row: Queue Stats & Worker Health -->
<div class="admin-grid admin-grid--2-col">
<!-- Queue Statistics Card -->
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Queue Statistics</h3>
<span class="admin-badge admin-badge--info">Live</span>
</div>
<div class="admin-card__content">
<x-queue-stats />
</div>
</div>
<!-- Worker Health Card -->
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Worker Health</h3>
<span class="admin-badge admin-badge--info">Live</span>
</div>
<div class="admin-card__content">
<x-worker-health />
</div>
</div>
</div>
<!-- Dashboard Grid - Middle Row: Scheduler Timeline -->
<div class="admin-grid admin-grid--1-col">
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Scheduled Tasks Timeline</h3>
<span class="admin-badge admin-badge--info">Live</span>
</div>
<div class="admin-card__content">
<x-scheduler-timeline />
</div>
</div>
</div>
<!-- Dashboard Grid - Bottom Row: Failed Jobs -->
<div class="admin-grid admin-grid--1-col">
<div class="admin-card">
<div class="admin-card__header">
<h3 class="admin-card__title">Failed Jobs</h3>
<span class="admin-badge admin-badge--warning">Needs Attention</span>
</div>
<div class="admin-card__content">
<x-failed-jobs-list />
</div>
</div>
</div>
<!-- Dashboard Info Footer -->
<div class="admin-info-box admin-info-box--info">
<strong>📊 Live Dashboard</strong> - All components auto-update in real-time. Queue Stats and Worker Health refresh every 5 seconds, Failed Jobs every 10 seconds, and Scheduler Timeline every 30 seconds.
</div>

View File

@@ -1,142 +0,0 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{title} | Admin Panel</title>
<meta name="description" content="{description}">
<meta property="og:type" content="website">
<link rel='stylesheet' href='/css/admin.css'>
<link rel="stylesheet" href="/assets/css/main-DLVw97vA.css">
<script src="/assets/js/main-CyVTPjIx.js" type="module"></script>
<style>
.admin-layout {
display: grid;
grid-template-columns: 250px 1fr;
grid-template-rows: auto 1fr;
min-height: 100vh;
grid-template-areas:
"sidebar header"
"sidebar content";
}
.admin-header {
grid-area: header;
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
padding: 1rem 2rem;
}
.admin-sidebar {
grid-area: sidebar;
background: #343a40;
color: white;
padding: 1rem;
}
.admin-content {
grid-area: content;
padding: 2rem;
overflow-y: auto;
}
.breadcrumbs {
display: flex;
gap: 0.5rem;
align-items: center;
margin-bottom: 1rem;
font-size: 0.875rem;
color: #6c757d;
}
.breadcrumbs a {
color: #007bff;
text-decoration: none;
}
.breadcrumbs a:hover {
text-decoration: underline;
}
.nav-section {
margin-bottom: 2rem;
}
.nav-section h3 {
color: #adb5bd;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
font-weight: 600;
}
.nav-items {
list-style: none;
padding: 0;
margin: 0;
}
.nav-items li {
margin-bottom: 0.25rem;
}
.nav-items a {
display: block;
color: #dee2e6;
text-decoration: none;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
transition: background-color 0.15s;
}
.nav-items a:hover {
background-color: #495057;
color: white;
}
.nav-items a.active {
background-color: #007bff;
color: white;
}
.logo {
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid #495057;
}
.logo h2 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
</style>
</head>
<body>
<div class="admin-layout">
<aside class="admin-sidebar">
<div class="logo">
<h2>Admin Panel</h2>
</div>
{navigation}
</aside>
<header class="admin-header">
<nav class="breadcrumbs">
{breadcrumbs}
</nav>
<h1>{page_title}</h1>
</header>
<main class="admin-content">
{content}
</main>
</div>
</body>
</html>

View File

@@ -1,4 +1,4 @@
<layout name="layouts/admin" />
<layout name="admin" />
<div class="section">
<h2>📄 {{ title }}</h2>
@@ -324,17 +324,21 @@ class LogViewer {
Object.values(logs).forEach(log => {
const li = document.createElement('li');
li.className = 'log-item';
// Extract log name - handle both string and object (VO)
const logName = typeof log.name === 'object' ? log.name.value : log.name;
li.innerHTML = `
<div class="log-name">${log.name}</div>
<div class="log-name">${logName}</div>
<div class="log-info">
${log.size_human} • ${log.modified_human}
${log.size} • ${log.modified}
${!log.readable ? ' • ⚠️ Not readable' : ''}
</div>
`;
if (log.readable) {
li.addEventListener('click', () => {
this.selectLog(log.name, li);
this.selectLog(logName, li);
});
} else {
li.style.opacity = '0.5';

View File

@@ -1,4 +1,4 @@
<layout name="layouts/admin" />
<layout name="admin" />
<div class="section">
<h2>{{ title }}</h2>

View File

@@ -1,4 +1,4 @@
<layout name="layouts/admin" />
<layout name="admin" />
<div class="section">
<h2>Performance Übersicht</h2>

View File

@@ -1,4 +1,4 @@
<layout name="layouts/admin" />
<layout name="admin" />
<div class="section">
<h2>{{ title }}</h2>

View File

@@ -1,4 +1,4 @@
<layout name="layouts/admin" />
<layout name="admin" />
<div class="section">
<h2>{{ title }}</h2>

View File

@@ -1,4 +1,4 @@
<layout name="layouts/admin" />
<layout name="admin" />
<div class="section">
<h2>System Routes</h2>

View File

@@ -1,4 +1,4 @@
<layout name="layouts/admin" />
<layout name="admin" />
<div class="section">
<h2>Registrierte Dienste</h2>

View File

@@ -1,4 +1,4 @@
<layout name="layouts/admin" />
<layout name="admin" />
<div class="section">
<h2>{{ title }}</h2>

View File

@@ -1,4 +1,4 @@
<layout name="layouts/admin" />
<layout name="admin" />
<div class="section">
<h2>{{ title }}</h2>