Files
TurboTrades/frontend/src/components/AdminDebugPanel.vue
iDefineHD 63c578b0ae feat: Complete admin panel implementation
- Add user management system with all CRUD operations
- Add promotion statistics dashboard with export
- Simplify Trading & Market settings UI
- Fix promotion schema (dates now optional)
- Add missing API endpoints and PATCH support
- Add comprehensive documentation
- Fix critical bugs (deletePromotion, duplicate endpoints)

All features tested and production-ready.
2026-01-10 21:57:55 +00:00

1297 lines
32 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="admin-debug-panel">
<!-- Confirm Delete Modal -->
<ConfirmModal
v-model="showDeleteModal"
title="Delete Announcement"
:message="`Are you sure you want to delete this announcement?`"
:details="announcementToDelete?.message"
confirm-text="Delete"
cancel-text="Cancel"
type="danger"
:loading="deleting"
@confirm="confirmDelete"
/>
<div class="debug-header">
<h2>🔧 Admin Debug Panel</h2>
<button @click="runAllTests" class="btn-test" :disabled="testing">
<Loader2 v-if="testing" class="animate-spin" />
<span>{{ testing ? "Testing..." : "Run All Tests" }}</span>
</button>
</div>
<!-- Tab Navigation -->
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
@click="activeTab = tab.id"
class="tab"
:class="{ active: activeTab === tab.id }"
>
{{ tab.label }}
</button>
</div>
<!-- Authentication Tab -->
<div v-if="activeTab === 'auth'" class="tab-content">
<div class="debug-section">
<h3>Authentication Status</h3>
<div class="status-grid">
<div class="status-item">
<span class="label">Logged In:</span>
<span
class="value"
:class="authStore.isAuthenticated ? 'success' : 'error'"
>
{{ authStore.isAuthenticated ? "✓ Yes" : "✗ No" }}
</span>
</div>
<div class="status-item">
<span class="label">Username:</span>
<span class="value">{{ authStore.username || "N/A" }}</span>
</div>
<div class="status-item">
<span class="label">Steam ID:</span>
<span class="value">{{ authStore.steamId || "N/A" }}</span>
</div>
<div class="status-item">
<span class="label">Staff Level:</span>
<span class="value" :class="getStaffLevelClass()">
{{ authStore.staffLevel }}
</span>
</div>
<div class="status-item">
<span class="label">Is Admin:</span>
<span
class="value"
:class="authStore.isAdmin ? 'success' : 'error'"
>
{{ authStore.isAdmin ? "✓ Yes" : "✗ No" }}
</span>
</div>
<div class="status-item">
<span class="label">Balance:</span>
<span class="value">${{ authStore.balance.toFixed(2) }}</span>
</div>
</div>
</div>
<div class="debug-section">
<h3>Backend Connectivity</h3>
<div class="test-results">
<div v-for="test in tests" :key="test.name" class="test-item">
<div class="test-header">
<span class="test-name">{{ test.name }}</span>
<span class="test-status" :class="test.status">
<Loader2
v-if="test.status === 'running'"
class="animate-spin"
/>
<CheckCircle v-else-if="test.status === 'success'" />
<XCircle v-else-if="test.status === 'error'" />
<Clock v-else />
</span>
</div>
<div v-if="test.message" class="test-message" :class="test.status">
{{ test.message }}
</div>
<div v-if="test.details" class="test-details">
<pre>{{ JSON.stringify(test.details, null, 2) }}</pre>
</div>
</div>
</div>
</div>
</div>
<!-- Announcements Tab -->
<div v-if="activeTab === 'announcements'" class="tab-content">
<div class="debug-section">
<div class="section-header">
<h3>📢 Announcements Testing</h3>
<button @click="loadAnnouncements" class="btn-action">
<RefreshCw class="icon" />
Reload
</button>
</div>
<div class="status-grid">
<div class="status-item">
<span class="label">Total Loaded:</span>
<span class="value">{{ announcements.length }}</span>
</div>
<div class="status-item">
<span class="label">Active:</span>
<span class="value success">{{ activeAnnouncements.length }}</span>
</div>
<div class="status-item">
<span class="label">Dismissed:</span>
<span class="value">{{ dismissedIds.length }}</span>
</div>
</div>
<div class="actions-row">
<button @click="clearDismissed" class="btn-action">
<Trash2 class="icon" />
Clear Dismissed
</button>
<button @click="testAnnouncementAPI" class="btn-action">
<TestTube class="icon" />
Test API
</button>
</div>
<div v-if="announcements.length === 0" class="empty-state">
No announcements loaded. Check API or create one in the admin
panel.
</div>
<div v-else class="announcement-list">
<div
v-for="announcement in announcements"
:key="announcement.id"
class="announcement-debug-item"
:class="`type-${announcement.type}`"
>
<div class="announcement-row">
<span class="announcement-type">{{ announcement.type }}</span>
<span class="announcement-enabled">
{{ announcement.enabled ? "✅ Enabled" : "❌ Disabled" }}
</span>
<span class="announcement-dismissible">
{{
announcement.dismissible ? "👋 Dismissible" : "🔒 Permanent"
}}
</span>
<button
@click="deleteAnnouncement(announcement)"
class="btn-delete-inline"
title="Delete announcement"
>
<Trash2 :size="16" />
</button>
</div>
<div class="announcement-message">{{ announcement.message }}</div>
<div class="announcement-meta">
<span>ID: {{ announcement.id.substring(0, 8) }}...</span>
<span v-if="announcement.startDate"
>Start: {{ formatDate(announcement.startDate) }}</span
>
<span v-if="announcement.endDate"
>End: {{ formatDate(announcement.endDate) }}</span
>
<span
v-if="dismissedIds.includes(announcement.id)"
class="dismissed-badge"
>DISMISSED</span
>
</div>
</div>
</div>
</div>
<div v-if="apiResponse" class="debug-section">
<h3>Last API Response</h3>
<div class="test-details">
<pre>{{ JSON.stringify(apiResponse, null, 2) }}</pre>
</div>
</div>
</div>
<!-- Maintenance Tab -->
<div v-if="activeTab === 'maintenance'" class="tab-content">
<div class="debug-section">
<div class="section-header">
<h3>🔧 Maintenance Mode Testing</h3>
<button @click="loadMaintenanceStatus" class="btn-action">
<RefreshCw class="icon" />
Reload
</button>
</div>
<div v-if="maintenanceInfo" class="maintenance-status">
<div
class="status-card"
:class="maintenanceInfo.enabled ? 'warning' : 'success'"
>
<div class="status-icon">
{{ maintenanceInfo.enabled ? "⚠️" : "✅" }}
</div>
<div class="status-content">
<h4>
{{
maintenanceInfo.enabled
? "MAINTENANCE MODE ACTIVE"
: "Site Operational"
}}
</h4>
<p>{{ maintenanceInfo.message }}</p>
</div>
</div>
<div class="status-grid">
<div class="status-item">
<span class="label">Maintenance Enabled:</span>
<span
class="value"
:class="maintenanceInfo.enabled ? 'error' : 'success'"
>
{{ maintenanceInfo.enabled ? "Yes" : "No" }}
</span>
</div>
<div class="status-item">
<span class="label">Scheduled End:</span>
<span class="value">
{{
maintenanceInfo.scheduledEnd
? formatDate(maintenanceInfo.scheduledEnd)
: "None"
}}
</span>
</div>
<div class="status-item">
<span class="label">Whitelisted Users:</span>
<span class="value">{{
maintenanceInfo.allowedSteamIds?.length || 0
}}</span>
</div>
</div>
<div class="actions-row">
<button @click="testMaintenanceBypass" class="btn-action">
<Shield class="icon" />
Test Admin Bypass
</button>
<button @click="testMaintenanceBlock" class="btn-action">
<XCircle class="icon" />
Test User Block
</button>
</div>
</div>
</div>
<div v-if="maintenanceTests.length > 0" class="debug-section">
<h3>Test Results</h3>
<div class="test-results">
<div
v-for="test in maintenanceTests"
:key="test.name"
class="test-item"
>
<div class="test-header">
<span class="test-name">{{ test.name }}</span>
<span class="test-status" :class="test.status">
<CheckCircle v-if="test.status === 'success'" />
<XCircle v-else-if="test.status === 'error'" />
<Clock v-else />
</span>
</div>
<div v-if="test.message" class="test-message" :class="test.status">
{{ test.message }}
</div>
</div>
</div>
</div>
</div>
<!-- System Tab -->
<div v-if="activeTab === 'system'" class="tab-content"></div>
<div class="debug-section">
<h3>Environment Info</h3>
<div class="info-grid">
<div class="info-item">
<span class="label">API Base URL:</span>
<span class="value">{{ apiBaseUrl }}</span>
</div>
<div class="info-item">
<span class="label">Current Route:</span>
<span class="value">{{ currentRoute }}</span>
</div>
<div class="info-item">
<span class="label">Window Location:</span>
<span class="value">{{ windowLocation }}</span>
</div>
<div class="info-item">
<span class="label">User Agent:</span>
<span class="value small">{{ userAgent }}</span>
</div>
</div>
</div>
<div class="debug-section">
<h3>Quick Actions</h3>
<div class="actions-grid">
<button @click="refreshAuth" class="btn-action">
<RefreshCw class="icon" />
Refresh Auth
</button>
<button @click="clearCache" class="btn-action">
<Trash2 class="icon" />
Clear Cache
</button>
<button @click="testAdminRoute" class="btn-action">
<Shield class="icon" />
Test Admin Route
</button>
<button @click="copyDebugInfo" class="btn-action">
<Copy class="icon" />
Copy Debug Info
</button>
</div>
</div>
<div v-if="errorLog.length > 0" class="debug-section">
<h3>Error Log</h3>
<div class="error-log">
<div v-for="(error, index) in errorLog" :key="index" class="error-item">
<span class="error-time">{{ error.time }}</span>
<span class="error-message">{{ error.message }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from "vue";
import { useRouter } from "vue-router";
import { useAuthStore } from "@/stores/auth";
import { useToast } from "vue-toastification";
import axios from "@/utils/axios";
import ConfirmModal from "@/components/ConfirmModal.vue";
import {
Loader2,
CheckCircle,
XCircle,
Clock,
RefreshCw,
Trash2,
Shield,
Copy,
TestTube,
} from "lucide-vue-next";
const router = useRouter();
const authStore = useAuthStore();
const toast = useToast();
const testing = ref(false);
const errorLog = ref([]);
const activeTab = ref("auth");
const announcements = ref([]);
const dismissedIds = ref([]);
const apiResponse = ref(null);
const maintenanceInfo = ref(null);
const maintenanceTests = ref([]);
const showDeleteModal = ref(false);
const announcementToDelete = ref(null);
const deleting = ref(false);
const tabs = [
{ id: "auth", label: "🔐 Auth & Connectivity" },
{ id: "announcements", label: "📢 Announcements" },
{ id: "maintenance", label: "🔧 Maintenance" },
{ id: "system", label: "⚙️ System" },
];
const tests = ref([
{ name: "Health Check", status: "pending", message: "", details: null },
{ name: "Auth Check", status: "pending", message: "", details: null },
{
name: "Admin Config Access",
status: "pending",
message: "",
details: null,
},
{
name: "Admin Routes Access",
status: "pending",
message: "",
details: null,
},
]);
const apiBaseUrl = computed(() => {
return import.meta.env.VITE_API_URL || "/api";
});
const currentRoute = computed(() => router.currentRoute.value.path);
const windowLocation = computed(() => window.location.href);
const userAgent = computed(() => navigator.userAgent);
const activeAnnouncements = computed(() => {
return announcements.value.filter((announcement) => {
if (dismissedIds.value.includes(announcement.id)) return false;
if (!announcement.enabled) return false;
const now = new Date();
if (announcement.startDate && now < new Date(announcement.startDate))
return false;
if (announcement.endDate && now > new Date(announcement.endDate))
return false;
return true;
});
});
const getStaffLevelClass = () => {
const level = authStore.staffLevel;
if (level >= 5) return "admin-super";
if (level >= 3) return "admin";
if (level >= 2) return "moderator";
if (level >= 1) return "staff";
return "user";
};
const logError = (message) => {
const now = new Date();
errorLog.value.unshift({
time: now.toLocaleTimeString(),
message: message,
});
if (errorLog.value.length > 10) {
errorLog.value.pop();
}
};
const formatDate = (date) => {
if (!date) return "N/A";
return new Date(date).toLocaleString("en-US", {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
});
};
// Announcements functions
const loadAnnouncements = async () => {
try {
console.log("📡 Debug: Fetching announcements...");
const response = await axios.get("/api/config/announcements");
console.log("📡 Debug: Response:", response);
apiResponse.value = response.data;
if (response.data.success) {
announcements.value = response.data.announcements;
console.log("✅ Debug: Loaded announcements:", announcements.value);
toast.success(`Loaded ${announcements.value.length} announcements`);
}
} catch (error) {
console.error("❌ Debug: Failed to load announcements:", error);
toast.error("Failed to load announcements");
logError("Load announcements: " + error.message);
}
};
const clearDismissed = () => {
dismissedIds.value = [];
localStorage.removeItem("dismissedAnnouncements");
toast.success("Cleared dismissed announcements");
console.log("🗑️ Debug: Cleared dismissed");
};
const testAnnouncementAPI = async () => {
try {
const response = await fetch(
"http://localhost:3000/api/config/announcements"
);
const data = await response.json();
apiResponse.value = { direct: true, ...data };
toast.success("Direct API test successful");
console.log("🧪 Debug: Direct fetch result:", data);
} catch (error) {
apiResponse.value = { error: error.message };
toast.error("Direct API test failed");
console.error("🧪 Debug: Direct fetch error:", error);
}
};
// Maintenance functions
const loadMaintenanceStatus = async () => {
try {
const response = await axios.get("/api/config/public");
if (response.data.success) {
maintenanceInfo.value = response.data.config.maintenance;
toast.success("Maintenance status loaded");
}
} catch (error) {
toast.error("Failed to load maintenance status");
logError("Load maintenance: " + error.message);
}
};
const testMaintenanceBypass = async () => {
maintenanceTests.value = [];
try {
const response = await axios.get("/api/admin/config");
maintenanceTests.value.push({
name: "Admin Bypass Test",
status: "success",
message: "✅ Admin can access during maintenance",
});
toast.success("Admin bypass working!");
} catch (error) {
maintenanceTests.value.push({
name: "Admin Bypass Test",
status: "error",
message:
"❌ Admin blocked: " + (error.response?.data?.message || error.message),
});
toast.error("Admin bypass failed!");
}
};
const testMaintenanceBlock = () => {
maintenanceTests.value.push({
name: "User Block Test",
status: "success",
message: " Test in incognito window to verify non-admin blocking",
});
toast.info("Open incognito window to test user blocking");
};
// Delete announcement
const deleteAnnouncement = (announcement) => {
announcementToDelete.value = announcement;
showDeleteModal.value = true;
};
const confirmDelete = async () => {
if (!announcementToDelete.value) return;
deleting.value = true;
try {
const announcementId = announcementToDelete.value.id;
const deleteUrl = `/api/admin/announcements/${announcementId}`;
console.log("🗑️ Deleting announcement ID:", announcementId);
console.log("🗑️ Delete URL:", deleteUrl);
console.log(
"🗑️ Axios instance baseURL:",
import.meta.env.VITE_API_URL || "/api"
);
const response = await axios.delete(deleteUrl);
console.log("✅ Delete response:", response.data);
if (response.data.success) {
toast.success("Announcement deleted successfully");
showDeleteModal.value = false;
announcementToDelete.value = null;
// Reload announcements
await loadAnnouncements();
} else {
toast.error(response.data.message || "Failed to delete announcement");
}
} catch (error) {
console.error("❌ Failed to delete announcement:", error);
console.error("❌ Error response:", error.response);
console.error("❌ Error config:", error.config);
const errorMessage =
error.response?.data?.message ||
error.message ||
"Failed to delete announcement";
toast.error(errorMessage);
logError("Delete announcement: " + errorMessage);
} finally {
deleting.value = false;
}
};
const runAllTests = async () => {
testing.value = true;
// Reset all tests
tests.value.forEach((test) => {
test.status = "pending";
test.message = "";
test.details = null;
});
// Test 1: Health Check
tests.value[0].status = "running";
try {
// Use relative path - will go through Vite proxy in dev
const response = await axios.get("/api/health", {
timeout: 5000,
});
tests.value[0].status = "success";
tests.value[0].message = "Backend is running";
tests.value[0].details = response.data;
} catch (error) {
tests.value[0].status = "error";
tests.value[0].message = error.message || "Backend not accessible";
logError("Health check failed: " + error.message);
}
// Test 2: Auth Check
await new Promise((resolve) => setTimeout(resolve, 500));
tests.value[1].status = "running";
try {
const response = await axios.get("/api/auth/me");
tests.value[1].status = "success";
tests.value[1].message = `Authenticated as ${response.data.user.username}`;
tests.value[1].details = {
steamId: response.data.user.steamId,
staffLevel: response.data.user.staffLevel,
isAdmin: response.data.user.staffLevel >= 3,
};
} catch (error) {
tests.value[1].status = "error";
tests.value[1].message = "Not authenticated or session expired";
tests.value[1].details = {
error: error.response?.data?.message || error.message,
};
logError("Auth check failed: " + error.message);
}
// Test 3: Admin Config Access
await new Promise((resolve) => setTimeout(resolve, 500));
tests.value[2].status = "running";
try {
const response = await axios.get("/api/admin/config");
tests.value[2].status = "success";
tests.value[2].message = "Admin config accessible";
tests.value[2].details = {
hasConfig: !!response.data.config,
maintenance: response.data.config?.maintenance?.enabled,
trading: response.data.config?.trading?.enabled,
};
} catch (error) {
tests.value[2].status = "error";
tests.value[2].message =
error.response?.data?.message || "Access denied or route not found";
tests.value[2].details = {
status: error.response?.status,
error: error.response?.data?.message || error.message,
};
logError("Admin config access failed: " + error.message);
}
// Test 4: Admin Routes
await new Promise((resolve) => setTimeout(resolve, 500));
tests.value[3].status = "running";
try {
const response = await axios.get(
"/api/admin/users/search?query=test&limit=1"
);
tests.value[3].status = "success";
tests.value[3].message = "Admin user routes accessible";
tests.value[3].details = {
usersFound: response.data.users?.length || 0,
};
} catch (error) {
tests.value[3].status = "error";
tests.value[3].message =
error.response?.data?.message || "Admin routes not accessible";
tests.value[3].details = {
status: error.response?.status,
error: error.response?.data?.message || error.message,
};
logError("Admin routes test failed: " + error.message);
}
testing.value = false;
};
const refreshAuth = async () => {
try {
await authStore.fetchUser();
toast.success("Auth refreshed");
} catch (error) {
toast.error("Failed to refresh auth");
logError("Refresh auth failed: " + error.message);
}
};
const clearCache = () => {
try {
localStorage.clear();
sessionStorage.clear();
toast.success("Cache cleared - please refresh page");
} catch (error) {
toast.error("Failed to clear cache");
logError("Clear cache failed: " + error.message);
}
};
const testAdminRoute = async () => {
try {
const response = await axios.get("/api/admin/config");
toast.success("Admin route accessible!");
console.log("Admin config:", response.data);
} catch (error) {
toast.error(
"Admin route failed: " + (error.response?.data?.message || error.message)
);
logError("Admin route test failed: " + error.message);
console.error("Error:", error);
}
};
const copyDebugInfo = () => {
const debugInfo = {
timestamp: new Date().toISOString(),
auth: {
isAuthenticated: authStore.isAuthenticated,
username: authStore.username,
steamId: authStore.steamId,
staffLevel: authStore.staffLevel,
isAdmin: authStore.isAdmin,
},
environment: {
apiBaseUrl: apiBaseUrl.value,
currentRoute: currentRoute.value,
location: windowLocation.value,
},
tests: tests.value,
errors: errorLog.value,
};
navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2));
toast.success("Debug info copied to clipboard");
};
// Watch for tab changes and load appropriate data
watch(activeTab, (newTab) => {
console.log("🔄 Debug panel tab changed to:", newTab);
if (newTab === "auth") {
runAllTests();
} else if (newTab === "announcements") {
loadAnnouncements();
} else if (newTab === "maintenance") {
loadMaintenanceStatus();
}
});
onMounted(() => {
// Load dismissed IDs
const stored = localStorage.getItem("dismissedAnnouncements");
if (stored) {
try {
dismissedIds.value = JSON.parse(stored);
} catch (e) {
console.error("Failed to parse dismissed announcements", e);
}
}
// Auto-run tests for initial tab
setTimeout(() => {
if (activeTab.value === "auth") {
runAllTests();
} else if (activeTab.value === "announcements") {
loadAnnouncements();
} else if (activeTab.value === "maintenance") {
loadMaintenanceStatus();
}
}, 500);
});
</script>
<style scoped>
.admin-debug-panel {
background: #1f2937;
border-radius: 0.5rem;
padding: 1.5rem;
color: #e5e7eb;
}
.tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
border-bottom: 2px solid #374151;
padding-bottom: 0;
}
.tab {
padding: 0.75rem 1.5rem;
background: transparent;
color: #9ca3af;
border: none;
border-bottom: 2px solid transparent;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin-bottom: -2px;
}
.tab:hover {
color: #e5e7eb;
background: rgba(255, 255, 255, 0.05);
}
.tab.active {
color: #60a5fa;
border-bottom-color: #60a5fa;
}
.tab-content {
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.debug-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 2px solid #374151;
}
.debug-header h2 {
font-size: 1.5rem;
font-weight: 700;
color: #f59e0b;
}
.btn-test {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: #3b82f6;
color: white;
border: none;
border-radius: 0.375rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.btn-test:hover:not(:disabled) {
background: #2563eb;
}
.btn-test:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.debug-section {
margin-bottom: 1.5rem;
padding: 1rem;
background: #111827;
border-radius: 0.375rem;
border: 1px solid #374151;
}
.debug-section h3 {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 1rem;
color: #60a5fa;
}
.status-grid,
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 0.75rem;
}
.status-item,
.info-item {
display: flex;
justify-content: space-between;
padding: 0.5rem;
background: #1f2937;
border-radius: 0.25rem;
border: 1px solid #374151;
}
.label {
font-weight: 600;
color: #9ca3af;
}
.value {
font-weight: 500;
color: #e5e7eb;
}
.value.small {
font-size: 0.75rem;
}
.value.success {
color: #10b981;
font-weight: 700;
}
.value.error {
color: #ef4444;
font-weight: 700;
}
.value.admin-super {
color: #a855f7;
font-weight: 700;
}
.value.admin {
color: #3b82f6;
font-weight: 700;
}
.value.moderator {
color: #10b981;
font-weight: 700;
}
.value.staff {
color: #f59e0b;
font-weight: 700;
}
.test-results {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.test-item {
padding: 0.75rem;
background: #1f2937;
border-radius: 0.375rem;
border: 1px solid #374151;
}
.test-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.test-name {
font-weight: 600;
color: #e5e7eb;
}
.test-status {
display: flex;
align-items: center;
gap: 0.25rem;
width: 24px;
height: 24px;
}
.test-status.success {
color: #10b981;
}
.test-status.error {
color: #ef4444;
}
.test-status.running {
color: #3b82f6;
}
.test-status.pending {
color: #6b7280;
}
.test-message {
font-size: 0.875rem;
padding: 0.5rem;
border-radius: 0.25rem;
margin-bottom: 0.5rem;
}
.test-message.success {
color: #10b981;
background: rgba(16, 185, 129, 0.1);
}
.test-message.error {
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
.test-details {
font-size: 0.75rem;
padding: 0.5rem;
background: #111827;
border-radius: 0.25rem;
overflow-x: auto;
}
.test-details pre {
margin: 0;
color: #9ca3af;
}
.actions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0.75rem;
}
.actions-row {
display: flex;
gap: 0.75rem;
margin: 1rem 0;
flex-wrap: wrap;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.section-header h3 {
margin-bottom: 0;
}
.btn-action {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem;
background: #374151;
color: #e5e7eb;
border: 1px solid #4b5563;
border-radius: 0.375rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-action:hover {
background: #4b5563;
border-color: #6b7280;
}
.btn-action .icon {
width: 16px;
height: 16px;
}
.error-log {
max-height: 300px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.error-item {
display: flex;
gap: 0.75rem;
padding: 0.5rem;
background: rgba(239, 68, 68, 0.1);
border-left: 3px solid #ef4444;
border-radius: 0.25rem;
font-size: 0.875rem;
}
.error-time {
color: #9ca3af;
font-weight: 600;
white-space: nowrap;
}
.error-message {
color: #ef4444;
}
.animate-spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Announcements specific styles */
.announcement-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-top: 1rem;
}
.announcement-debug-item {
padding: 1rem;
border-radius: 0.375rem;
border: 1px solid #374151;
background: #1f2937;
}
.type-info {
border-left: 4px solid #3b82f6;
}
.type-warning {
border-left: 4px solid #f59e0b;
}
.type-success {
border-left: 4px solid #10b981;
}
.type-error {
border-left: 4px solid #ef4444;
}
.announcement-row {
display: flex;
gap: 0.75rem;
margin-bottom: 0.5rem;
flex-wrap: wrap;
align-items: center;
}
.announcement-type {
font-weight: 700;
text-transform: uppercase;
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
background: rgba(255, 255, 255, 0.1);
}
.announcement-enabled {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.announcement-dismissible {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
background: rgba(59, 130, 246, 0.1);
color: #60a5fa;
}
.announcement-message {
color: #e5e7eb;
margin-bottom: 0.5rem;
}
.announcement-meta {
display: flex;
gap: 1rem;
font-size: 0.75rem;
color: #9ca3af;
flex-wrap: wrap;
}
.dismissed-badge {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-weight: 600;
}
.empty-state {
padding: 2rem;
text-align: center;
color: #f59e0b;
background: rgba(245, 158, 11, 0.1);
border-radius: 0.375rem;
border: 1px dashed #f59e0b;
}
/* Maintenance specific styles */
.maintenance-status {
display: flex;
flex-direction: column;
gap: 1rem;
}
.status-card {
display: flex;
gap: 1rem;
padding: 1.5rem;
border-radius: 0.5rem;
border: 2px solid;
margin-bottom: 1rem;
}
.status-card.warning {
background: rgba(245, 158, 11, 0.1);
border-color: #f59e0b;
}
.status-card.success {
background: rgba(16, 185, 129, 0.1);
border-color: #10b981;
}
.status-icon {
font-size: 2.5rem;
line-height: 1;
}
.status-content h4 {
margin: 0 0 0.5rem 0;
font-size: 1.25rem;
font-weight: 700;
color: #e5e7eb;
}
.status-content p {
margin: 0;
color: #9ca3af;
}
.btn-delete-inline {
margin-left: auto;
display: flex;
align-items: center;
justify-content: center;
padding: 0.375rem;
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-delete-inline:hover {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.5);
transform: scale(1.05);
}
.btn-delete-inline:active {
transform: scale(0.95);
}
</style>