refactor: reorganize project structure for better maintainability
- Move 45 debug/test files from root to organized scripts/ directories - Secure public/ directory by removing debug files (security improvement) - Create structured scripts organization: • scripts/debug/ (20 files) - Framework debugging tools • scripts/test/ (18 files) - Test and validation scripts • scripts/maintenance/ (5 files) - Maintenance utilities • scripts/dev/ (2 files) - Development tools Security improvements: - Removed all debug/test files from public/ directory - Only production files remain: index.php, health.php Root directory cleanup: - Reduced from 47 to 2 PHP files in root - Only essential production files: console.php, worker.php This improves: ✅ Security (no debug code in public/) ✅ Organization (clear separation of concerns) ✅ Maintainability (easy to find and manage scripts) ✅ Professional structure (clean root directory)
This commit is contained in:
@@ -1,96 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= $title ?></title>
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div class="admin-header">
|
||||
<h1>Framework Admin Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div class="admin-nav">
|
||||
<a href="/admin" class="active">Dashboard</a>
|
||||
<a href="/admin/routes">Routen</a>
|
||||
<a href="/admin/services">Dienste</a>
|
||||
<a href="/admin/environment">Umgebung</a>
|
||||
<a href="/admin/performance">Performance</a>
|
||||
<a href="/admin/redis">Redis</a>
|
||||
<a href="/admin/phpinfo">PHP Info</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-content">
|
||||
<div class="dashboard-stats">
|
||||
<div class="stat-box">
|
||||
<h3>Framework Version</h3>
|
||||
<div class="stat-value"><?= $stats['frameworkVersion'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>PHP Version</h3>
|
||||
<div class="stat-value"><?= $stats['phpVersion'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Speicherverbrauch</h3>
|
||||
<div class="stat-value"><?= $stats['memoryUsage'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Max. Speicherverbrauch</h3>
|
||||
<div class="stat-value"><?= $stats['peakMemoryUsage'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Server</h3>
|
||||
<div class="stat-value"><?= $stats['serverInfo'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Serverzeit</h3>
|
||||
<div class="stat-value"><?= $stats['serverTime'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Zeitzone</h3>
|
||||
<div class="stat-value"><?= $stats['timezone'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Betriebssystem</h3>
|
||||
<div class="stat-value"><?= $stats['operatingSystem'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Server Uptime</h3>
|
||||
<div class="stat-value"><?= $stats['uptime'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Aktive Sessions</h3>
|
||||
<div class="stat-value"><?= $stats['sessionCount'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Registrierte Dienste</h3>
|
||||
<div class="stat-value"><?= $stats['servicesCount'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-section">
|
||||
<h2>PHP Erweiterungen</h2>
|
||||
<div class="extensions-list">
|
||||
<?php foreach ($stats['loadedExtensions'] as $extension): ?>
|
||||
<span class="extension-badge"><?= $extension ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-footer">
|
||||
<p>© <?= date('Y') ?> Framework Admin</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,86 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ title }}</title>
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div class="admin-header">
|
||||
<h1>Umgebungsvariablen</h1>
|
||||
</div>
|
||||
|
||||
<div class="admin-nav">
|
||||
<a href="/admin">Dashboard</a>
|
||||
<a href="/admin/routes">Routen</a>
|
||||
<a href="/admin/services">Dienste</a>
|
||||
<a href="/admin/environment" class="active">Umgebung</a>
|
||||
<a href="/admin/performance">Performance</a>
|
||||
<a href="/admin/redis">Redis</a>
|
||||
<a href="/admin/phpinfo">PHP Info</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-content">
|
||||
<div class="admin-tools">
|
||||
<input type="text" id="envFilter" placeholder="Variablen filtern..." class="search-input">
|
||||
<div class="filter-tags">
|
||||
<button class="filter-tag" data-prefix="APP_">APP_</button>
|
||||
<button class="filter-tag" data-prefix="DB_">DB_</button>
|
||||
<button class="filter-tag" data-prefix="REDIS_">REDIS_</button>
|
||||
<button class="filter-tag" data-prefix="RATE_LIMIT_">RATE_LIMIT_</button>
|
||||
<button class="filter-tag" data-prefix="PHP_">PHP_</button>
|
||||
<button class="filter-tag active" data-prefix="">Alle</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="admin-table" id="envTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Wert</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<for var="envVar" in="env">
|
||||
<tr>
|
||||
<td>{{ envVar.key }}</td>
|
||||
<td>{{ envVar.value }}</td>
|
||||
</tr>
|
||||
</for>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="admin-footer">
|
||||
<p>© {{ current_year }} Framework Admin</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Filterung der Umgebungsvariablen
|
||||
document.getElementById('envFilter').addEventListener('input', filterTable);
|
||||
|
||||
// Tag-Filter
|
||||
document.querySelectorAll('.filter-tag').forEach(tag => {
|
||||
tag.addEventListener('click', function() {
|
||||
document.querySelectorAll('.filter-tag').forEach(t => t.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
const prefix = this.getAttribute('data-prefix');
|
||||
document.getElementById('envFilter').value = prefix;
|
||||
filterTable();
|
||||
});
|
||||
});
|
||||
|
||||
function filterTable() {
|
||||
const filterValue = document.getElementById('envFilter').value.toLowerCase();
|
||||
const rows = document.querySelectorAll('#envTable tbody tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const key = row.cells[0].textContent.toLowerCase();
|
||||
row.style.display = key.includes(filterValue) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,250 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Image Manager</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container-fluid">
|
||||
<h1>Image Manager</h1>
|
||||
|
||||
<div class="row">
|
||||
<!-- Image Slots Section -->
|
||||
<div class="col-md-6">
|
||||
<h2>Image Slots</h2>
|
||||
<div id="image-slots" class="list-group">
|
||||
<for var="slot" in="slots">
|
||||
<div class="list-group-item slot-item" data-slot-id="{{ slot.id }}">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h5>{{ slot.slotName }}</h5>
|
||||
<small class="text-muted">ID: {{ slot.id }}</small>
|
||||
</div>
|
||||
<div class="slot-image-container" style="width: 100px; height: 100px;">
|
||||
<div class="border border-dashed d-flex align-items-center justify-content-center h-100"
|
||||
ondrop="handleDrop(event, '{{ slot.id }}')"
|
||||
ondragover="handleDragOver(event)"
|
||||
ondragleave="handleDragLeave(event)">
|
||||
<span class="text-muted">Drop image here or click to select</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</for>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Available Images Section -->
|
||||
<div class="col-md-6">
|
||||
<h2>Available Images</h2>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="mb-3">
|
||||
<input type="text"
|
||||
id="image-search"
|
||||
class="form-control"
|
||||
placeholder="Search images..."
|
||||
onkeyup="searchImages()">
|
||||
</div>
|
||||
|
||||
<!-- Images Grid -->
|
||||
<div id="images-grid" class="row g-2">
|
||||
<for var="image" in="images">
|
||||
<div class="col-md-4 image-item"
|
||||
data-filename="{{ image.originalFilename }}"
|
||||
data-alt="{{ image.altText }}">
|
||||
<div class="card">
|
||||
<img src="/media/images/{{ image.path }}"
|
||||
alt="{{ image.altText }}"
|
||||
class="card-img-top"
|
||||
style="height: 150px; object-fit: cover; cursor: move;"
|
||||
draggable="true"
|
||||
ondragstart="handleDragStart(event, '{{ image.ulid }}')"
|
||||
onclick="selectImage('{{ image.ulid }}')">
|
||||
<div class="card-body p-2">
|
||||
<small class="text-truncate d-block">
|
||||
{{ image.originalFilename }}
|
||||
</small>
|
||||
<small class="text-muted">
|
||||
{{ image.width }}x{{ image.height }} • {{ image.fileSize }}KB
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</for>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Selection Modal -->
|
||||
<div class="modal fade" id="imageSelectModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Select Image for <span id="modal-slot-name"></span></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="modal-images" class="row g-2">
|
||||
<!-- Images will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Current dragging image
|
||||
let draggedImageUlid = null;
|
||||
let selectedSlotId = null;
|
||||
|
||||
// Handle drag start
|
||||
function handleDragStart(event, imageUlid) {
|
||||
draggedImageUlid = imageUlid;
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
}
|
||||
|
||||
// Handle drag over
|
||||
function handleDragOver(event) {
|
||||
event.preventDefault();
|
||||
event.currentTarget.classList.add('bg-light');
|
||||
}
|
||||
|
||||
// Handle drag leave
|
||||
function handleDragLeave(event) {
|
||||
event.currentTarget.classList.remove('bg-light');
|
||||
}
|
||||
|
||||
// Handle drop
|
||||
async function handleDrop(event, slotId) {
|
||||
event.preventDefault();
|
||||
event.currentTarget.classList.remove('bg-light');
|
||||
|
||||
if (draggedImageUlid) {
|
||||
await assignImageToSlot(slotId, draggedImageUlid);
|
||||
}
|
||||
}
|
||||
|
||||
// Assign image to slot
|
||||
async function assignImageToSlot(slotId, imageUlid) {
|
||||
try {
|
||||
const response = await fetch(`/api/image-slots/${slotId}/image`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ image_ulid: imageUlid })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
location.reload(); // Simple reload for now
|
||||
} else {
|
||||
alert('Failed to assign image');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Error assigning image');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove image from slot
|
||||
async function removeImage(slotId) {
|
||||
if (!confirm('Remove image from this slot?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/image-slots/${slotId}/image`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Failed to remove image');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Error removing image');
|
||||
}
|
||||
}
|
||||
|
||||
// Select image (click handler)
|
||||
function selectImage(imageUlid) {
|
||||
// Find which slot was clicked if any
|
||||
const clickedSlot = document.querySelector('.slot-item.selecting');
|
||||
if (clickedSlot) {
|
||||
const slotId = clickedSlot.dataset.slotId;
|
||||
assignImageToSlot(slotId, imageUlid);
|
||||
clickedSlot.classList.remove('selecting');
|
||||
}
|
||||
}
|
||||
|
||||
// Search images
|
||||
function searchImages() {
|
||||
const searchTerm = document.getElementById('image-search').value.toLowerCase();
|
||||
const imageItems = document.querySelectorAll('.image-item');
|
||||
|
||||
imageItems.forEach(item => {
|
||||
const filename = item.dataset.filename.toLowerCase();
|
||||
const alt = item.dataset.alt.toLowerCase();
|
||||
|
||||
if (filename.includes(searchTerm) || alt.includes(searchTerm)) {
|
||||
item.style.display = '';
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add click handler to slots for selection mode
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.slot-item').forEach(slot => {
|
||||
const container = slot.querySelector('.slot-image-container');
|
||||
if (container && !container.querySelector('img')) {
|
||||
container.style.cursor = 'pointer';
|
||||
container.addEventListener('click', function() {
|
||||
// Remove previous selection
|
||||
document.querySelectorAll('.slot-item').forEach(s => s.classList.remove('selecting'));
|
||||
// Mark as selecting
|
||||
slot.classList.add('selecting');
|
||||
// Highlight available images
|
||||
document.getElementById('images-grid').classList.add('selecting-mode');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add some CSS
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.slot-item.selecting {
|
||||
border: 2px solid #0d6efd;
|
||||
background-color: #e7f1ff;
|
||||
}
|
||||
|
||||
.selecting-mode .card {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selecting-mode .card:hover {
|
||||
border-color: #0d6efd;
|
||||
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
|
||||
}
|
||||
|
||||
.border-dashed {
|
||||
border-style: dashed !important;
|
||||
}
|
||||
|
||||
.object-fit-cover {
|
||||
object-fit: cover;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
</script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,125 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ title }}</title>
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div class="admin-header">
|
||||
<h1>Performance-Übersicht</h1>
|
||||
</div>
|
||||
|
||||
<div class="admin-nav">
|
||||
<a href="/admin">Dashboard</a>
|
||||
<a href="/admin/routes">Routen</a>
|
||||
<a href="/admin/services">Dienste</a>
|
||||
<a href="/admin/environment">Umgebung</a>
|
||||
<a href="/admin/performance" class="active">Performance</a>
|
||||
<a href="/admin/redis">Redis</a>
|
||||
<a href="/admin/phpinfo">PHP Info</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-content">
|
||||
<div class="dashboard-stats">
|
||||
<div class="stat-box">
|
||||
<h3>Aktueller Speicherverbrauch</h3>
|
||||
<div class="stat-value">{{ performance.currentMemoryUsage }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Maximaler Speicherverbrauch</h3>
|
||||
<div class="stat-value">{{ performance.peakMemoryUsage }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Speicherlimit</h3>
|
||||
<div class="stat-value">{{ performance.memoryLimit }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Speicherauslastung</h3>
|
||||
<div class="stat-value">
|
||||
<div class="progress-bar">
|
||||
<div class="progress" style="width: {{ performance.memoryUsagePercentage }}%"></div>
|
||||
</div>
|
||||
<div class="progress-value">{{ performance.memoryUsagePercentage }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Systemlast (1/5/15 min)</h3>
|
||||
<div class="stat-value">
|
||||
{{ performance.loadAverage.0 }} /
|
||||
{{ performance.loadAverage.1 }} /
|
||||
{{ performance.loadAverage.2 }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>OPCache aktiviert</h3>
|
||||
<div class="stat-value">{{ performance.opcacheEnabled }}</div>
|
||||
</div>
|
||||
|
||||
<div if="performance.opcacheMemoryUsage">
|
||||
<div class="stat-box">
|
||||
<h3>OPCache Speicherverbrauch</h3>
|
||||
<div class="stat-value">{{ performance.opcacheMemoryUsage }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>OPCache Cache Hits</h3>
|
||||
<div class="stat-value">{{ performance.opcacheCacheHits }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>OPCache Miss Rate</h3>
|
||||
<div class="stat-value">{{ performance.opcacheMissRate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Ausführungszeit</h3>
|
||||
<div class="stat-value">{{ performance.executionTime }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Geladene Dateien</h3>
|
||||
<div class="stat-value">{{ performance.includedFiles }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-section">
|
||||
<h2>Geladene Dateien</h2>
|
||||
<div class="admin-tools">
|
||||
<input type="text" id="fileFilter" placeholder="Dateien filtern..." class="search-input">
|
||||
</div>
|
||||
|
||||
<div class="file-list" id="fileList">
|
||||
<for var="file" in="performance.files">
|
||||
<div class="file-item">
|
||||
{{ file }}
|
||||
</div>
|
||||
</for>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-footer">
|
||||
<p>© {{ current_year }} Framework Admin</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('fileFilter').addEventListener('input', function() {
|
||||
const filterValue = this.value.toLowerCase();
|
||||
const items = document.querySelectorAll('#fileList .file-item');
|
||||
|
||||
items.forEach(item => {
|
||||
const text = item.textContent.toLowerCase();
|
||||
item.style.display = text.includes(filterValue) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,94 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ title }}</title>
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div class="admin-header">
|
||||
<h1>Redis-Informationen</h1>
|
||||
</div>
|
||||
|
||||
<div class="admin-nav">
|
||||
<a href="/admin">Dashboard</a>
|
||||
<a href="/admin/routes">Routen</a>
|
||||
<a href="/admin/services">Dienste</a>
|
||||
<a href="/admin/environment">Umgebung</a>
|
||||
<a href="/admin/performance">Performance</a>
|
||||
<a href="/admin/redis" class="active">Redis</a>
|
||||
<a href="/admin/phpinfo">PHP Info</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-content">
|
||||
<div class="dashboard-stats">
|
||||
<div class="stat-box">
|
||||
<h3>Status</h3>
|
||||
<div class="stat-value status-connected">{{ redis.status }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Version</h3>
|
||||
<div class="stat-value">{{ redis.version }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Uptime</h3>
|
||||
<div class="stat-value">{{ redis.uptime }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Speicherverbrauch</h3>
|
||||
<div class="stat-value">{{ redis.memory }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Max. Speicherverbrauch</h3>
|
||||
<div class="stat-value">{{ redis.peak_memory }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Verbundene Clients</h3>
|
||||
<div class="stat-value">{{ redis.clients }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Anzahl Schlüssel</h3>
|
||||
<div class="stat-value">{{ redis.keys }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-section">
|
||||
<h2>Schlüssel (max. 50 angezeigt)</h2>
|
||||
<div class="admin-tools">
|
||||
<input type="text" id="keyFilter" placeholder="Schlüssel filtern..." class="search-input">
|
||||
</div>
|
||||
|
||||
<div class="key-list" id="keyList">
|
||||
<for var="key" in="redis.key_sample">
|
||||
<div class="key-item">
|
||||
{{ key }}
|
||||
</div>
|
||||
</for>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-footer">
|
||||
<p>© {{ current_year }} Framework Admin</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('keyFilter')?.addEventListener('input', function() {
|
||||
const filterValue = this.value.toLowerCase();
|
||||
const items = document.querySelectorAll('#keyList .key-item');
|
||||
|
||||
items.forEach(item => {
|
||||
const text = item.textContent.toLowerCase();
|
||||
item.style.display = text.includes(filterValue) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,69 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= $title ?></title>
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div class="admin-header">
|
||||
<h1>Routen-Übersicht</h1>
|
||||
</div>
|
||||
|
||||
<div class="admin-nav">
|
||||
<a href="/admin">Dashboard</a>
|
||||
<a href="/admin/routes" class="active">Routen</a>
|
||||
<a href="/admin/services">Dienste</a>
|
||||
<a href="/admin/environment">Umgebung</a>
|
||||
<a href="/admin/performance">Performance</a>
|
||||
<a href="/admin/redis">Redis</a>
|
||||
<a href="/admin/phpinfo">PHP Info</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-content">
|
||||
<div class="admin-tools">
|
||||
<input type="text" id="routeFilter" placeholder="Routen filtern..." class="search-input">
|
||||
</div>
|
||||
|
||||
<table class="admin-table" id="routesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Pfad</th>
|
||||
<th>Methode</th>
|
||||
<th>Controller</th>
|
||||
<th>Aktion</th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($routes as $route): ?>
|
||||
<tr>
|
||||
<td><?= $route->path ?></td>
|
||||
<td><?= $route->method ?></td>
|
||||
<td><?= $route->controllerClass ?></td>
|
||||
<td><?= $route->methodName ?></td>
|
||||
<td><?= $route->name ?? '-' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="admin-footer">
|
||||
<p>© <?= date('Y') ?> Framework Admin</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('routeFilter').addEventListener('input', function() {
|
||||
const filterValue = this.value.toLowerCase();
|
||||
const rows = document.querySelectorAll('#routesTable tbody tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filterValue) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,67 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ title }}</title>
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div class="admin-header">
|
||||
<h1>Registrierte Dienste</h1>
|
||||
</div>
|
||||
|
||||
<div class="admin-nav">
|
||||
<a href="/admin">Dashboard</a>
|
||||
<a href="/admin/routes">Routen</a>
|
||||
<a href="/admin/services" class="active">Dienste</a>
|
||||
<a href="/admin/environment">Umgebung</a>
|
||||
<a href="/admin/performance">Performance</a>
|
||||
<a href="/admin/redis">Redis</a>
|
||||
<a href="/admin/phpinfo">PHP Info</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-content">
|
||||
<div class="admin-tools">
|
||||
<input type="text" id="serviceFilter" placeholder="Dienste filtern..." class="search-input">
|
||||
<span class="services-count">{{ servicesCount }} Dienste insgesamt</span>
|
||||
</div>
|
||||
|
||||
<div class="service-list" id="serviceList">
|
||||
<for var="service" in="services">
|
||||
<div class="service-item">
|
||||
<div class="service-name">{{ service.name }}</div>
|
||||
<div class="service-category">
|
||||
<span class="category-badge">{{ service.category }}</span>
|
||||
<if condition="service.subCategory">
|
||||
<span class="subcategory-badge">{{ service.subCategory }}</span>
|
||||
</if>
|
||||
</div>
|
||||
</div>
|
||||
</for>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-footer">
|
||||
<p>© {{ date('Y') }} Framework Admin</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('serviceFilter').addEventListener('input', function() {
|
||||
const filterValue = this.value.toLowerCase();
|
||||
const items = document.querySelectorAll('#serviceList .service-item');
|
||||
let visibleCount = 0;
|
||||
|
||||
items.forEach(item => {
|
||||
const text = item.textContent.toLowerCase();
|
||||
const isVisible = text.includes(filterValue);
|
||||
item.style.display = isVisible ? '' : 'none';
|
||||
if (isVisible) visibleCount++;
|
||||
});
|
||||
|
||||
document.querySelector('.services-count').textContent =
|
||||
visibleCount + ' von ' + {{ servicesCount }} + ' Diensten';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
183
src/Framework/DI/ContainerIntrospector.php
Normal file
183
src/Framework/DI/ContainerIntrospector.php
Normal file
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\DI;
|
||||
|
||||
use App\Framework\Core\ValueObjects\ClassName;
|
||||
use App\Framework\Reflection\ReflectionProvider;
|
||||
|
||||
final readonly class ContainerIntrospector
|
||||
{
|
||||
public function __construct(
|
||||
private Container $container,
|
||||
private InstanceRegistry $instances,
|
||||
private BindingRegistry $bindings,
|
||||
private ReflectionProvider $reflectionProvider,
|
||||
private \Closure $resolutionChainProvider
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function listBindings(): array
|
||||
{
|
||||
return array_keys($this->bindings->getAllBindings());
|
||||
}
|
||||
|
||||
public function getBinding(string $abstract): callable|string|object|null
|
||||
{
|
||||
return $this->bindings->getBinding($abstract);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function listSingletons(): array
|
||||
{
|
||||
return $this->instances->getSingletons();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function listInstances(): array
|
||||
{
|
||||
return $this->instances->getInstanceKeys();
|
||||
}
|
||||
|
||||
/** @param class-string $class */
|
||||
public function isSingleton(string $class): bool
|
||||
{
|
||||
return $this->instances->isMarkedAsSingleton($class) || $this->instances->hasSingleton($class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<class-string>
|
||||
*/
|
||||
public function getResolutionChain(): array
|
||||
{
|
||||
$f = $this->resolutionChainProvider;
|
||||
|
||||
/** @var array<class-string> $chain */
|
||||
$chain = $f();
|
||||
return $chain;
|
||||
}
|
||||
|
||||
/** @param class-string $class */
|
||||
public function isInstantiable(string $class): bool
|
||||
{
|
||||
if ($class === '') {
|
||||
return false;
|
||||
}
|
||||
$className = ClassName::create($class);
|
||||
if (! $className->exists()) {
|
||||
return false;
|
||||
}
|
||||
return $this->reflectionProvider->getClass($className)->isInstantiable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe resolution state and constructor parameters for diagnostics.
|
||||
* @param class-string $class
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function describe(string $class): array
|
||||
{
|
||||
$className = ClassName::create($class);
|
||||
$exists = $className->exists();
|
||||
$hasBinding = $this->bindings->hasBinding($class);
|
||||
$hasInstance = $this->instances->hasInstance($class) || $this->instances->hasSingleton($class);
|
||||
$singletonMarked = $this->instances->isMarkedAsSingleton($class);
|
||||
|
||||
$instantiable = false;
|
||||
$constructor = [
|
||||
'has_constructor' => false,
|
||||
'parameters' => [],
|
||||
];
|
||||
|
||||
$binding = $this->bindings->getBinding($class);
|
||||
$bindingType = null;
|
||||
if ($binding !== null) {
|
||||
$bindingType = is_callable($binding) ? 'callable' : (is_string($binding) ? 'string' : 'object');
|
||||
}
|
||||
|
||||
if ($exists) {
|
||||
try {
|
||||
$reflection = $this->reflectionProvider->getClass($className);
|
||||
$instantiable = $reflection->isInstantiable();
|
||||
if ($reflection->hasMethod('__construct')) {
|
||||
$ctor = $reflection->getConstructor();
|
||||
if ($ctor !== null) {
|
||||
$constructor['has_constructor'] = true;
|
||||
foreach ($ctor->getParameters() as $param) {
|
||||
$type = $param->getType();
|
||||
$typeName = null;
|
||||
$isBuiltin = false;
|
||||
if ($type instanceof \ReflectionNamedType) {
|
||||
$typeName = $type->getName();
|
||||
$isBuiltin = $type->isBuiltin();
|
||||
} elseif ($type !== null) {
|
||||
// union or complex type - string cast
|
||||
$typeName = (string) $type;
|
||||
}
|
||||
|
||||
$resolvable = true;
|
||||
if ($typeName !== null && ! $isBuiltin) {
|
||||
// best-effort check for class/interface
|
||||
$resolvable = $this->container->has($typeName);
|
||||
}
|
||||
|
||||
$constructor['parameters'][] = [
|
||||
'name' => $param->getName(),
|
||||
'type' => $typeName,
|
||||
'allows_null' => $type?->allowsNull() ?? true,
|
||||
'is_builtin' => $isBuiltin,
|
||||
'has_default' => $param->isDefaultValueAvailable(),
|
||||
'resolvable' => $resolvable,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Keep defaults if reflection fails, but include error message for diagnostics.
|
||||
$constructor['error'] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$suggestions = [];
|
||||
if (! $exists) {
|
||||
$suggestions[] = 'Class does not exist - check namespace and autoloading.';
|
||||
} elseif (! $instantiable && ! $hasBinding) {
|
||||
$suggestions[] = 'Class is not instantiable - add a binding from interface/abstract to a concrete implementation.';
|
||||
}
|
||||
if (! $hasBinding && $instantiable && ($constructor['has_constructor'] ?? false)) {
|
||||
foreach ($constructor['parameters'] as $p) {
|
||||
if ($p['type'] && ! $p['is_builtin'] && ! $p['resolvable']) {
|
||||
$suggestions[] = "Add binding for dependency '{$p['type']}' or ensure it is instantiable.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$chain = $this->getResolutionChain();
|
||||
|
||||
return [
|
||||
'class' => $class,
|
||||
'exists' => $exists,
|
||||
'instantiable' => $instantiable,
|
||||
'has_binding' => $hasBinding,
|
||||
'binding_type' => $bindingType,
|
||||
'has_instance' => $hasInstance,
|
||||
'singleton_marked' => $singletonMarked,
|
||||
'constructor' => $constructor,
|
||||
'resolution_chain' => $chain,
|
||||
'counts' => [
|
||||
'bindings' => count($this->bindings->getAllBindings()),
|
||||
'singletons' => count($this->instances->getSingletons()),
|
||||
'instances' => count($this->instances->getInstanceKeys()),
|
||||
],
|
||||
'suggestions' => array_values(array_unique($suggestions)),
|
||||
];
|
||||
}
|
||||
}
|
||||
37
src/Framework/Mcp/Tools/RouteInspectorTool.php
Normal file
37
src/Framework/Mcp/Tools/RouteInspectorTool.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Mcp\Tools;
|
||||
|
||||
use App\Framework\Mcp\McpTool;
|
||||
use App\Framework\Router\CompiledRoutes;
|
||||
use App\Framework\Router\RouteInspector;
|
||||
|
||||
/**
|
||||
* MCP tool exposing routing sanity checks
|
||||
*/
|
||||
final readonly class RouteInspectorTool
|
||||
{
|
||||
public function __construct(
|
||||
private CompiledRoutes $compiledRoutes
|
||||
) {
|
||||
}
|
||||
|
||||
#[McpTool(
|
||||
name: 'route_sanity_check',
|
||||
description: 'Analyze compiled routes for common issues (missing controllers/actions, parameter mismatches, duplicates)'
|
||||
)]
|
||||
public function routeSanityCheck(): array
|
||||
{
|
||||
try {
|
||||
$inspector = new RouteInspector($this->compiledRoutes);
|
||||
|
||||
return $inspector->analyze();
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
211
src/Framework/Router/RouteInspector.php
Normal file
211
src/Framework/Router/RouteInspector.php
Normal file
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Router;
|
||||
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
|
||||
/**
|
||||
* Performs sanity checks on compiled routes (controller/action presence, parameter consistency, etc.)
|
||||
*/
|
||||
final readonly class RouteInspector
|
||||
{
|
||||
public function __construct(
|
||||
private CompiledRoutes $compiledRoutes
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze compiled routes and return structured diagnostics
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function analyze(): array
|
||||
{
|
||||
$issues = [];
|
||||
$staticRoutes = $this->compiledRoutes->getStaticRoutes();
|
||||
$namedRoutes = $this->compiledRoutes->getAllNamedRoutes();
|
||||
|
||||
$totalStatic = 0;
|
||||
|
||||
// Track seen routes for potential duplicates per method+subdomain+path
|
||||
$seen = [];
|
||||
|
||||
foreach ($staticRoutes as $method => $subdomains) {
|
||||
foreach ($subdomains as $subdomain => $paths) {
|
||||
foreach ($paths as $path => $route) {
|
||||
$totalStatic++;
|
||||
$key = "{$method}|{$subdomain}|{$path}";
|
||||
$seen[$key] = ($seen[$key] ?? 0) + 1;
|
||||
|
||||
$routeName = $route->name ?? null;
|
||||
|
||||
// Controller existence
|
||||
$controller = $route->controller ?? null;
|
||||
$action = $route->action ?? null;
|
||||
|
||||
if (!is_string($controller) || $controller === '' || !class_exists($controller)) {
|
||||
$issues[] = $this->issue('controller_missing', 'error', $method, $subdomain, $path, $routeName, "Controller class not found or invalid: " . var_export($controller, true));
|
||||
continue; // skip further checks for this route
|
||||
}
|
||||
|
||||
// Action existence and visibility
|
||||
if (!is_string($action) || $action === '') {
|
||||
$issues[] = $this->issue('action_missing', 'error', $method, $subdomain, $path, $routeName, 'Action method not defined or invalid');
|
||||
} else {
|
||||
$refClass = new ReflectionClass($controller);
|
||||
if (!$refClass->hasMethod($action)) {
|
||||
$issues[] = $this->issue('action_missing', 'error', $method, $subdomain, $path, $routeName, "Action method '{$action}' not found in {$controller}");
|
||||
} else {
|
||||
$refMethod = $refClass->getMethod($action);
|
||||
if (!$refMethod->isPublic()) {
|
||||
$issues[] = $this->issue('action_not_public', 'warning', $method, $subdomain, $path, $routeName, "Action method '{$action}' is not public");
|
||||
}
|
||||
// Parameter consistency check (placeholders vs method signature)
|
||||
$this->checkParameterConsistency($issues, $method, $subdomain, $path, $routeName, $route, $refMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Duplicate path checks (should normally be prevented by map keys, but guard anyway)
|
||||
foreach ($seen as $k => $count) {
|
||||
if ($count > 1) {
|
||||
[$m, $sub, $p] = explode('|', $k, 3);
|
||||
$issues[] = $this->issue('duplicate_route', 'error', $m, $sub, $p, null, "Duplicate route detected for {$m} {$sub} {$p}");
|
||||
}
|
||||
}
|
||||
|
||||
// Named routes basic validation: ensure name -> route is consistent
|
||||
$namedIssues = $this->validateNamedRoutes($namedRoutes);
|
||||
array_push($issues, ...$namedIssues);
|
||||
|
||||
$summary = [
|
||||
'total_static_routes' => $totalStatic,
|
||||
'total_named_routes' => count($namedRoutes),
|
||||
'issue_count' => count($issues),
|
||||
];
|
||||
|
||||
return [
|
||||
'summary' => $summary,
|
||||
'issues' => $issues,
|
||||
'stats' => $this->compiledRoutes->getStats(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that route parameters in path are consistent with action method signature
|
||||
*/
|
||||
private function checkParameterConsistency(array &$issues, string $method, string $subdomain, string $path, ?string $routeName, object $route, ReflectionMethod $refMethod): void
|
||||
{
|
||||
$pathParams = $this->extractPathParams($path);
|
||||
|
||||
// Try to read expected parameters from route definition; otherwise from reflection
|
||||
$expected = [];
|
||||
if (isset($route->parameters) && is_array($route->parameters)) {
|
||||
// If associative, use keys; if list, use values
|
||||
$keys = array_keys($route->parameters);
|
||||
$expected = array_values(array_filter(
|
||||
count($keys) !== count($route->parameters) ? $route->parameters : $keys,
|
||||
fn($v) => is_string($v) && $v !== ''
|
||||
));
|
||||
} else {
|
||||
$expected = array_map(
|
||||
static fn(\ReflectionParameter $p) => $p->getName(),
|
||||
$refMethod->getParameters()
|
||||
);
|
||||
}
|
||||
|
||||
// Normalize unique sets
|
||||
$pathSet = array_values(array_unique($pathParams));
|
||||
$expectedSet = array_values(array_unique($expected));
|
||||
|
||||
// Missing placeholders in path for expected parameters
|
||||
$missingInPath = array_values(array_diff($expectedSet, $pathSet));
|
||||
if (!empty($missingInPath)) {
|
||||
$issues[] = $this->issue(
|
||||
'param_mismatch',
|
||||
'warning',
|
||||
$method,
|
||||
$subdomain,
|
||||
$path,
|
||||
$routeName,
|
||||
'Expected parameters not present in path: ' . implode(', ', $missingInPath)
|
||||
);
|
||||
}
|
||||
|
||||
// Extra placeholders not expected by the action
|
||||
$extraInPath = array_values(array_diff($pathSet, $expectedSet));
|
||||
if (!empty($extraInPath)) {
|
||||
$issues[] = $this->issue(
|
||||
'param_mismatch',
|
||||
'warning',
|
||||
$method,
|
||||
$subdomain,
|
||||
$path,
|
||||
$routeName,
|
||||
'Path has placeholders not expected by action: ' . implode(', ', $extraInPath)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate named routes (basic structural checks)
|
||||
* @param array<string, object> $namedRoutes
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function validateNamedRoutes(array $namedRoutes): array
|
||||
{
|
||||
$issues = [];
|
||||
foreach ($namedRoutes as $name => $route) {
|
||||
// Minimal: ensure a path exists
|
||||
$path = $route->path ?? null;
|
||||
if (!is_string($path) || $path === '') {
|
||||
$issues[] = [
|
||||
'type' => 'invalid_named_route',
|
||||
'severity' => 'error',
|
||||
'route_name' => $name,
|
||||
'message' => 'Named route has no valid path',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract placeholders from route path like /users/{id}/posts/{slug}
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function extractPathParams(string $path): array
|
||||
{
|
||||
$matches = [];
|
||||
preg_match_all('/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/', $path, $matches);
|
||||
|
||||
/** @var array<int, string> $params */
|
||||
$params = $matches[1] ?? [];
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a standardized issue array
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function issue(string $type, string $severity, string $method, string $subdomain, string $path, ?string $name, string $message): array
|
||||
{
|
||||
return [
|
||||
'type' => $type,
|
||||
'severity' => $severity,
|
||||
'route' => [
|
||||
'method' => $method,
|
||||
'subdomain' => $subdomain,
|
||||
'path' => $path,
|
||||
'name' => $name,
|
||||
],
|
||||
'message' => $message,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user