feat(monitoring): add Redis exporter + Sentry docs to deployment guide
Some checks failed
Some checks failed
- 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:
@@ -173,11 +173,11 @@ Example:
|
||||
return {
|
||||
loading: false,
|
||||
stats: {},
|
||||
|
||||
|
||||
async init() {
|
||||
await this.loadStats();
|
||||
},
|
||||
|
||||
|
||||
async loadStats() {
|
||||
this.loading = true;
|
||||
try {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user