feat(monitoring): add Redis exporter + Sentry docs to deployment guide
Some checks failed
CI / ruff (push) Successful in 10s
CI / pytest (push) Failing after 47m30s
CI / validate (push) Successful in 24s
CI / dependency-scanning (push) Successful in 29s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped

- Add redis-exporter container to docker-compose (oliver006/redis_exporter, 32MB)
- Add Redis scrape target to Prometheus config
- Add 4 Redis alert rules: RedisDown, HighMemory, HighConnections, RejectedConnections
- Document Step 19b (Sentry Error Tracking) in Hetzner deployment guide
- Document Step 19c (Redis Monitoring) in Hetzner deployment guide
- Update resource budget and port reference tables

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 23:30:18 +01:00
parent ce822af883
commit 35d1559162
54 changed files with 664 additions and 343 deletions

View File

@@ -173,11 +173,11 @@ Example:
return {
loading: false,
stats: {},
async init() {
await this.loadStats();
},
async loadStats() {
this.loading = true;
try {

View File

@@ -55,7 +55,7 @@ app/
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
[Page Name]
</h2>
<!-- Action Buttons -->
<div class="flex items-center space-x-3">
<button
@@ -67,7 +67,7 @@ app/
<span x-show="loading" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
<span x-text="loading ? 'Loading...' : 'Refresh'"></span>
</button>
<button
@click="openCreateModal()"
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"
@@ -89,7 +89,7 @@ app/
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- ERROR STATE -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div x-show="error && !loading"
<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>
@@ -115,7 +115,7 @@ app/
/>
</label>
</div>
<!-- Status Filter -->
<div>
<label class="block text-sm">
@@ -131,7 +131,7 @@ app/
</select>
</label>
</div>
<!-- Sort -->
<div>
<label class="block text-sm">
@@ -185,15 +185,15 @@ app/
<div class="flex items-center text-sm">
<div>
<p class="font-semibold" x-text="item.name"></p>
<p class="text-xs text-gray-600 dark:text-gray-400"
<p class="text-xs text-gray-600 dark:text-gray-400"
x-text="item.description"></p>
</div>
</div>
</td>
<td class="px-4 py-3 text-xs">
<span class="px-2 py-1 font-semibold leading-tight rounded-full"
:class="item.status === 'active'
? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100'
:class="item.status === 'active'
? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100'
: 'text-gray-700 bg-gray-100 dark:text-gray-100 dark:bg-gray-700'"
x-text="item.status"></span>
</td>
@@ -229,7 +229,7 @@ app/
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="px-4 py-3 text-xs font-semibold tracking-wide text-gray-500 uppercase border-t dark:border-gray-700 bg-gray-50 sm:grid-cols-9 dark:text-gray-400 dark:bg-gray-800">
<span class="flex items-center col-span-3">
@@ -272,21 +272,21 @@ app/
<!-- MODALS (if needed) -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- Create/Edit Modal -->
<div x-show="showModal"
<div x-show="showModal"
x-cloak
class="fixed inset-0 z-50 flex items-center justify-center overflow-auto bg-black bg-opacity-50"
@click.self="closeModal()">
<div class="relative w-full max-w-lg p-6 mx-auto bg-white rounded-lg shadow-xl dark:bg-gray-800">
<!-- Modal Header -->
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200"
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200"
x-text="modalTitle"></h3>
<button @click="closeModal()"
<button @click="closeModal()"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<span x-html="$icon('x', 'w-6 h-6')"></span>
</button>
</div>
<!-- Modal Body -->
<form @submit.prevent="saveItem()">
<div class="space-y-4">
@@ -303,7 +303,7 @@ app/
</label>
</div>
</div>
<!-- Modal Footer -->
<div class="flex justify-end mt-6 space-x-3">
<button
@@ -414,7 +414,7 @@ function store[PageName]() {
await this.loadData();
store[PageName]Log.info('[PageName] page initialized');
},
// ═══════════════════════════════════════════════════════════
// DATA LOADING
// ═══════════════════════════════════════════════════════════
@@ -453,11 +453,11 @@ function store[PageName]() {
this.loading = false;
}
},
async refresh() {
await this.loadData();
},
updatePagination(response) {
this.pagination = {
currentPage: response.page || 1,
@@ -470,7 +470,7 @@ function store[PageName]() {
hasNext: response.page < response.pages
};
},
// ═══════════════════════════════════════════════════════════
// FILTERING & PAGINATION
// ═══════════════════════════════════════════════════════════
@@ -478,21 +478,21 @@ function store[PageName]() {
this.pagination.currentPage = 1; // Reset to first page
await this.loadData();
},
async previousPage() {
if (this.pagination.hasPrevious) {
this.pagination.currentPage--;
await this.loadData();
}
},
async nextPage() {
if (this.pagination.hasNext) {
this.pagination.currentPage++;
await this.loadData();
}
},
// ═══════════════════════════════════════════════════════════
// CRUD OPERATIONS
// ═══════════════════════════════════════════════════════════
@@ -506,12 +506,12 @@ function store[PageName]() {
};
this.showModal = true;
},
async viewItem(id) {
// Navigate to detail page or open view modal
window.location.href = `/store/${this.storeCode}/[endpoint]/${id}`;
},
async editItem(id) {
try {
// Load item data
@@ -577,12 +577,12 @@ function store[PageName]() {
alert(error.message || 'Failed to delete item');
}
},
closeModal() {
this.showModal = false;
this.formData = {};
},
// ═══════════════════════════════════════════════════════════
// UTILITIES
// ═══════════════════════════════════════════════════════════
@@ -595,7 +595,7 @@ function store[PageName]() {
day: 'numeric'
});
},
formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
@@ -1024,7 +1024,7 @@ async startImport() {
this.error = 'Please enter a CSV URL';
return;
}
this.importing = true;
try {
const response = await apiClient.post('/store/marketplace/import', {
@@ -1032,7 +1032,7 @@ async startImport() {
marketplace: this.importForm.marketplace,
batch_size: this.importForm.batch_size
});
this.successMessage = `Import job #${response.job_id} started!`;
await this.loadJobs(); // Refresh list
} catch (error) {
@@ -1050,7 +1050,7 @@ startAutoRefresh() {
const hasActiveJobs = this.jobs.some(job =>
job.status === 'pending' || job.status === 'processing'
);
if (hasActiveJobs) {
await this.loadJobs();
}
@@ -1077,7 +1077,7 @@ quickFill(language) {
'en': this.storeSettings.letzshop_csv_url_en,
'de': this.storeSettings.letzshop_csv_url_de
};
if (urlMap[language]) {
this.importForm.csv_url = urlMap[language];
this.importForm.language = language;
@@ -1124,15 +1124,15 @@ formatDate(dateString) {
calculateDuration(job) {
if (!job.started_at) return 'Not started';
const start = new Date(job.started_at);
const end = job.completed_at ? new Date(job.completed_at) : new Date();
const durationMs = end - start;
const seconds = Math.floor(durationMs / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}m`;
} else if (minutes > 0) {
@@ -1190,4 +1190,3 @@ calculateDuration(job) {
- [Marketplace Integration Guide](../../guides/marketplace-integration.md) - Complete marketplace system documentation
- [Admin Page Templates](../admin/page-templates.md) - Admin page patterns
- [Icons Guide](../../development/icons-guide.md) - Available icons