diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..e7ed193
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,44 @@
+# FlatRun UI Agent Guide
+
+Vue 3 (`
+
+
diff --git a/src/components/TrafficDashboard.vue b/src/components/TrafficDashboard.vue
index dad78f3..df61acf 100644
--- a/src/components/TrafficDashboard.vue
+++ b/src/components/TrafficDashboard.vue
@@ -1326,15 +1326,15 @@ onMounted(() => {
}
.insight-chip.info {
- background: #dbeafe;
- color: #1d4ed8;
+ background: var(--color-info-50);
+ color: var(--color-info-700);
}
.insight-chip.warning {
- background: #fef3c7;
- color: #b45309;
+ background: var(--color-warning-50);
+ color: var(--color-warning-700);
}
.insight-chip.success {
- background: #d1fae5;
+ background: var(--color-success-50);
color: #059669;
}
.insight-chip.anomaly {
@@ -1363,12 +1363,12 @@ onMounted(() => {
}
.recommendation-btn.warning {
- background: #fef3c7;
- color: #b45309;
+ background: var(--color-warning-50);
+ color: var(--color-warning-700);
}
.recommendation-btn.critical {
- background: #fee2e2;
- color: #dc2626;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.recommendation-btn:hover {
filter: brightness(0.95);
@@ -1397,7 +1397,7 @@ onMounted(() => {
.stat-card.error {
border-color: #fca5a5;
- background: #fef2f2;
+ background: var(--color-danger-50);
}
.stat-header {
@@ -1424,7 +1424,7 @@ onMounted(() => {
color: #059669;
}
.trend.down {
- color: #dc2626;
+ color: var(--color-danger-700);
}
.trend i {
font-size: 0.5rem;
@@ -1436,10 +1436,10 @@ onMounted(() => {
color: var(--text);
}
.stat-value.warning {
- color: #b45309;
+ color: var(--color-warning-700);
}
.stat-card.error .stat-value {
- color: #dc2626;
+ color: var(--color-danger-700);
}
.stat-sub {
@@ -1496,7 +1496,7 @@ onMounted(() => {
.warning-panel {
border-color: #fcd34d;
- background: #fffbeb;
+ background: var(--color-warning-50);
}
.panel-header {
@@ -1609,10 +1609,10 @@ onMounted(() => {
}
.dep-stat.slow {
- color: #b45309;
+ color: var(--color-warning-700);
}
.dep-stat.high {
- color: #dc2626;
+ color: var(--color-danger-700);
}
.deployment-row i {
@@ -1635,7 +1635,7 @@ onMounted(() => {
}
.unknown-row:hover {
- background: #fef3c7;
+ background: var(--color-warning-50);
}
.unknown-row:last-child {
@@ -1644,12 +1644,12 @@ onMounted(() => {
.unknown-row code {
font-size: 0.75rem;
- color: #92400e;
+ color: var(--color-warning-700);
}
.unknown-row span {
font-size: 0.6875rem;
- color: #b45309;
+ color: var(--color-warning-700);
}
/* Suspicious IPs */
@@ -1678,13 +1678,13 @@ onMounted(() => {
.suspicious-info code {
font-size: 0.75rem;
- color: #92400e;
+ color: var(--color-warning-700);
font-weight: 500;
}
.suspicious-stat {
font-size: 0.625rem;
- color: #b45309;
+ color: var(--color-warning-700);
}
.suspicious-actions {
@@ -1708,12 +1708,12 @@ onMounted(() => {
}
.btn-action.danger {
- color: #dc2626;
+ color: var(--color-danger-700);
}
.btn-action.danger:hover {
- background: #fee2e2;
- color: #991b1b;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.btn-icon-xs {
@@ -1831,16 +1831,16 @@ onMounted(() => {
}
.status-item.success {
- background: #d1fae5;
+ background: var(--color-success-50);
}
.status-item.redirect {
- background: #dbeafe;
+ background: var(--color-info-50);
}
.status-item.client-error {
- background: #fef3c7;
+ background: var(--color-warning-50);
}
.status-item.server-error {
- background: #fee2e2;
+ background: var(--color-danger-50);
}
.status-code {
@@ -1852,13 +1852,13 @@ onMounted(() => {
color: #059669;
}
.status-item.redirect .status-code {
- color: #1d4ed8;
+ color: var(--color-info-700);
}
.status-item.client-error .status-code {
- color: #b45309;
+ color: var(--color-warning-700);
}
.status-item.server-error .status-code {
- color: #dc2626;
+ color: var(--color-danger-700);
}
.status-count {
@@ -1939,7 +1939,7 @@ onMounted(() => {
background: var(--surface-sunken);
}
.data-table tbody tr.error-row {
- background: #fef2f2;
+ background: var(--color-danger-50);
}
.cell-time {
@@ -1951,13 +1951,13 @@ onMounted(() => {
color: #059669;
}
.cell-time.moderate {
- color: #b45309;
+ color: var(--color-warning-700);
}
.cell-time.slow {
color: #ea580c;
}
.cell-time.critical {
- color: #dc2626;
+ color: var(--color-danger-700);
}
.cell-domain {
@@ -2008,12 +2008,12 @@ onMounted(() => {
}
.btn-icon-sm.danger {
- color: #dc2626;
+ color: var(--color-danger-700);
}
.btn-icon-sm.danger:hover {
- background: #fee2e2;
- color: #991b1b;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.method-tag {
@@ -2025,20 +2025,20 @@ onMounted(() => {
}
.method-tag.get {
- background: #dbeafe;
- color: #1d4ed8;
+ background: var(--color-info-50);
+ color: var(--color-info-700);
}
.method-tag.post {
- background: #d1fae5;
+ background: var(--color-success-50);
color: #059669;
}
.method-tag.put {
- background: #fef3c7;
- color: #b45309;
+ background: var(--color-warning-50);
+ color: var(--color-warning-700);
}
.method-tag.delete {
- background: #fee2e2;
- color: #dc2626;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.method-tag.patch {
background: #ede9fe;
@@ -2054,20 +2054,20 @@ onMounted(() => {
}
.status-tag.s2xx {
- background: #d1fae5;
+ background: var(--color-success-50);
color: #059669;
}
.status-tag.s3xx {
- background: #dbeafe;
- color: #1d4ed8;
+ background: var(--color-info-50);
+ color: var(--color-info-700);
}
.status-tag.s4xx {
- background: #fef3c7;
- color: #b45309;
+ background: var(--color-warning-50);
+ color: var(--color-warning-700);
}
.status-tag.s5xx {
- background: #fee2e2;
- color: #dc2626;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.tag {
@@ -2125,20 +2125,20 @@ onMounted(() => {
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border-radius: var(--radius-sm);
- background: #fef2f2;
+ background: var(--color-danger-50);
}
.alert-row.warning {
- background: #fffbeb;
+ background: var(--color-warning-50);
}
.alert-row.critical {
- background: #fef2f2;
+ background: var(--color-danger-50);
}
.alert-row.warning i {
color: #d97706;
}
.alert-row.critical i {
- color: #dc2626;
+ color: var(--color-danger-700);
}
.alert-text {
@@ -2225,7 +2225,7 @@ onMounted(() => {
display: flex;
gap: 0.75rem;
padding: 0.75rem;
- background: #f0f9ff;
+ background: var(--color-info-50);
border: 1px solid #bae6fd;
border-radius: var(--radius-sm);
margin-top: 0.5rem;
diff --git a/src/components/base/BaseButton.vue b/src/components/base/BaseButton.vue
new file mode 100644
index 0000000..feb49a8
--- /dev/null
+++ b/src/components/base/BaseButton.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/src/components/base/BaseCard.vue b/src/components/base/BaseCard.vue
new file mode 100644
index 0000000..2380c27
--- /dev/null
+++ b/src/components/base/BaseCard.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
diff --git a/src/components/base/BaseField.vue b/src/components/base/BaseField.vue
new file mode 100644
index 0000000..d81299c
--- /dev/null
+++ b/src/components/base/BaseField.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
diff --git a/src/components/base/BaseInput.vue b/src/components/base/BaseInput.vue
new file mode 100644
index 0000000..1cc641c
--- /dev/null
+++ b/src/components/base/BaseInput.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
diff --git a/src/components/base/BaseSelect.vue b/src/components/base/BaseSelect.vue
new file mode 100644
index 0000000..76bec70
--- /dev/null
+++ b/src/components/base/BaseSelect.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/base/BaseTextarea.vue b/src/components/base/BaseTextarea.vue
new file mode 100644
index 0000000..9ba8f99
--- /dev/null
+++ b/src/components/base/BaseTextarea.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
diff --git a/src/layouts/DashboardLayout.vue b/src/layouts/DashboardLayout.vue
index a52b644..e4f48f7 100644
--- a/src/layouts/DashboardLayout.vue
+++ b/src/layouts/DashboardLayout.vue
@@ -58,6 +58,7 @@
+
Deployments
{{ stats.deployments }}
@@ -90,6 +91,7 @@
class="nav-subitem"
active-class="active"
>
+
Containers
{{ stats.containers }}
@@ -99,6 +101,7 @@
class="nav-subitem"
active-class="active"
>
+
Images
{{ stats.images }}
@@ -108,6 +111,7 @@
class="nav-subitem"
active-class="active"
>
+
Volumes
{{ stats.volumes }}
@@ -117,6 +121,7 @@
class="nav-subitem"
active-class="active"
>
+
Networks
{{ stats.networks }}
@@ -126,12 +131,124 @@
class="nav-subitem"
active-class="active"
>
+
Port Mappings
{{ stats.dockerPorts }}
+
+
+
+
+
+ Databases
+ {{ stats.databases }}
+
+
+
+ Object Stores
+
+
+
+
+
+
+
+
+
+ Observability
+
+
+
+ Security
+
+
+
+ Certificates
+ {{ stats.certificates }}
+
+
+
+
+
+
+
+
+
+ Installed Apps
+ {{ stats.apps }}
+
+
+
+ Marketplace
+
+
+
+
+
Server Info
+
Terminal
+
Files
+
Cluster
{{ clusterPeers.length + 1 }}
@@ -191,6 +312,7 @@
class="nav-subitem"
active-class="active"
>
+
Infrastructure
{{ stats.infrastructure }}
@@ -200,6 +322,7 @@
class="nav-subitem"
active-class="active"
>
+
Ports
{{ stats.ports }}
@@ -209,6 +332,7 @@
class="nav-subitem"
active-class="active"
>
+
Services
{{ stats.services }}
@@ -218,30 +342,12 @@
class="nav-subitem"
active-class="active"
>
+
Cron Jobs
-
-
-
-
- Servers
- {{ stats.databases }}
-
-
-
-
- Zones
- External Providers
-
-
-
-
-
-
-
- Observability
+
+
+ Zones
-
- Security
-
-
- Certificates
- {{ stats.certificates }}
-
-
-
-
-
-
-
-
- Installed Apps
- {{ stats.apps }}
+
+
+ External Providers
- Marketplace
@@ -351,6 +396,7 @@
class="nav-subitem"
active-class="active"
>
+
Settings
+
Users
+
API Keys
@@ -511,7 +559,7 @@ const expandedGroups = reactive({
stacks: true,
docker: true,
system: false,
- databases: false,
+ storage: false,
dns: false,
security: false,
extensions: false,
@@ -891,7 +939,7 @@ onMounted(() => {
display: flex;
align-items: center;
gap: 0.5rem;
- padding: 0.625rem 1.25rem 0.625rem 3.25rem;
+ padding: 0.625rem 1.25rem 0.625rem 2.5rem;
color: var(--sidebar-text);
text-decoration: none;
font-size: 0.875rem;
diff --git a/src/router/index.ts b/src/router/index.ts
index b78810b..ce4b3b3 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -94,6 +94,12 @@ const routes: RouteRecordRaw[] = [
component: () => import("@/views/DatabaseManagerView.vue"),
meta: { permission: "databases:read" },
},
+ {
+ path: "storage/object-stores",
+ name: "object-stores",
+ component: () => import("@/views/ObjectStoresView.vue"),
+ meta: { permission: "backups:read" },
+ },
{
path: "certificates",
name: "certificates",
diff --git a/src/services/api.ts b/src/services/api.ts
index b9bd698..eb59b1d 100755
--- a/src/services/api.ts
+++ b/src/services/api.ts
@@ -931,6 +931,49 @@ export const credentialsApi = {
test: (id: string) => apiClient.post<{ message: string; success: boolean }>(`/credentials/${id}/test`),
};
+export type CredentialKind = "s3";
+
+export interface StorageCredential {
+ readonly id: string;
+ readonly name: string;
+ readonly kind: CredentialKind;
+ readonly data: Record;
+ readonly created_at: string;
+ readonly updated_at: string;
+}
+
+export interface BackupDestination {
+ name: string;
+ type: string;
+ kind?: string;
+ deployment?: string;
+ endpoint: string;
+ region: string;
+ bucket: string;
+ prefix: string;
+ credential_id: string;
+ use_path_style: boolean;
+ enabled?: boolean | null;
+}
+
+export const storageCredentialsApi = {
+ list: (kind?: CredentialKind) =>
+ apiClient.get<{ credentials: StorageCredential[] }>("/storage-credentials", {
+ params: kind ? { kind } : undefined,
+ }),
+ create: (data: { name: string; kind: CredentialKind; data: Record }) =>
+ apiClient.post<{ message: string; credential: StorageCredential }>("/storage-credentials", data),
+ update: (id: string, data: { name?: string; data?: Record }) =>
+ apiClient.put<{ message: string; credential: StorageCredential }>(`/storage-credentials/${id}`, data),
+ delete: (id: string) => apiClient.delete(`/storage-credentials/${id}`),
+};
+
+export const backupDestinationsApi = {
+ list: () => apiClient.get<{ destinations: BackupDestination[] }>("/backup-destinations"),
+ test: (data: BackupDestination) =>
+ apiClient.post<{ success: boolean; message?: string; error?: string }>("/backup-destinations/test", data),
+};
+
export interface SecurityEventFilter {
event_type?: string;
severity?: string;
@@ -1139,6 +1182,7 @@ export interface Backup {
readonly created_at: string;
readonly completed_at?: string;
readonly expires_at?: string;
+ readonly locations?: readonly string[];
}
export interface BackupSpec {
diff --git a/src/views/CertificatesView.vue b/src/views/CertificatesView.vue
index 9f018b4..a874b2b 100755
--- a/src/views/CertificatesView.vue
+++ b/src/views/CertificatesView.vue
@@ -561,8 +561,8 @@ onMounted(() => {
.btn-danger-sm {
padding: 0.375rem;
- background: #fee2e2;
- color: #dc2626;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
border: none;
border-radius: var(--radius-md);
cursor: pointer;
@@ -570,7 +570,7 @@ onMounted(() => {
}
.btn-danger-sm:hover:not(:disabled) {
- background: #fecaca;
+ background: var(--color-danger-50);
}
.btn-danger-sm:disabled {
@@ -717,22 +717,22 @@ onMounted(() => {
}
.status-badge.valid {
- background: #dcfce7;
- color: #166534;
+ background: var(--color-success-50);
+ color: var(--color-success-700);
}
.status-badge.expiring {
- background: #fef3c7;
- color: #92400e;
+ background: var(--color-warning-50);
+ color: var(--color-warning-700);
}
.status-badge.expired {
- background: #fee2e2;
- color: #991b1b;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.days-left.good {
- color: #16a34a;
+ color: var(--color-success-700);
font-weight: 500;
}
@@ -742,7 +742,7 @@ onMounted(() => {
}
.days-left.critical {
- color: #dc2626;
+ color: var(--color-danger-700);
font-weight: 500;
}
@@ -877,18 +877,18 @@ onMounted(() => {
}
.summary-icon.valid {
- background: #dcfce7;
- color: #16a34a;
+ background: var(--color-success-50);
+ color: var(--color-success-700);
}
.summary-icon.expiring {
- background: #fef3c7;
+ background: var(--color-warning-50);
color: #d97706;
}
.summary-icon.expired {
- background: #fee2e2;
- color: #dc2626;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.summary-info {
diff --git a/src/views/ClusterView.vue b/src/views/ClusterView.vue
index d34a14a..33f6deb 100644
--- a/src/views/ClusterView.vue
+++ b/src/views/ClusterView.vue
@@ -619,18 +619,18 @@ onMounted(() => {
.peer-status.active,
.peer-status.peered {
- background: #dcfce7;
- color: #166534;
+ background: var(--color-success-50);
+ color: var(--color-success-700);
}
.peer-status.unreachable {
- background: #fee2e2;
- color: #991b1b;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.peer-status.pending {
- background: #fef3c7;
- color: #92400e;
+ background: var(--color-warning-50);
+ color: var(--color-warning-700);
}
.time-cell {
@@ -685,12 +685,12 @@ code {
}
.btn-danger {
- background: #fee2e2;
- color: #dc2626;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.btn-danger:hover:not(:disabled) {
- background: #fecaca;
+ background: var(--color-danger-100);
}
.btn:disabled {
@@ -818,8 +818,8 @@ code {
align-items: center;
gap: 0.375rem;
font-size: 0.8125rem;
- color: #dc2626;
- background: #fef2f2;
+ color: var(--color-danger-700);
+ background: var(--color-danger-50);
padding: 0.5rem 0.75rem;
border-radius: var(--radius-sm);
}
@@ -829,8 +829,8 @@ code {
align-items: center;
gap: 0.375rem;
font-size: 0.8125rem;
- color: #92400e;
- background: #fef3c7;
+ color: var(--color-warning-700);
+ background: var(--color-warning-50);
padding: 0.625rem 0.75rem;
border-radius: var(--radius-sm);
margin-top: 0.75rem;
diff --git a/src/views/CronJobsView.vue b/src/views/CronJobsView.vue
index 46e3b52..0b83647 100644
--- a/src/views/CronJobsView.vue
+++ b/src/views/CronJobsView.vue
@@ -885,7 +885,7 @@ onMounted(() => {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface-raised);
- color: #374151;
+ color: var(--text);
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s;
@@ -945,7 +945,7 @@ onMounted(() => {
.btn-icon:hover:not(:disabled) {
background: var(--surface-sunken);
- color: #374151;
+ color: var(--text);
}
.btn-primary {
@@ -965,7 +965,7 @@ onMounted(() => {
.btn-secondary {
background: var(--surface-raised);
border: 1px solid var(--border);
- color: #374151;
+ color: var(--text);
}
.btn-secondary:hover:not(:disabled) {
@@ -978,12 +978,12 @@ onMounted(() => {
}
.btn-danger {
- background: #fee2e2;
- color: #dc2626;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.btn-danger:hover:not(:disabled) {
- background: #fecaca;
+ background: var(--color-danger-50);
}
.loading-state,
@@ -1060,13 +1060,13 @@ onMounted(() => {
}
.task-type.command {
- background: #dbeafe;
- color: #1d4ed8;
+ background: var(--color-info-50);
+ color: var(--color-info-700);
}
.task-type.backup {
- background: #dcfce7;
- color: #166534;
+ background: var(--color-success-50);
+ color: var(--color-success-700);
}
.switch {
@@ -1139,7 +1139,7 @@ input:checked + .slider:before {
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
- color: #374151;
+ color: var(--text);
margin-bottom: 1rem;
}
@@ -1199,7 +1199,7 @@ input:checked + .slider:before {
}
.timing-value {
- color: #374151;
+ color: var(--text);
margin-left: 0.25rem;
}
@@ -1357,7 +1357,7 @@ input:checked + .slider:before {
display: block;
font-size: 0.875rem;
font-weight: 600;
- color: #374151;
+ color: var(--text);
margin-bottom: 0.5rem;
}
@@ -1439,18 +1439,18 @@ input:checked + .slider:before {
}
.exec-status-badge.completed {
- background: #dcfce7;
- color: #166534;
+ background: var(--color-success-50);
+ color: var(--color-success-700);
}
.exec-status-badge.failed {
- background: #fee2e2;
- color: #991b1b;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.exec-status-badge.running {
- background: #dbeafe;
- color: #1d4ed8;
+ background: var(--color-info-50);
+ color: var(--color-info-700);
}
.output-block,
@@ -1464,7 +1464,7 @@ input:checked + .slider:before {
font-size: 0.875rem;
font-weight: 600;
margin: 0 0 0.5rem;
- color: #374151;
+ color: var(--text);
}
.output-block pre,
diff --git a/src/views/DeploymentDetailView.vue b/src/views/DeploymentDetailView.vue
index 83cf4ed..99f4555 100755
--- a/src/views/DeploymentDetailView.vue
+++ b/src/views/DeploymentDetailView.vue
@@ -4005,6 +4005,7 @@ onUnmounted(() => {
border-radius: var(--radius-sm);
font-size: var(--text-md);
background: var(--surface);
+ color: var(--text);
}
.terminal-tab {
@@ -4346,6 +4347,8 @@ onUnmounted(() => {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-size: var(--text-sm);
+ background: var(--surface);
+ color: var(--text);
transition: all var(--transition-base);
}
diff --git a/src/views/DnsExternalView.vue b/src/views/DnsExternalView.vue
index c9661c2..479b2f3 100644
--- a/src/views/DnsExternalView.vue
+++ b/src/views/DnsExternalView.vue
@@ -487,12 +487,12 @@ onMounted(() => {
padding: 2rem;
text-align: center;
border-bottom: 1px solid var(--border);
- background: linear-gradient(135deg, #f8fafc, #f1f5f9);
+ background: linear-gradient(135deg, var(--surface-sunken), var(--surface-inset));
}
.setup-header i {
font-size: 3rem;
- color: #6366f1;
+ color: var(--accent);
margin-bottom: 1rem;
}
@@ -533,13 +533,13 @@ onMounted(() => {
}
.provider-btn:hover {
- border-color: #6366f1;
- background: #f5f3ff;
+ border-color: var(--accent);
+ background: var(--accent-subtle);
}
.provider-btn.active {
- border-color: #6366f1;
- background: #eef2ff;
+ border-color: var(--accent);
+ background: var(--accent-subtle);
}
.provider-btn i {
@@ -548,12 +548,12 @@ onMounted(() => {
}
.provider-btn.active i {
- color: #6366f1;
+ color: var(--accent);
}
.provider-btn span {
font-weight: 500;
- color: #374151;
+ color: var(--text);
}
.credentials-form {
@@ -565,7 +565,7 @@ onMounted(() => {
.credentials-form h3 {
font-size: 1rem;
font-weight: 600;
- color: #374151;
+ color: var(--text);
margin: 0 0 1rem 0;
}
@@ -592,10 +592,10 @@ onMounted(() => {
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
- background: #eef2ff;
+ background: var(--accent-subtle);
border-radius: var(--radius-sm);
font-weight: 600;
- color: #4f46e5;
+ color: var(--accent-hover);
}
.zones-section,
@@ -627,8 +627,8 @@ onMounted(() => {
}
.proxied-badge.active {
- background: #fef3c7;
- color: #d97706;
+ background: var(--color-warning-50);
+ color: var(--color-warning-700);
}
/* Modal overrides */
diff --git a/src/views/DnsZonesView.vue b/src/views/DnsZonesView.vue
index df309cd..50b31a2 100644
--- a/src/views/DnsZonesView.vue
+++ b/src/views/DnsZonesView.vue
@@ -738,12 +738,12 @@ onMounted(() => {
padding: 2rem;
text-align: center;
border-bottom: 1px solid var(--border);
- background: linear-gradient(135deg, #f8fafc, #f1f5f9);
+ background: linear-gradient(135deg, var(--surface-sunken), var(--surface-inset));
}
.setup-header i {
font-size: 3rem;
- color: #6366f1;
+ color: var(--accent);
margin-bottom: 1rem;
}
@@ -831,10 +831,10 @@ onMounted(() => {
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
- background: #dcfce7;
+ background: var(--color-success-50);
border-radius: var(--radius-sm);
font-weight: 600;
- color: #166534;
+ color: var(--color-success-700);
}
.status-dot {
@@ -902,7 +902,7 @@ onMounted(() => {
}
.badge-success {
- color: #166534;
+ color: var(--color-success-700);
}
/* Content line for multi-value records */
@@ -925,7 +925,7 @@ onMounted(() => {
.settings-section h4 {
font-size: 0.875rem;
font-weight: 600;
- color: #374151;
+ color: var(--text);
margin: 0 0 0.75rem 0;
}
@@ -960,8 +960,8 @@ onMounted(() => {
}
.status-badge.online {
- background: #dcfce7;
- color: #166534;
+ background: var(--color-success-50);
+ color: var(--color-success-700);
}
.action-buttons {
diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue
index addee33..4d14ca6 100644
--- a/src/views/HomeView.vue
+++ b/src/views/HomeView.vue
@@ -536,7 +536,7 @@ onMounted(() => {
.btn-secondary {
background: var(--surface-raised);
- color: #374151;
+ color: var(--text);
border: 1px solid var(--border);
}
@@ -807,8 +807,8 @@ onMounted(() => {
}
.status-badge.running {
- background: #dcfce7;
- color: #166534;
+ background: var(--color-success-50);
+ color: var(--color-success-700);
}
.status-badge.stopped {
@@ -817,8 +817,8 @@ onMounted(() => {
}
.status-badge.error {
- background: #fee2e2;
- color: #991b1b;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.time-cell {
@@ -933,18 +933,18 @@ onMounted(() => {
}
.health-indicator.healthy {
- background: #dcfce7;
- color: #166534;
+ background: var(--color-success-50);
+ color: var(--color-success-700);
}
.health-indicator.warning {
- background: #fef3c7;
- color: #92400e;
+ background: var(--color-warning-50);
+ color: var(--color-warning-700);
}
.health-indicator.critical {
- background: #fee2e2;
- color: #991b1b;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.resource-item {
@@ -1073,7 +1073,7 @@ onMounted(() => {
background: var(--surface-inset);
padding: 0.125rem 0.375rem;
border-radius: var(--radius-sm);
- color: #374151;
+ color: var(--text);
font-family: "SF Mono", "Fira Code", monospace;
}
diff --git a/src/views/InfrastructureView.vue b/src/views/InfrastructureView.vue
index 3816e84..05312bb 100644
--- a/src/views/InfrastructureView.vue
+++ b/src/views/InfrastructureView.vue
@@ -432,18 +432,18 @@ onMounted(() => {
}
.health-healthy {
- background: #dcfce7;
- color: #166534;
+ background: var(--color-success-50);
+ color: var(--color-success-700);
}
.health-unhealthy {
- background: #fee2e2;
- color: #991b1b;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.health-starting {
- background: #fef3c7;
- color: #92400e;
+ background: var(--color-warning-50);
+ color: var(--color-warning-700);
}
.btn {
@@ -492,7 +492,7 @@ onMounted(() => {
}
.btn-secondary {
- background: #e5e7eb;
+ background: var(--surface-inset);
color: var(--text);
}
diff --git a/src/views/NetworksView.vue b/src/views/NetworksView.vue
index 9b3889a..776f9f4 100644
--- a/src/views/NetworksView.vue
+++ b/src/views/NetworksView.vue
@@ -450,7 +450,7 @@ onMounted(() => {
}
.btn-secondary:hover {
- background: #e5e7eb;
+ background: var(--surface-inset);
}
.btn-danger {
@@ -495,8 +495,8 @@ onMounted(() => {
}
.btn-icon-sm.danger:hover {
- background: #fee2e2;
- color: #dc2626;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.btn-icon-xs {
@@ -515,8 +515,8 @@ onMounted(() => {
}
.btn-icon-xs.danger:hover {
- background: #fee2e2;
- color: #dc2626;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.btn-text {
@@ -774,7 +774,7 @@ onMounted(() => {
}
.warning-text {
- color: #dc2626;
+ color: var(--color-danger-700);
font-size: 0.875rem;
}
diff --git a/src/views/ObjectStoresView.vue b/src/views/ObjectStoresView.vue
new file mode 100644
index 0000000..62f70d5
--- /dev/null
+++ b/src/views/ObjectStoresView.vue
@@ -0,0 +1,288 @@
+
+
+
+
+
+
+
+
+
+
+
+
Connected stores
+
+
+ Deploy a local store
+
+
+ Manage
+
+
+
+
+
+
+
+
+
No object stores connected yet.
+
+
+ Deploy a local store
+
+
+ Connect external
+
+
+
+
+
+
+
+
+ {{ d.name }}
+
+ {{ storeKind(d) === "managed" ? "Managed" : "External" }}
+
+
+ {{ d.enabled === false ? "Disabled" : "Active" }}
+
+
+
+ - Bucket
+ - {{ d.bucket }}
+ - Endpoint
+ - {{ d.endpoint || "AWS default" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/SecurityView.vue b/src/views/SecurityView.vue
index 7bd33b3..8a8d7d4 100644
--- a/src/views/SecurityView.vue
+++ b/src/views/SecurityView.vue
@@ -1327,7 +1327,7 @@ onMounted(() => {
.stat-card.warning {
border-color: #fca5a5;
- background: #fef2f2;
+ background: var(--color-danger-50);
}
.stat-value {
@@ -1337,7 +1337,7 @@ onMounted(() => {
}
.stat-card.warning .stat-value {
- color: #dc2626;
+ color: var(--color-danger-700);
}
.stat-label {
@@ -1430,18 +1430,18 @@ onMounted(() => {
}
.severity-badge.critical {
- background: #fee2e2;
- color: #991b1b;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.severity-badge.high {
- background: #ffedd5;
+ background: var(--color-warning-50);
color: #9a3412;
}
.severity-badge.medium {
- background: #fef3c7;
- color: #92400e;
+ background: var(--color-warning-50);
+ color: var(--color-warning-700);
}
.severity-badge.low {
@@ -1569,8 +1569,8 @@ onMounted(() => {
}
.badge.auto {
- background: #dbeafe;
- color: #1d4ed8;
+ background: var(--color-info-50);
+ color: var(--color-info-700);
}
.thresholds-grid {
@@ -1692,7 +1692,7 @@ onMounted(() => {
}
.preset-btn:hover {
- background: #e5e7eb;
+ background: var(--surface-inset);
}
.preset-btn i {
@@ -1773,7 +1773,7 @@ onMounted(() => {
}
.btn-danger-ghost:hover:not(:disabled) {
- background: #fef2f2;
+ background: var(--color-danger-50);
border-color: #fecaca;
}
@@ -1887,7 +1887,7 @@ onMounted(() => {
}
.confirm-icon.danger {
- background: #fef2f2;
+ background: var(--color-danger-50);
color: #ef4444;
}
@@ -2012,7 +2012,7 @@ onMounted(() => {
}
.status-badge.active {
- background: #d1fae5;
+ background: var(--color-success-50);
color: #059669;
}
@@ -2033,8 +2033,8 @@ onMounted(() => {
}
.setting-note.info {
- background: #eff6ff;
- color: #1e40af;
+ background: var(--color-info-50);
+ color: var(--color-info-700);
}
.setting-note.info i {
@@ -2042,8 +2042,8 @@ onMounted(() => {
}
.setting-note.warning {
- background: #fffbeb;
- color: #92400e;
+ background: var(--color-warning-50);
+ color: var(--color-warning-700);
}
.setting-note.warning i {
@@ -2085,12 +2085,12 @@ onMounted(() => {
}
.stat-icon.total {
- background: #eff6ff;
+ background: var(--color-info-50);
color: #3b82f6;
}
.stat-icon.day {
- background: #f0fdf4;
+ background: var(--color-success-50);
color: #22c55e;
}
@@ -2100,12 +2100,12 @@ onMounted(() => {
}
.stat-icon.blocked {
- background: #fef2f2;
+ background: var(--color-danger-50);
color: #ef4444;
}
.stat-icon.routes {
- background: #fffbeb;
+ background: var(--color-warning-50);
color: #f59e0b;
}
@@ -2291,7 +2291,7 @@ onMounted(() => {
width: 24px;
height: 24px;
border-radius: 50%;
- background: #e5e7eb;
+ background: var(--surface-inset);
display: flex;
align-items: center;
justify-content: center;
@@ -2337,12 +2337,12 @@ onMounted(() => {
}
.top-badge.critical {
- background: #fee2e2;
- color: #991b1b;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.top-badge.high {
- background: #ffedd5;
+ background: var(--color-warning-50);
color: #9a3412;
}
@@ -2369,7 +2369,7 @@ onMounted(() => {
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
- background: #fef2f2;
+ background: var(--color-danger-50);
border-radius: var(--radius-sm);
border-left: 3px solid #dc2626;
}
@@ -2388,10 +2388,10 @@ onMounted(() => {
.critical-event-ip {
font-size: 0.75rem;
- background: #fecaca;
+ background: var(--color-danger-50);
padding: 0.125rem 0.375rem;
border-radius: var(--radius-sm);
- color: #991b1b;
+ color: var(--color-danger-700);
}
.critical-event-path {
diff --git a/src/views/ServerInfoView.vue b/src/views/ServerInfoView.vue
index 6d42b84..afebb59 100644
--- a/src/views/ServerInfoView.vue
+++ b/src/views/ServerInfoView.vue
@@ -296,13 +296,13 @@ onMounted(() => {
}
.health-pill.healthy {
- background: #dcfce7;
- color: #166534;
+ background: var(--color-success-50);
+ color: var(--color-success-700);
}
.health-pill.unhealthy {
- background: #fee2e2;
- color: #991b1b;
+ background: var(--color-danger-50);
+ color: var(--color-danger-700);
}
.card-body {
@@ -354,19 +354,19 @@ code.ipv6 {
}
.status-badge.ok {
- color: #16a34a;
+ color: var(--color-success-700);
}
.status-badge.ok i {
- color: #16a34a;
+ color: var(--color-success-700);
}
.status-badge.fail {
- color: #dc2626;
+ color: var(--color-danger-700);
}
.status-badge.fail i {
- color: #dc2626;
+ color: var(--color-danger-700);
}
.data-table {
@@ -410,7 +410,7 @@ code.ipv6 {
.error-text {
font-size: 0.8125rem;
- color: #dc2626;
+ color: var(--color-danger-700);
}
.muted {
diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue
index 638e65f..f9a7a23 100644
--- a/src/views/SettingsView.vue
+++ b/src/views/SettingsView.vue
@@ -1716,11 +1716,11 @@ onMounted(() => {
.tab:hover {
background: var(--surface-inset);
- color: #374151;
+ color: var(--text);
}
.tab.active {
- background: #3b82f6;
+ background: var(--accent);
color: white;
}
@@ -1740,7 +1740,7 @@ onMounted(() => {
.btn-icon:hover:not(:disabled) {
background: var(--surface-inset);
- color: #374151;
+ color: var(--text);
}
.btn-icon:disabled {
@@ -1800,7 +1800,7 @@ onMounted(() => {
.card-header i:first-child {
font-size: 1.125rem;
- color: #3b82f6;
+ color: var(--accent);
}
.card-header h3 {
@@ -1870,7 +1870,7 @@ onMounted(() => {
border-radius: var(--radius-sm);
font-family: "SF Mono", "Fira Code", monospace;
font-size: 0.75rem;
- color: #374151;
+ color: var(--text);
}
.status-badge {
@@ -1909,7 +1909,7 @@ onMounted(() => {
border-radius: var(--radius-sm);
font-size: 0.8125rem;
font-weight: 500;
- color: #374151;
+ color: var(--text);
cursor: pointer;
transition: all 0.2s;
}
@@ -1926,7 +1926,7 @@ onMounted(() => {
.action-btn i {
font-size: 0.875rem;
- color: #3b82f6;
+ color: var(--accent);
}
.config-grid {
@@ -1959,7 +1959,7 @@ onMounted(() => {
border-radius: var(--radius-md);
font-family: "SF Mono", "Fira Code", monospace;
font-size: 0.8125rem;
- color: #374151;
+ color: var(--text);
}
.origins-list {
@@ -1974,7 +1974,7 @@ onMounted(() => {
border-radius: var(--radius-sm);
font-family: "SF Mono", "Fira Code", monospace;
font-size: 0.75rem;
- color: #374151;
+ color: var(--text);
}
.config-note {
@@ -1982,13 +1982,13 @@ onMounted(() => {
align-items: flex-start;
gap: 0.625rem;
padding: 0.875rem;
- background: #eff6ff;
+ background: var(--color-info-50);
border-radius: var(--radius-sm);
margin-bottom: 1rem;
}
.config-note i {
- color: #3b82f6;
+ color: var(--accent);
margin-top: 0.125rem;
flex-shrink: 0;
}
@@ -2039,7 +2039,7 @@ onMounted(() => {
.form-label {
font-size: 0.8125rem;
font-weight: 600;
- color: #374151;
+ color: var(--text);
}
.form-hint {
@@ -2050,7 +2050,7 @@ onMounted(() => {
.subsection-title {
font-size: 0.875rem;
font-weight: 600;
- color: #374151;
+ color: var(--text);
margin: 1.25rem 0 0.25rem;
padding-top: 1rem;
border-top: 1px solid var(--border-subtle);
@@ -2067,7 +2067,7 @@ onMounted(() => {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-size: 0.875rem;
- color: #374151;
+ color: var(--text);
background: var(--surface);
transition:
border-color 0.2s,
@@ -2076,8 +2076,8 @@ onMounted(() => {
.form-input:focus {
outline: none;
- border-color: #3b82f6;
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
+ border-color: var(--accent);
+ box-shadow: 0 0 0 var(--ring-width) var(--ring-color);
}
.form-input::placeholder {
@@ -2089,9 +2089,9 @@ onMounted(() => {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-size: 0.875rem;
- color: #374151;
+ color: var(--text);
background: var(--surface)
- url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")
+ url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%239ca3af' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")
right 0.5rem center/1.5em 1.5em no-repeat;
appearance: none;
cursor: pointer;
@@ -2102,8 +2102,8 @@ onMounted(() => {
.form-select:focus {
outline: none;
- border-color: #3b82f6;
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
+ border-color: var(--accent);
+ box-shadow: 0 0 0 var(--ring-width) var(--ring-color);
}
.toggle-row {
@@ -2159,7 +2159,7 @@ onMounted(() => {
}
.toggle-switch input:checked + .toggle-slider {
- background-color: #3b82f6;
+ background-color: var(--accent);
}
.toggle-switch input:checked + .toggle-slider::before {
@@ -2167,7 +2167,7 @@ onMounted(() => {
}
.toggle-switch input:focus + .toggle-slider {
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
+ box-shadow: 0 0 0 var(--ring-width) var(--ring-color);
}
.save-footer {
@@ -2190,7 +2190,7 @@ onMounted(() => {
}
.btn-primary {
- background: #3b82f6;
+ background: var(--accent);
color: white;
}
@@ -2276,7 +2276,7 @@ onMounted(() => {
}
.empty-state p {
- color: #374151;
+ color: var(--text);
font-weight: 500;
margin: 0 0 0.5rem 0;
}
@@ -2357,7 +2357,7 @@ onMounted(() => {
}
.btn-danger-ghost:hover:not(:disabled) {
- background: #fef2f2;
+ background: var(--color-danger-50);
border-color: #fecaca;
}
@@ -2420,8 +2420,8 @@ onMounted(() => {
.form-control:focus {
outline: none;
- border-color: #3b82f6;
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
+ border-color: var(--accent);
+ box-shadow: 0 0 0 var(--ring-width) var(--ring-color);
}
.password-input-wrapper {
@@ -2453,7 +2453,7 @@ onMounted(() => {
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
- color: #374151;
+ color: var(--text);
cursor: pointer;
}
@@ -2502,7 +2502,7 @@ onMounted(() => {
}
.confirm-icon.danger {
- background: #fef2f2;
+ background: var(--color-danger-50);
color: #ef4444;
}