zephyrfs/zephyrfs-web / cd963ec

Browse files

production deployment with monitoring & zero-downtime deployment

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
cd963ec82dd509497696c49ce480025273b415df
Parents
6f3d442
Tree
0e6f035

22 changed files

StatusFile+-
A .env.production.example 50 0
M README.md 166 43
A client/Dockerfile 53 0
A client/nginx.conf 129 0
A client/src/components/ErrorBoundary.vue 248 0
A client/src/composables/useErrorHandler.ts 278 0
A client/src/views/SettingsView.vue 431 0
A deploy/production.yml 257 0
M docker-compose.yml 79 6
A monitoring/grafana/dashboards/dashboard.yml 13 0
A monitoring/grafana/datasources/prometheus.yml 21 0
A monitoring/loki.yml 64 0
A monitoring/prometheus.yml 49 0
A monitoring/promtail.yml 79 0
A nginx/nginx.conf 160 0
A nginx/production.conf 212 0
A scripts/deploy.sh 209 0
A scripts/generate-ssl.sh 53 0
A scripts/health-check.sh 143 0
M server/src/index.ts 4 0
A server/src/middleware/cache.ts 349 0
A server/src/middleware/performance.ts 231 0
.env.production.exampleadded
@@ -0,0 +1,50 @@
1
+# ZephyrFS Production Configuration
2
+
3
+# Core Configuration
4
+NODE_ENV=production
5
+PORT=3000
6
+HOST=0.0.0.0
7
+
8
+# ZephyrFS Node Configuration
9
+ZEPHYRFS_NODE_URL=http://zephyrfs-node:8080
10
+ZEPHYRFS_NODE_TIMEOUT=30000
11
+
12
+# Security
13
+JWT_SECRET=your-super-secure-jwt-secret-here
14
+CORS_ORIGINS=https://your-domain.com,https://api.your-domain.com
15
+
16
+# File Upload Limits
17
+MAX_FILE_SIZE=1073741824  # 1GB in bytes
18
+MAX_CONCURRENT_UPLOADS=10
19
+
20
+# Logging
21
+LOG_LEVEL=info
22
+
23
+# Storage Paths
24
+DATA_PATH=/opt/zephyrfs/data
25
+SSL_CERTS_PATH=/opt/zephyrfs/ssl
26
+
27
+# Monitoring
28
+GRAFANA_PASSWORD=admin
29
+
30
+# Performance Settings
31
+CACHE_MAX_SIZE=104857600  # 100MB
32
+CACHE_TTL=300000          # 5 minutes
33
+
34
+# Database (if using external DB)
35
+# DATABASE_URL=postgresql://user:pass@localhost:5432/zephyrfs
36
+
37
+# Redis (if using external cache)
38
+# REDIS_URL=redis://localhost:6379
39
+
40
+# Email (for notifications)
41
+# SMTP_HOST=smtp.gmail.com
42
+# SMTP_PORT=587
43
+# SMTP_USER=your-email@gmail.com
44
+# SMTP_PASS=your-app-password
45
+
46
+# S3 Backup (optional)
47
+# S3_BUCKET=zephyrfs-backups
48
+# S3_ACCESS_KEY=your-access-key
49
+# S3_SECRET_KEY=your-secret-key
50
+# S3_REGION=us-east-1
README.mdmodified
@@ -1,53 +1,56 @@
11
 # ZephyrFS Web Interface
22
 
3
-A modern web interface and WebDAV server for ZephyrFS distributed storage system.
3
+A modern, responsive web interface for ZephyrFS with zero-knowledge encryption support and production-ready deployment.
44
 
55
 ## Features
66
 
7
-- **RESTful API** - Complete file management operations
8
-- **JWT Authentication** - Secure token-based authentication
9
-- **WebDAV Server** - Native OS integration for mounting as network drive
10
-- **Real-time Updates** - WebSocket-based status monitoring
11
-- **Zero-Knowledge Security** - Preserves ZephyrFS encryption architecture
12
-- **High Performance** - Sub-2-second response times with streaming support
7
+- **Modern Web UI**: Vue.js 3 + TypeScript + Tailwind CSS
8
+- **Zero-Knowledge Encryption**: Client-side encryption with capability-based access
9
+- **Real-time Operations**: WebSocket support for live updates
10
+- **File Management**: Upload, download, organize files with folder support
11
+- **Batch Operations**: Multi-select for bulk operations
12
+- **File Preview**: Built-in preview for images, documents, and media
13
+- **Mobile Responsive**: Progressive Web App (PWA) support
14
+- **WebDAV Server**: Native OS integration for mounting as network drive
15
+- **Production Ready**: Docker containerization with monitoring
16
+- **High Performance**: Sub-500ms response times with intelligent caching
1317
 
1418
 ## Quick Start
1519
 
1620
 ### Development
1721
 
18
-1. **Install dependencies**
19
-   ```bash
20
-   cd server
21
-   npm install
22
-   ```
22
+```bash
23
+# Install dependencies
24
+npm install
2325
 
24
-2. **Configure environment**
25
-   ```bash
26
-   cp .env.example .env
27
-   # Edit .env with your settings
28
-   ```
26
+# Start development servers
27
+npm run dev:server    # Backend on :3000
28
+npm run dev:client    # Frontend on :5173
29
+```
2930
 
30
-3. **Start development server**
31
-   ```bash
32
-   npm run dev
33
-   ```
31
+### Production Deployment
3432
 
35
-### Production
33
+```bash
34
+# Copy and configure environment
35
+cp .env.production.example .env.production
36
+# Edit .env.production with your settings
3637
 
37
-1. **Build the application**
38
-   ```bash
39
-   npm run build
40
-   ```
38
+# Deploy with Docker Swarm
39
+./scripts/deploy.sh deploy
4140
 
42
-2. **Start production server**
43
-   ```bash
44
-   npm start
45
-   ```
41
+# Check health and performance
42
+./scripts/health-check.sh
43
+```
4644
 
47
-### Docker
45
+### Alternative Deployment
4846
 
4947
 ```bash
50
-docker-compose up -d
48
+# Docker Compose
49
+docker-compose -f deploy/production.yml up -d
50
+
51
+# Manual deployment
52
+npm run build
53
+npm run start:prod
5154
 ```
5255
 
5356
 ## API Endpoints
@@ -58,15 +61,21 @@ docker-compose up -d
5861
 - `POST /api/auth/logout` - Logout and invalidate session
5962
 - `GET /api/auth/me` - Get current user info
6063
 
61
-### Files
62
-- `GET /api/files` - List files in directory
63
-- `POST /api/files/upload` - Upload file
64
+### Core Operations
65
+- `GET /api/files` - List files and folders
66
+- `POST /api/files/upload` - Upload files
6467
 - `GET /api/files/:id/download` - Download file
65
-- `GET /api/files/:id/info` - Get file metadata
66
-- `DELETE /api/files/:id` - Delete file
68
+- `DELETE /api/files/:id` - Delete file/folder
69
+- `POST /api/folders` - Create folder
70
+
71
+### Batch Operations
72
+- `POST /api/bulk/download` - Bulk download as ZIP
73
+- `DELETE /api/bulk/delete` - Bulk delete
6774
 
68
-### Status
75
+### Monitoring
6976
 - `GET /api/health` - Health check
77
+- `GET /api/metrics` - Performance metrics
78
+- `GET /api/cache/stats` - Cache statistics
7079
 - `GET /api/status/network` - Network status
7180
 - `GET /api/status/node` - Node status
7281
 - `GET /api/status/ws` - WebSocket status updates
@@ -138,10 +147,124 @@ Web Client/WebDAV → Fastify API → ZephyrFS Client → ZephyrFS Node
138147
 - Input validation with Zod schemas
139148
 - Secure error handling (no info leakage)
140149
 
150
+## Architecture
151
+
152
+```
153
+┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
154
+│   Vue.js Client │────│  Fastify Server │────│  ZephyrFS Node  │
155
+│                 │    │                 │    │                 │
156
+│ • File Browser  │    │ • REST API      │    │ • Storage Layer │
157
+│ • Upload/DL     │    │ • WebSocket     │    │ • Encryption    │
158
+│ • Auth UI       │    │ • JWT Auth      │    │ • Deduplication │
159
+│ • Real-time     │    │ • Caching       │    │ • Capabilities  │
160
+└─────────────────┘    └─────────────────┘    └─────────────────┘
161
+```
162
+
141163
 ## Performance
142164
 
143
-- Sub-200ms API response times
144
-- Streaming file uploads/downloads
145
-- Efficient chunked transfer
146
-- WebSocket real-time updates
147
-- Connection pooling and timeouts
165
+- **Response Times**: Sub-500ms for all operations
166
+- **Caching**: Intelligent multi-layer caching
167
+- **Compression**: Gzip compression for all responses
168
+- **CDN Ready**: Static asset optimization
169
+- **Monitoring**: Prometheus + Grafana metrics
170
+- **Streaming**: Efficient file uploads/downloads
171
+- **WebSocket**: Real-time updates
172
+- **Connection Pooling**: Optimized HTTP connections
173
+
174
+## Security
175
+
176
+- **Zero-Knowledge**: Files encrypted client-side
177
+- **JWT Authentication**: Secure token-based auth
178
+- **CORS Protection**: Configurable origin restrictions
179
+- **Rate Limiting**: API endpoint protection
180
+- **TLS Termination**: HTTPS with nginx proxy
181
+- **CSP Headers**: Content Security Policy
182
+- **Input Validation**: Zod schema validation
183
+- **Secure Headers**: HSTS, X-Frame-Options, etc.
184
+
185
+## Deployment Options
186
+
187
+### Docker Swarm (Recommended)
188
+```bash
189
+./scripts/deploy.sh deploy
190
+```
191
+
192
+### Manual Docker
193
+```bash
194
+docker-compose -f deploy/production.yml up -d
195
+```
196
+
197
+### Kubernetes
198
+```bash
199
+# Coming soon - K8s manifests
200
+kubectl apply -f deploy/k8s/
201
+```
202
+
203
+## Configuration
204
+
205
+### Environment Variables
206
+
207
+| Variable | Description | Default |
208
+|----------|-------------|---------|
209
+| `NODE_ENV` | Environment mode | `production` |
210
+| `PORT` | Server port | `3000` |
211
+| `ZEPHYRFS_NODE_URL` | ZephyrFS node URL | `http://zephyrfs-node:8080` |
212
+| `JWT_SECRET` | JWT signing secret | (required) |
213
+| `CORS_ORIGINS` | Allowed CORS origins | `https://your-domain.com` |
214
+| `MAX_FILE_SIZE` | Max upload size | `1073741824` (1GB) |
215
+| `DATA_PATH` | Data storage path | `/opt/zephyrfs/data` |
216
+
217
+### SSL Certificates
218
+
219
+Place SSL certificates in `${SSL_CERTS_PATH}/`:
220
+- `cert.pem` - Certificate file
221
+- `key.pem` - Private key file
222
+
223
+Or use Let's Encrypt with:
224
+```bash
225
+certbot certonly --webroot -w /var/www/html -d your-domain.com
226
+```
227
+
228
+## Monitoring
229
+
230
+Access monitoring dashboards:
231
+- **Grafana**: http://localhost:3001 (admin/admin)
232
+- **Prometheus**: http://localhost:9090
233
+- **Logs**: `docker service logs zephyrfs_loki`
234
+
235
+## Development
236
+
237
+### Project Structure
238
+```
239
+zephyrfs-web/
240
+├── client/           # Vue.js frontend
241
+│   ├── src/
242
+│   │   ├── components/
243
+│   │   ├── views/
244
+│   │   ├── stores/
245
+│   │   └── utils/
246
+│   └── public/
247
+├── server/           # Node.js backend
248
+│   ├── src/
249
+│   │   ├── routes/
250
+│   │   ├── middleware/
251
+│   │   └── integration/
252
+│   └── tests/
253
+├── deploy/           # Production configs
254
+├── monitoring/       # Prometheus/Grafana
255
+└── scripts/          # Deployment scripts
256
+```
257
+
258
+### Testing
259
+```bash
260
+npm test
261
+```
262
+
263
+### Contributing
264
+
265
+1. Fork the repository
266
+2. Create feature branch: `git checkout -b feature/name`
267
+3. Run tests: `npm test`
268
+4. Commit changes: `git commit -m "feat: description"`
269
+5. Push branch: `git push origin feature/name`
270
+6. Create Pull Request
client/Dockerfileadded
@@ -0,0 +1,53 @@
1
+# Multi-stage build for Vue.js client
2
+FROM node:18-alpine AS builder
3
+
4
+WORKDIR /app
5
+
6
+# Copy package files
7
+COPY package*.json ./
8
+COPY ../shared ./shared
9
+
10
+# Install dependencies
11
+RUN npm ci --only=production
12
+
13
+# Copy source code
14
+COPY . .
15
+
16
+# Build the application
17
+RUN npm run build
18
+
19
+# Production stage with nginx
20
+FROM nginx:alpine
21
+
22
+# Copy built assets
23
+COPY --from=builder /app/dist /usr/share/nginx/html
24
+
25
+# Copy nginx configuration
26
+COPY nginx.conf /etc/nginx/nginx.conf
27
+
28
+# Create non-root user
29
+RUN addgroup -g 1001 -S nodejs && \
30
+    adduser -S zephyrfs -u 1001 -G nodejs
31
+
32
+# Set ownership
33
+RUN chown -R zephyrfs:nodejs /usr/share/nginx/html && \
34
+    chown -R zephyrfs:nodejs /var/cache/nginx && \
35
+    chown -R zephyrfs:nodejs /var/log/nginx && \
36
+    chown -R zephyrfs:nodejs /etc/nginx/conf.d
37
+
38
+# Make directories writable
39
+RUN touch /var/run/nginx.pid && \
40
+    chown -R zephyrfs:nodejs /var/run/nginx.pid
41
+
42
+# Switch to non-root user
43
+USER zephyrfs
44
+
45
+# Expose port
46
+EXPOSE 8080
47
+
48
+# Health check
49
+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
50
+  CMD wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1
51
+
52
+# Start nginx
53
+CMD ["nginx", "-g", "daemon off;"]
client/nginx.confadded
@@ -0,0 +1,129 @@
1
+user nginx;
2
+worker_processes auto;
3
+error_log /var/log/nginx/error.log notice;
4
+pid /var/run/nginx.pid;
5
+
6
+events {
7
+    worker_connections 1024;
8
+    use epoll;
9
+    multi_accept on;
10
+}
11
+
12
+http {
13
+    include /etc/nginx/mime.types;
14
+    default_type application/octet-stream;
15
+
16
+    # Logging format
17
+    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
18
+                    '$status $body_bytes_sent "$http_referer" '
19
+                    '"$http_user_agent" "$http_x_forwarded_for" '
20
+                    'rt=$request_time uct="$upstream_connect_time" '
21
+                    'uht="$upstream_header_time" urt="$upstream_response_time"';
22
+
23
+    access_log /var/log/nginx/access.log main;
24
+
25
+    # Performance optimizations
26
+    sendfile on;
27
+    tcp_nopush on;
28
+    tcp_nodelay on;
29
+    keepalive_timeout 65;
30
+    types_hash_max_size 2048;
31
+    client_max_body_size 1G;
32
+
33
+    # Gzip compression
34
+    gzip on;
35
+    gzip_vary on;
36
+    gzip_min_length 1024;
37
+    gzip_proxied any;
38
+    gzip_comp_level 6;
39
+    gzip_types
40
+        text/plain
41
+        text/css
42
+        text/xml
43
+        text/javascript
44
+        application/json
45
+        application/javascript
46
+        application/xml+rss
47
+        application/atom+xml
48
+        image/svg+xml;
49
+
50
+    # Security headers
51
+    add_header X-Frame-Options DENY always;
52
+    add_header X-Content-Type-Options nosniff always;
53
+    add_header X-XSS-Protection "1; mode=block" always;
54
+    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
55
+
56
+    server {
57
+        listen 8080;
58
+        listen [::]:8080;
59
+        server_name _;
60
+        root /usr/share/nginx/html;
61
+        index index.html;
62
+
63
+        # Security
64
+        server_tokens off;
65
+
66
+        # Static assets with long cache
67
+        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
68
+            expires 1y;
69
+            add_header Cache-Control "public, immutable";
70
+            add_header X-Content-Type-Options nosniff;
71
+        }
72
+
73
+        # API proxy to backend
74
+        location /api/ {
75
+            proxy_pass http://web-server:3000;
76
+            proxy_http_version 1.1;
77
+            proxy_set_header Upgrade $http_upgrade;
78
+            proxy_set_header Connection 'upgrade';
79
+            proxy_set_header Host $host;
80
+            proxy_set_header X-Real-IP $remote_addr;
81
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
82
+            proxy_set_header X-Forwarded-Proto $scheme;
83
+            proxy_cache_bypass $http_upgrade;
84
+            proxy_read_timeout 300;
85
+            proxy_connect_timeout 300;
86
+            proxy_send_timeout 300;
87
+        }
88
+
89
+        # WebSocket support for API
90
+        location /api/status/ws {
91
+            proxy_pass http://web-server:3000;
92
+            proxy_http_version 1.1;
93
+            proxy_set_header Upgrade $http_upgrade;
94
+            proxy_set_header Connection "Upgrade";
95
+            proxy_set_header Host $host;
96
+            proxy_set_header X-Real-IP $remote_addr;
97
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
98
+            proxy_set_header X-Forwarded-Proto $scheme;
99
+        }
100
+
101
+        # SPA fallback - serve index.html for all routes
102
+        location / {
103
+            try_files $uri $uri/ /index.html;
104
+            add_header Cache-Control "no-cache, no-store, must-revalidate";
105
+            add_header Pragma "no-cache";
106
+            add_header Expires "0";
107
+        }
108
+
109
+        # Health check endpoint
110
+        location /health {
111
+            access_log off;
112
+            return 200 "healthy\n";
113
+            add_header Content-Type text/plain;
114
+        }
115
+
116
+        # Block access to sensitive files
117
+        location ~ /\. {
118
+            deny all;
119
+            access_log off;
120
+            log_not_found off;
121
+        }
122
+
123
+        location ~ ^/(package\.json|Dockerfile|docker-compose\.yml)$ {
124
+            deny all;
125
+            access_log off;
126
+            log_not_found off;
127
+        }
128
+    }
129
+}
client/src/components/ErrorBoundary.vueadded
@@ -0,0 +1,248 @@
1
+<template>
2
+  <div v-if="hasError" class="error-boundary">
3
+    <div class="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center px-4">
4
+      <div class="max-w-md w-full">
5
+        <!-- Error icon -->
6
+        <div class="text-center mb-6">
7
+          <ExclamationTriangleIcon class="w-16 h-16 text-red-500 mx-auto mb-4" />
8
+          <h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
9
+            Something went wrong
10
+          </h1>
11
+          <p class="text-gray-600 dark:text-gray-400">
12
+            We encountered an unexpected error. This has been reported to our team.
13
+          </p>
14
+        </div>
15
+
16
+        <!-- Error details (development only) -->
17
+        <div v-if="showDetails && errorInfo" class="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md">
18
+          <details class="text-sm">
19
+            <summary class="font-medium text-red-800 dark:text-red-200 cursor-pointer">
20
+              Error Details
21
+            </summary>
22
+            <div class="mt-2 text-red-700 dark:text-red-300 space-y-2">
23
+              <div>
24
+                <strong>Message:</strong> {{ errorInfo.message }}
25
+              </div>
26
+              <div v-if="errorInfo.stack">
27
+                <strong>Stack trace:</strong>
28
+                <pre class="mt-1 text-xs bg-red-100 dark:bg-red-900/30 p-2 rounded overflow-x-auto">{{ errorInfo.stack }}</pre>
29
+              </div>
30
+              <div>
31
+                <strong>Timestamp:</strong> {{ new Date(errorInfo.timestamp).toLocaleString() }}
32
+              </div>
33
+              <div v-if="errorInfo.context">
34
+                <strong>Context:</strong>
35
+                <pre class="mt-1 text-xs bg-red-100 dark:bg-red-900/30 p-2 rounded overflow-x-auto">{{ JSON.stringify(errorInfo.context, null, 2) }}</pre>
36
+              </div>
37
+            </div>
38
+          </details>
39
+        </div>
40
+
41
+        <!-- Actions -->
42
+        <div class="flex flex-col sm:flex-row gap-3">
43
+          <button
44
+            @click="handleReload"
45
+            class="btn btn-primary flex-1"
46
+          >
47
+            <ArrowPathIcon class="w-4 h-4 mr-2" />
48
+            Reload Page
49
+          </button>
50
+
51
+          <button
52
+            @click="handleReset"
53
+            class="btn btn-outline flex-1"
54
+          >
55
+            Reset Application
56
+          </button>
57
+        </div>
58
+
59
+        <!-- Additional actions -->
60
+        <div class="mt-4 text-center space-y-2">
61
+          <button
62
+            v-if="errorInfo?.retryable"
63
+            @click="handleRetry"
64
+            class="text-sm text-primary-600 hover:text-primary-700 dark:text-primary-400"
65
+          >
66
+            Try Last Action Again
67
+          </button>
68
+
69
+          <div class="text-xs text-gray-500 dark:text-gray-400">
70
+            Error ID: {{ errorInfo?.id || 'unknown' }}
71
+          </div>
72
+        </div>
73
+
74
+        <!-- Report issue link -->
75
+        <div class="mt-6 text-center">
76
+          <a
77
+            href="https://github.com/anthropics/claude-code/issues"
78
+            target="_blank"
79
+            rel="noopener noreferrer"
80
+            class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
81
+          >
82
+            Report this issue on GitHub →
83
+          </a>
84
+        </div>
85
+      </div>
86
+    </div>
87
+  </div>
88
+
89
+  <slot v-else />
90
+</template>
91
+
92
+<script setup lang="ts">
93
+import { ref, onErrorCaptured, onMounted } from 'vue'
94
+import { ExclamationTriangleIcon, ArrowPathIcon } from '@heroicons/vue/24/outline'
95
+import { useErrorHandler, type ErrorInfo } from '@/composables/useErrorHandler'
96
+
97
+const hasError = ref(false)
98
+const errorInfo = ref<ErrorInfo | null>(null)
99
+const showDetails = ref(import.meta.env.DEV)
100
+const lastAction = ref<Function | null>(null)
101
+
102
+const { handleError } = useErrorHandler()
103
+
104
+// Capture Vue component errors
105
+onErrorCaptured((error: Error, instance: any, info: string) => {
106
+  const errorDetails = handleError(error, {
107
+    component: instance?.$options.name || 'Unknown',
108
+    errorInfo: info,
109
+    timestamp: Date.now(),
110
+  })
111
+
112
+  hasError.value = true
113
+  errorInfo.value = {
114
+    ...errorDetails,
115
+    stack: error.stack,
116
+    context: {
117
+      ...errorDetails.context,
118
+      vueInfo: info,
119
+    },
120
+  }
121
+
122
+  // Prevent the error from propagating further
123
+  return false
124
+})
125
+
126
+// Handle global errors
127
+onMounted(() => {
128
+  const originalOnError = window.onerror
129
+  const originalOnUnhandledRejection = window.onunhandledrejection
130
+
131
+  window.onerror = (message, source, lineno, colno, error) => {
132
+    if (error) {
133
+      const errorDetails = handleError(error, {
134
+        source,
135
+        lineno,
136
+        colno,
137
+        timestamp: Date.now(),
138
+      })
139
+
140
+      hasError.value = true
141
+      errorInfo.value = {
142
+        ...errorDetails,
143
+        stack: error.stack,
144
+      }
145
+    }
146
+
147
+    // Call original handler
148
+    if (originalOnError) {
149
+      return originalOnError(message, source, lineno, colno, error)
150
+    }
151
+    return false
152
+  }
153
+
154
+  window.onunhandledrejection = (event) => {
155
+    const errorDetails = handleError(event.reason, {
156
+      type: 'unhandled_promise_rejection',
157
+      timestamp: Date.now(),
158
+    })
159
+
160
+    hasError.value = true
161
+    errorInfo.value = errorDetails
162
+
163
+    // Call original handler
164
+    if (originalOnUnhandledRejection) {
165
+      return originalOnUnhandledRejection(event)
166
+    }
167
+  }
168
+})
169
+
170
+function handleReload() {
171
+  window.location.reload()
172
+}
173
+
174
+function handleReset() {
175
+  // Clear all localStorage/sessionStorage
176
+  localStorage.clear()
177
+  sessionStorage.clear()
178
+
179
+  // Clear any cached data
180
+  if ('caches' in window) {
181
+    caches.keys().then(names => {
182
+      names.forEach(name => {
183
+        caches.delete(name)
184
+      })
185
+    })
186
+  }
187
+
188
+  // Reload the page
189
+  window.location.reload()
190
+}
191
+
192
+function handleRetry() {
193
+  if (lastAction.value) {
194
+    hasError.value = false
195
+    errorInfo.value = null
196
+
197
+    try {
198
+      lastAction.value()
199
+    } catch (error) {
200
+      // If retry fails, show error again
201
+      setTimeout(() => {
202
+        const errorDetails = handleError(error, {
203
+          retry: true,
204
+          timestamp: Date.now(),
205
+        })
206
+        hasError.value = true
207
+        errorInfo.value = errorDetails
208
+      }, 100)
209
+    }
210
+  }
211
+}
212
+
213
+// Expose method to manually trigger error boundary
214
+function triggerError(error: Error, action?: Function) {
215
+  const errorDetails = handleError(error, {
216
+    manual: true,
217
+    timestamp: Date.now(),
218
+  })
219
+
220
+  hasError.value = true
221
+  errorInfo.value = errorDetails
222
+  lastAction.value = action || null
223
+}
224
+
225
+defineExpose({
226
+  triggerError,
227
+})
228
+</script>
229
+
230
+<style scoped>
231
+.error-boundary {
232
+  position: fixed;
233
+  inset: 0;
234
+  z-index: 9999;
235
+  background: rgba(0, 0, 0, 0.1);
236
+  backdrop-filter: blur(4px);
237
+}
238
+
239
+pre {
240
+  font-family: 'JetBrains Mono', 'Menlo', 'Monaco', monospace;
241
+  font-size: 10px;
242
+  line-height: 1.4;
243
+}
244
+
245
+details[open] summary {
246
+  margin-bottom: 8px;
247
+}
248
+</style>
client/src/composables/useErrorHandler.tsadded
@@ -0,0 +1,278 @@
1
+import { ref } from 'vue'
2
+
3
+export interface ErrorInfo {
4
+  id: string
5
+  type: 'network' | 'validation' | 'auth' | 'server' | 'client' | 'unknown'
6
+  message: string
7
+  details?: string
8
+  timestamp: Date
9
+  retryable: boolean
10
+  context?: Record<string, any>
11
+}
12
+
13
+const errors = ref<ErrorInfo[]>([])
14
+
15
+export function useErrorHandler() {
16
+  function handleError(error: any, context?: Record<string, any>): ErrorInfo {
17
+    const errorInfo = parseError(error, context)
18
+
19
+    // Store error for debugging
20
+    errors.value.unshift(errorInfo)
21
+
22
+    // Keep only last 50 errors
23
+    if (errors.value.length > 50) {
24
+      errors.value.splice(50)
25
+    }
26
+
27
+    // Show user notification
28
+    showErrorNotification(errorInfo)
29
+
30
+    // Log error for development
31
+    if (import.meta.env.DEV) {
32
+      console.error('Error handled:', errorInfo, error)
33
+    }
34
+
35
+    return errorInfo
36
+  }
37
+
38
+  function parseError(error: any, context?: Record<string, any>): ErrorInfo {
39
+    const id = crypto.randomUUID()
40
+    const timestamp = new Date()
41
+
42
+    // Network errors
43
+    if (error.name === 'NetworkError' || error.code === 'NETWORK_ERROR') {
44
+      return {
45
+        id,
46
+        type: 'network',
47
+        message: 'Network connection failed',
48
+        details: 'Please check your internet connection and try again.',
49
+        timestamp,
50
+        retryable: true,
51
+        context,
52
+      }
53
+    }
54
+
55
+    // Fetch/Axios errors
56
+    if (error.response) {
57
+      const status = error.response.status
58
+      const data = error.response.data
59
+
60
+      if (status === 401) {
61
+        return {
62
+          id,
63
+          type: 'auth',
64
+          message: 'Authentication required',
65
+          details: 'Please log in again to continue.',
66
+          timestamp,
67
+          retryable: false,
68
+          context,
69
+        }
70
+      }
71
+
72
+      if (status === 403) {
73
+        return {
74
+          id,
75
+          type: 'auth',
76
+          message: 'Access denied',
77
+          details: 'You do not have permission to perform this action.',
78
+          timestamp,
79
+          retryable: false,
80
+          context,
81
+        }
82
+      }
83
+
84
+      if (status === 404) {
85
+        return {
86
+          id,
87
+          type: 'client',
88
+          message: 'Resource not found',
89
+          details: 'The requested resource could not be found.',
90
+          timestamp,
91
+          retryable: false,
92
+          context,
93
+        }
94
+      }
95
+
96
+      if (status === 413) {
97
+        return {
98
+          id,
99
+          type: 'validation',
100
+          message: 'File too large',
101
+          details: 'The file you are trying to upload exceeds the maximum size limit.',
102
+          timestamp,
103
+          retryable: false,
104
+          context,
105
+        }
106
+      }
107
+
108
+      if (status === 422) {
109
+        return {
110
+          id,
111
+          type: 'validation',
112
+          message: 'Invalid data',
113
+          details: data?.message || 'Please check your input and try again.',
114
+          timestamp,
115
+          retryable: false,
116
+          context,
117
+        }
118
+      }
119
+
120
+      if (status === 429) {
121
+        return {
122
+          id,
123
+          type: 'client',
124
+          message: 'Too many requests',
125
+          details: 'Please wait a moment before trying again.',
126
+          timestamp,
127
+          retryable: true,
128
+          context,
129
+        }
130
+      }
131
+
132
+      if (status >= 500) {
133
+        return {
134
+          id,
135
+          type: 'server',
136
+          message: 'Server error',
137
+          details: 'Something went wrong on our end. Please try again later.',
138
+          timestamp,
139
+          retryable: true,
140
+          context,
141
+        }
142
+      }
143
+
144
+      return {
145
+        id,
146
+        type: 'unknown',
147
+        message: data?.message || 'An unexpected error occurred',
148
+        details: data?.details || `HTTP ${status} error`,
149
+        timestamp,
150
+        retryable: status >= 500,
151
+        context,
152
+      }
153
+    }
154
+
155
+    // Validation errors
156
+    if (error.name === 'ValidationError' || error.type === 'validation') {
157
+      return {
158
+        id,
159
+        type: 'validation',
160
+        message: error.message || 'Validation failed',
161
+        details: 'Please check your input and try again.',
162
+        timestamp,
163
+        retryable: false,
164
+        context,
165
+      }
166
+    }
167
+
168
+    // Timeout errors
169
+    if (error.name === 'TimeoutError' || error.code === 'ECONNABORTED') {
170
+      return {
171
+        id,
172
+        type: 'network',
173
+        message: 'Request timeout',
174
+        details: 'The request took too long to complete. Please try again.',
175
+        timestamp,
176
+        retryable: true,
177
+        context,
178
+      }
179
+    }
180
+
181
+    // Generic error fallback
182
+    return {
183
+      id,
184
+      type: 'unknown',
185
+      message: error.message || 'An unexpected error occurred',
186
+      details: 'Please try again or contact support if the problem persists.',
187
+      timestamp,
188
+      retryable: true,
189
+      context,
190
+    }
191
+  }
192
+
193
+  function showErrorNotification(errorInfo: ErrorInfo) {
194
+    if (!window.$notify) return
195
+
196
+    const title = getErrorTitle(errorInfo.type)
197
+    const duration = errorInfo.retryable ? 8000 : 5000
198
+
199
+    window.$notify.error(errorInfo.message, title, duration)
200
+  }
201
+
202
+  function getErrorTitle(type: ErrorInfo['type']): string {
203
+    switch (type) {
204
+      case 'network':
205
+        return 'Connection Error'
206
+      case 'auth':
207
+        return 'Authentication Error'
208
+      case 'validation':
209
+        return 'Validation Error'
210
+      case 'server':
211
+        return 'Server Error'
212
+      case 'client':
213
+        return 'Client Error'
214
+      default:
215
+        return 'Error'
216
+    }
217
+  }
218
+
219
+  function clearErrors() {
220
+    errors.value = []
221
+  }
222
+
223
+  function getRecentErrors(limit = 10) {
224
+    return errors.value.slice(0, limit)
225
+  }
226
+
227
+  function retry(originalFunction: Function, errorInfo: ErrorInfo) {
228
+    if (!errorInfo.retryable) {
229
+      window.$notify?.warning('This action cannot be retried')
230
+      return
231
+    }
232
+
233
+    try {
234
+      return originalFunction()
235
+    } catch (error) {
236
+      handleError(error, { ...errorInfo.context, retry: true })
237
+    }
238
+  }
239
+
240
+  return {
241
+    handleError,
242
+    clearErrors,
243
+    getRecentErrors,
244
+    retry,
245
+    errors: errors.value,
246
+  }
247
+}
248
+
249
+// Global error handler for uncaught errors
250
+export function setupGlobalErrorHandler() {
251
+  const { handleError } = useErrorHandler()
252
+
253
+  // Handle uncaught JavaScript errors
254
+  window.addEventListener('error', (event) => {
255
+    handleError(event.error, {
256
+      filename: event.filename,
257
+      lineno: event.lineno,
258
+      colno: event.colno,
259
+    })
260
+  })
261
+
262
+  // Handle unhandled promise rejections
263
+  window.addEventListener('unhandledrejection', (event) => {
264
+    handleError(event.reason, {
265
+      type: 'unhandled_promise_rejection',
266
+    })
267
+  })
268
+
269
+  // Handle Vue errors (if using Vue)
270
+  if (window.Vue) {
271
+    window.Vue.config.errorHandler = (error: Error, instance: any, info: string) => {
272
+      handleError(error, {
273
+        component: instance?.$options.name || 'Unknown',
274
+        info,
275
+      })
276
+    }
277
+  }
278
+}
client/src/views/SettingsView.vueadded
@@ -0,0 +1,431 @@
1
+<template>
2
+  <AppLayout>
3
+    <div class="settings-view">
4
+      <div class="max-w-4xl mx-auto py-8 px-4">
5
+        <h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-8">Settings</h1>
6
+
7
+        <!-- Settings sections -->
8
+        <div class="space-y-8">
9
+          <!-- Appearance -->
10
+          <div class="card">
11
+            <div class="card-header">
12
+              <h2 class="text-lg font-semibold text-gray-900 dark:text-white">Appearance</h2>
13
+              <p class="text-sm text-gray-600 dark:text-gray-400">Customize the look and feel</p>
14
+            </div>
15
+            <div class="card-body space-y-4">
16
+              <!-- Theme -->
17
+              <div class="flex items-center justify-between">
18
+                <div>
19
+                  <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Theme</label>
20
+                  <p class="text-xs text-gray-500 dark:text-gray-400">Choose your preferred color scheme</p>
21
+                </div>
22
+                <select
23
+                  v-model="settings.theme"
24
+                  @change="updateSetting('theme', $event.target.value)"
25
+                  class="input w-32"
26
+                >
27
+                  <option value="system">System</option>
28
+                  <option value="light">Light</option>
29
+                  <option value="dark">Dark</option>
30
+                </select>
31
+              </div>
32
+
33
+              <!-- Language -->
34
+              <div class="flex items-center justify-between">
35
+                <div>
36
+                  <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Language</label>
37
+                  <p class="text-xs text-gray-500 dark:text-gray-400">Select your preferred language</p>
38
+                </div>
39
+                <select
40
+                  v-model="settings.language"
41
+                  @change="updateSetting('language', $event.target.value)"
42
+                  class="input w-32"
43
+                >
44
+                  <option value="en">English</option>
45
+                  <option value="es">Español</option>
46
+                  <option value="fr">Français</option>
47
+                  <option value="de">Deutsch</option>
48
+                </select>
49
+              </div>
50
+
51
+              <!-- File view -->
52
+              <div class="flex items-center justify-between">
53
+                <div>
54
+                  <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Default file view</label>
55
+                  <p class="text-xs text-gray-500 dark:text-gray-400">How files are displayed by default</p>
56
+                </div>
57
+                <select
58
+                  v-model="settings.defaultFileView"
59
+                  @change="updateSetting('defaultFileView', $event.target.value)"
60
+                  class="input w-32"
61
+                >
62
+                  <option value="grid">Grid</option>
63
+                  <option value="list">List</option>
64
+                </select>
65
+              </div>
66
+            </div>
67
+          </div>
68
+
69
+          <!-- Files & Storage -->
70
+          <div class="card">
71
+            <div class="card-header">
72
+              <h2 class="text-lg font-semibold text-gray-900 dark:text-white">Files & Storage</h2>
73
+              <p class="text-sm text-gray-600 dark:text-gray-400">Configure file handling preferences</p>
74
+            </div>
75
+            <div class="card-body space-y-4">
76
+              <!-- Auto-encrypt -->
77
+              <div class="flex items-center justify-between">
78
+                <div>
79
+                  <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Auto-encrypt uploads</label>
80
+                  <p class="text-xs text-gray-500 dark:text-gray-400">Automatically encrypt all uploaded files</p>
81
+                </div>
82
+                <label class="relative inline-flex items-center cursor-pointer">
83
+                  <input
84
+                    v-model="settings.autoEncrypt"
85
+                    @change="updateSetting('autoEncrypt', $event.target.checked)"
86
+                    type="checkbox"
87
+                    class="sr-only peer"
88
+                  />
89
+                  <div class="w-11 h-6 bg-gray-200 dark:bg-gray-700 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary-600"></div>
90
+                </label>
91
+              </div>
92
+
93
+              <!-- Default upload folder -->
94
+              <div class="flex items-center justify-between">
95
+                <div>
96
+                  <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Default upload folder</label>
97
+                  <p class="text-xs text-gray-500 dark:text-gray-400">Where new files are uploaded by default</p>
98
+                </div>
99
+                <input
100
+                  v-model="settings.defaultUploadFolder"
101
+                  @blur="updateSetting('defaultUploadFolder', $event.target.value)"
102
+                  type="text"
103
+                  placeholder="/uploads"
104
+                  class="input w-48"
105
+                />
106
+              </div>
107
+
108
+              <!-- Max file size -->
109
+              <div class="flex items-center justify-between">
110
+                <div>
111
+                  <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Max upload size</label>
112
+                  <p class="text-xs text-gray-500 dark:text-gray-400">Maximum file size for uploads</p>
113
+                </div>
114
+                <select
115
+                  v-model="settings.maxUploadSize"
116
+                  @change="updateSetting('maxUploadSize', parseInt($event.target.value))"
117
+                  class="input w-32"
118
+                >
119
+                  <option :value="100 * 1024 * 1024">100 MB</option>
120
+                  <option :value="500 * 1024 * 1024">500 MB</option>
121
+                  <option :value="1024 * 1024 * 1024">1 GB</option>
122
+                  <option :value="5 * 1024 * 1024 * 1024">5 GB</option>
123
+                </select>
124
+              </div>
125
+            </div>
126
+          </div>
127
+
128
+          <!-- Privacy & Security -->
129
+          <div class="card">
130
+            <div class="card-header">
131
+              <h2 class="text-lg font-semibold text-gray-900 dark:text-white">Privacy & Security</h2>
132
+              <p class="text-sm text-gray-600 dark:text-gray-400">Control your privacy and security settings</p>
133
+            </div>
134
+            <div class="card-body space-y-4">
135
+              <!-- Session timeout -->
136
+              <div class="flex items-center justify-between">
137
+                <div>
138
+                  <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Session timeout</label>
139
+                  <p class="text-xs text-gray-500 dark:text-gray-400">Automatically log out after inactivity</p>
140
+                </div>
141
+                <select
142
+                  v-model="settings.sessionTimeout"
143
+                  @change="updateSetting('sessionTimeout', parseInt($event.target.value))"
144
+                  class="input w-32"
145
+                >
146
+                  <option :value="30">30 minutes</option>
147
+                  <option :value="60">1 hour</option>
148
+                  <option :value="240">4 hours</option>
149
+                  <option :value="480">8 hours</option>
150
+                  <option :value="0">Never</option>
151
+                </select>
152
+              </div>
153
+
154
+              <!-- Clear data on logout -->
155
+              <div class="flex items-center justify-between">
156
+                <div>
157
+                  <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Clear data on logout</label>
158
+                  <p class="text-xs text-gray-500 dark:text-gray-400">Remove cached data when you log out</p>
159
+                </div>
160
+                <label class="relative inline-flex items-center cursor-pointer">
161
+                  <input
162
+                    v-model="settings.clearDataOnLogout"
163
+                    @change="updateSetting('clearDataOnLogout', $event.target.checked)"
164
+                    type="checkbox"
165
+                    class="sr-only peer"
166
+                  />
167
+                  <div class="w-11 h-6 bg-gray-200 dark:bg-gray-700 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary-600"></div>
168
+                </label>
169
+              </div>
170
+            </div>
171
+          </div>
172
+
173
+          <!-- Notifications -->
174
+          <div class="card">
175
+            <div class="card-header">
176
+              <h2 class="text-lg font-semibold text-gray-900 dark:text-white">Notifications</h2>
177
+              <p class="text-sm text-gray-600 dark:text-gray-400">Choose what notifications you receive</p>
178
+            </div>
179
+            <div class="card-body space-y-4">
180
+              <!-- Upload notifications -->
181
+              <div class="flex items-center justify-between">
182
+                <div>
183
+                  <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Upload notifications</label>
184
+                  <p class="text-xs text-gray-500 dark:text-gray-400">Show notifications when uploads complete</p>
185
+                </div>
186
+                <label class="relative inline-flex items-center cursor-pointer">
187
+                  <input
188
+                    v-model="settings.notifyUploads"
189
+                    @change="updateSetting('notifyUploads', $event.target.checked)"
190
+                    type="checkbox"
191
+                    class="sr-only peer"
192
+                  />
193
+                  <div class="w-11 h-6 bg-gray-200 dark:bg-gray-700 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary-600"></div>
194
+                </label>
195
+              </div>
196
+
197
+              <!-- Error notifications -->
198
+              <div class="flex items-center justify-between">
199
+                <div>
200
+                  <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Error notifications</label>
201
+                  <p class="text-xs text-gray-500 dark:text-gray-400">Show notifications when errors occur</p>
202
+                </div>
203
+                <label class="relative inline-flex items-center cursor-pointer">
204
+                  <input
205
+                    v-model="settings.notifyErrors"
206
+                    @change="updateSetting('notifyErrors', $event.target.checked)"
207
+                    type="checkbox"
208
+                    class="sr-only peer"
209
+                  />
210
+                  <div class="w-11 h-6 bg-gray-200 dark:bg-gray-700 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary-600"></div>
211
+                </label>
212
+              </div>
213
+            </div>
214
+          </div>
215
+
216
+          <!-- Actions -->
217
+          <div class="card">
218
+            <div class="card-header">
219
+              <h2 class="text-lg font-semibold text-gray-900 dark:text-white">Actions</h2>
220
+              <p class="text-sm text-gray-600 dark:text-gray-400">Manage your account and data</p>
221
+            </div>
222
+            <div class="card-body space-y-4">
223
+              <!-- Export settings -->
224
+              <div class="flex items-center justify-between">
225
+                <div>
226
+                  <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Export settings</label>
227
+                  <p class="text-xs text-gray-500 dark:text-gray-400">Download your settings as a file</p>
228
+                </div>
229
+                <button @click="exportSettings" class="btn btn-outline">
230
+                  Export
231
+                </button>
232
+              </div>
233
+
234
+              <!-- Import settings -->
235
+              <div class="flex items-center justify-between">
236
+                <div>
237
+                  <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Import settings</label>
238
+                  <p class="text-xs text-gray-500 dark:text-gray-400">Restore settings from a file</p>
239
+                </div>
240
+                <div class="flex items-center space-x-2">
241
+                  <input
242
+                    ref="importInput"
243
+                    type="file"
244
+                    accept=".json"
245
+                    @change="importSettings"
246
+                    class="hidden"
247
+                  />
248
+                  <button @click="$refs.importInput.click()" class="btn btn-outline">
249
+                    Import
250
+                  </button>
251
+                </div>
252
+              </div>
253
+
254
+              <!-- Reset settings -->
255
+              <div class="flex items-center justify-between">
256
+                <div>
257
+                  <label class="text-sm font-medium text-gray-700 dark:text-gray-300">Reset to defaults</label>
258
+                  <p class="text-xs text-gray-500 dark:text-gray-400">Restore all settings to their default values</p>
259
+                </div>
260
+                <button @click="resetSettings" class="btn btn-danger">
261
+                  Reset
262
+                </button>
263
+              </div>
264
+            </div>
265
+          </div>
266
+        </div>
267
+      </div>
268
+    </div>
269
+  </AppLayout>
270
+</template>
271
+
272
+<script setup lang="ts">
273
+import { ref, reactive, onMounted } from 'vue'
274
+import AppLayout from '@/components/AppLayout.vue'
275
+
276
+interface Settings {
277
+  theme: 'system' | 'light' | 'dark'
278
+  language: string
279
+  defaultFileView: 'grid' | 'list'
280
+  autoEncrypt: boolean
281
+  defaultUploadFolder: string
282
+  maxUploadSize: number
283
+  sessionTimeout: number
284
+  clearDataOnLogout: boolean
285
+  notifyUploads: boolean
286
+  notifyErrors: boolean
287
+}
288
+
289
+const defaultSettings: Settings = {
290
+  theme: 'system',
291
+  language: 'en',
292
+  defaultFileView: 'grid',
293
+  autoEncrypt: false,
294
+  defaultUploadFolder: '/uploads',
295
+  maxUploadSize: 1024 * 1024 * 1024, // 1GB
296
+  sessionTimeout: 60, // 1 hour
297
+  clearDataOnLogout: true,
298
+  notifyUploads: true,
299
+  notifyErrors: true,
300
+}
301
+
302
+const settings = reactive<Settings>({ ...defaultSettings })
303
+const importInput = ref<HTMLInputElement>()
304
+
305
+function loadSettings() {
306
+  try {
307
+    const saved = localStorage.getItem('zephyrfs_settings')
308
+    if (saved) {
309
+      const parsed = JSON.parse(saved)
310
+      Object.assign(settings, { ...defaultSettings, ...parsed })
311
+    }
312
+  } catch (error) {
313
+    console.warn('Failed to load settings:', error)
314
+  }
315
+}
316
+
317
+function saveSettings() {
318
+  try {
319
+    localStorage.setItem('zephyrfs_settings', JSON.stringify(settings))
320
+  } catch (error) {
321
+    console.error('Failed to save settings:', error)
322
+    if (window.$notify) {
323
+      window.$notify.error('Failed to save settings')
324
+    }
325
+  }
326
+}
327
+
328
+function updateSetting(key: keyof Settings, value: any) {
329
+  (settings as any)[key] = value
330
+  saveSettings()
331
+
332
+  // Apply certain settings immediately
333
+  if (key === 'theme') {
334
+    applyTheme(value)
335
+  }
336
+
337
+  if (window.$notify) {
338
+    window.$notify.success('Setting updated')
339
+  }
340
+}
341
+
342
+function applyTheme(theme: string) {
343
+  const html = document.documentElement
344
+
345
+  if (theme === 'dark') {
346
+    html.classList.add('dark')
347
+  } else if (theme === 'light') {
348
+    html.classList.remove('dark')
349
+  } else {
350
+    // System theme
351
+    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
352
+    html.classList.toggle('dark', prefersDark)
353
+  }
354
+}
355
+
356
+function exportSettings() {
357
+  try {
358
+    const data = JSON.stringify(settings, null, 2)
359
+    const blob = new Blob([data], { type: 'application/json' })
360
+    const url = URL.createObjectURL(blob)
361
+
362
+    const link = document.createElement('a')
363
+    link.href = url
364
+    link.download = `zephyrfs-settings-${new Date().toISOString().split('T')[0]}.json`
365
+    document.body.appendChild(link)
366
+    link.click()
367
+    document.body.removeChild(link)
368
+
369
+    URL.revokeObjectURL(url)
370
+
371
+    if (window.$notify) {
372
+      window.$notify.success('Settings exported successfully')
373
+    }
374
+  } catch (error) {
375
+    console.error('Failed to export settings:', error)
376
+    if (window.$notify) {
377
+      window.$notify.error('Failed to export settings')
378
+    }
379
+  }
380
+}
381
+
382
+function importSettings(event: Event) {
383
+  const file = (event.target as HTMLInputElement).files?.[0]
384
+  if (!file) return
385
+
386
+  const reader = new FileReader()
387
+  reader.onload = (e) => {
388
+    try {
389
+      const imported = JSON.parse(e.target?.result as string)
390
+      Object.assign(settings, { ...defaultSettings, ...imported })
391
+      saveSettings()
392
+
393
+      // Apply theme immediately
394
+      applyTheme(settings.theme)
395
+
396
+      if (window.$notify) {
397
+        window.$notify.success('Settings imported successfully')
398
+      }
399
+    } catch (error) {
400
+      console.error('Failed to import settings:', error)
401
+      if (window.$notify) {
402
+        window.$notify.error('Failed to import settings. Please check the file format.')
403
+      }
404
+    }
405
+  }
406
+
407
+  reader.readAsText(file)
408
+
409
+  // Reset input
410
+  if (importInput.value) {
411
+    importInput.value.value = ''
412
+  }
413
+}
414
+
415
+function resetSettings() {
416
+  if (confirm('Are you sure you want to reset all settings to their defaults? This action cannot be undone.')) {
417
+    Object.assign(settings, defaultSettings)
418
+    saveSettings()
419
+    applyTheme(settings.theme)
420
+
421
+    if (window.$notify) {
422
+      window.$notify.success('Settings reset to defaults')
423
+    }
424
+  }
425
+}
426
+
427
+onMounted(() => {
428
+  loadSettings()
429
+  applyTheme(settings.theme)
430
+})
431
+</script>
deploy/production.ymladded
@@ -0,0 +1,257 @@
1
+version: '3.8'
2
+
3
+services:
4
+  # Frontend (Vue.js + nginx)
5
+  web-client:
6
+    image: zephyrfs/web-client:latest
7
+    deploy:
8
+      replicas: 2
9
+      restart_policy:
10
+        condition: on-failure
11
+        delay: 5s
12
+        max_attempts: 3
13
+      resources:
14
+        limits:
15
+          memory: 512M
16
+          cpus: '0.5'
17
+        reservations:
18
+          memory: 256M
19
+          cpus: '0.25'
20
+    environment:
21
+      - NODE_ENV=production
22
+    networks:
23
+      - zephyrfs-frontend
24
+      - zephyrfs-backend
25
+    volumes:
26
+      - web-logs:/var/log/nginx
27
+    healthcheck:
28
+      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
29
+      interval: 30s
30
+      timeout: 10s
31
+      retries: 3
32
+      start_period: 40s
33
+
34
+  # Backend API server
35
+  web-server:
36
+    image: zephyrfs/web-server:latest
37
+    deploy:
38
+      replicas: 3
39
+      restart_policy:
40
+        condition: on-failure
41
+        delay: 5s
42
+        max_attempts: 3
43
+      resources:
44
+        limits:
45
+          memory: 1G
46
+          cpus: '1.0'
47
+        reservations:
48
+          memory: 512M
49
+          cpus: '0.5'
50
+    environment:
51
+      - NODE_ENV=production
52
+      - ZEPHYRFS_NODE_URL=http://zephyrfs-node:8080
53
+      - JWT_SECRET=${JWT_SECRET}
54
+      - LOG_LEVEL=info
55
+      - CORS_ORIGINS=${CORS_ORIGINS:-https://your-domain.com}
56
+      - PORT=3000
57
+    secrets:
58
+      - jwt_secret
59
+    volumes:
60
+      - web-logs:/app/logs
61
+    networks:
62
+      - zephyrfs-backend
63
+    healthcheck:
64
+      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"]
65
+      interval: 30s
66
+      timeout: 10s
67
+      retries: 3
68
+      start_period: 40s
69
+
70
+  # ZephyrFS core node
71
+  zephyrfs-node:
72
+    image: zephyrfs/node:latest
73
+    deploy:
74
+      replicas: 1
75
+      restart_policy:
76
+        condition: on-failure
77
+        delay: 10s
78
+        max_attempts: 3
79
+      resources:
80
+        limits:
81
+          memory: 2G
82
+          cpus: '2.0'
83
+        reservations:
84
+          memory: 1G
85
+          cpus: '1.0'
86
+    environment:
87
+      - RUST_LOG=info
88
+      - ZEPHYRFS_DATA_DIR=/data
89
+      - ZEPHYRFS_BIND_ADDR=0.0.0.0:8080
90
+    volumes:
91
+      - zephyrfs-data:/data
92
+      - node-logs:/logs
93
+    networks:
94
+      - zephyrfs-backend
95
+    healthcheck:
96
+      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
97
+      interval: 30s
98
+      timeout: 10s
99
+      retries: 3
100
+      start_period: 60s
101
+
102
+  # Load balancer with TLS termination
103
+  nginx-proxy:
104
+    image: nginx:alpine
105
+    ports:
106
+      - "443:443"
107
+      - "80:80"
108
+    deploy:
109
+      replicas: 2
110
+      restart_policy:
111
+        condition: on-failure
112
+        delay: 5s
113
+        max_attempts: 3
114
+      resources:
115
+        limits:
116
+          memory: 256M
117
+          cpus: '0.5'
118
+        reservations:
119
+          memory: 128M
120
+          cpus: '0.25'
121
+    volumes:
122
+      - ./nginx/production.conf:/etc/nginx/nginx.conf:ro
123
+      - ssl-certs:/etc/nginx/ssl:ro
124
+      - nginx-logs:/var/log/nginx
125
+    networks:
126
+      - zephyrfs-frontend
127
+    depends_on:
128
+      - web-client
129
+    healthcheck:
130
+      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/health"]
131
+      interval: 30s
132
+      timeout: 10s
133
+      retries: 3
134
+
135
+  # Monitoring and logging
136
+  prometheus:
137
+    image: prom/prometheus:latest
138
+    ports:
139
+      - "9090:9090"
140
+    deploy:
141
+      replicas: 1
142
+      resources:
143
+        limits:
144
+          memory: 512M
145
+          cpus: '0.5'
146
+    volumes:
147
+      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
148
+      - prometheus-data:/prometheus
149
+    networks:
150
+      - zephyrfs-monitoring
151
+    command:
152
+      - '--config.file=/etc/prometheus/prometheus.yml'
153
+      - '--storage.tsdb.path=/prometheus'
154
+      - '--web.console.libraries=/etc/prometheus/console_libraries'
155
+      - '--web.console.templates=/etc/prometheus/consoles'
156
+      - '--web.enable-lifecycle'
157
+
158
+  grafana:
159
+    image: grafana/grafana:latest
160
+    ports:
161
+      - "3001:3000"
162
+    deploy:
163
+      replicas: 1
164
+      resources:
165
+        limits:
166
+          memory: 256M
167
+          cpus: '0.25'
168
+    environment:
169
+      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin}
170
+      - GF_INSTALL_PLUGINS=grafana-piechart-panel
171
+    volumes:
172
+      - grafana-data:/var/lib/grafana
173
+      - ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards:ro
174
+      - ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources:ro
175
+    networks:
176
+      - zephyrfs-monitoring
177
+
178
+  # Log aggregation
179
+  loki:
180
+    image: grafana/loki:latest
181
+    ports:
182
+      - "3100:3100"
183
+    deploy:
184
+      replicas: 1
185
+      resources:
186
+        limits:
187
+          memory: 512M
188
+          cpus: '0.5'
189
+    volumes:
190
+      - ./monitoring/loki.yml:/etc/loki/local-config.yaml:ro
191
+      - loki-data:/loki
192
+    networks:
193
+      - zephyrfs-monitoring
194
+    command: -config.file=/etc/loki/local-config.yaml
195
+
196
+  promtail:
197
+    image: grafana/promtail:latest
198
+    deploy:
199
+      replicas: 1
200
+      resources:
201
+        limits:
202
+          memory: 128M
203
+          cpus: '0.25'
204
+    volumes:
205
+      - /var/log:/var/log:ro
206
+      - web-logs:/var/log/web:ro
207
+      - node-logs:/var/log/node:ro
208
+      - nginx-logs:/var/log/nginx:ro
209
+      - ./monitoring/promtail.yml:/etc/promtail/config.yml:ro
210
+    networks:
211
+      - zephyrfs-monitoring
212
+    command: -config.file=/etc/promtail/config.yml
213
+
214
+volumes:
215
+  zephyrfs-data:
216
+    driver: local
217
+    driver_opts:
218
+      type: none
219
+      o: bind
220
+      device: ${DATA_PATH:-/opt/zephyrfs/data}
221
+  web-logs:
222
+    driver: local
223
+  node-logs:
224
+    driver: local
225
+  nginx-logs:
226
+    driver: local
227
+  prometheus-data:
228
+    driver: local
229
+  grafana-data:
230
+    driver: local
231
+  loki-data:
232
+    driver: local
233
+  ssl-certs:
234
+    driver: local
235
+    driver_opts:
236
+      type: none
237
+      o: bind
238
+      device: ${SSL_CERTS_PATH:-/opt/zephyrfs/ssl}
239
+
240
+networks:
241
+  zephyrfs-frontend:
242
+    driver: overlay
243
+    attachable: true
244
+  zephyrfs-backend:
245
+    driver: overlay
246
+    internal: true
247
+  zephyrfs-monitoring:
248
+    driver: overlay
249
+    internal: true
250
+
251
+secrets:
252
+  jwt_secret:
253
+    external: true
254
+
255
+configs:
256
+  nginx_config:
257
+    external: true
docker-compose.ymlmodified
@@ -1,6 +1,28 @@
11
 version: '3.8'
22
 
33
 services:
4
+  # Frontend (Vue.js + nginx)
5
+  web-client:
6
+    build:
7
+      context: ./client
8
+      dockerfile: Dockerfile
9
+    ports:
10
+      - "80:8080"
11
+    environment:
12
+      - NODE_ENV=production
13
+    depends_on:
14
+      - web-server
15
+    networks:
16
+      - zephyrfs
17
+    restart: unless-stopped
18
+    healthcheck:
19
+      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
20
+      interval: 30s
21
+      timeout: 10s
22
+      retries: 3
23
+      start_period: 40s
24
+
25
+  # Backend API server
426
   web-server:
527
     build:
628
       context: ./server
@@ -8,29 +30,80 @@ services:
830
     ports:
931
       - "3000:3000"
1032
     environment:
11
-      - NODE_ENV=development
12
-      - ZEPHYRFS_NODE_URL=http://localhost:8080
13
-      - JWT_SECRET=your-jwt-secret-change-in-production
33
+      - NODE_ENV=production
34
+      - ZEPHYRFS_NODE_URL=http://zephyrfs-node:8080
35
+      - JWT_SECRET=${JWT_SECRET:-change-this-in-production-min-32-chars-long}
36
+      - LOG_LEVEL=info
37
+      - CORS_ORIGINS=http://localhost
1438
     volumes:
15
-      - ./server/src:/app/src:ro
16
-      - ./server/dist:/app/dist
39
+      - web-logs:/app/logs
1740
     depends_on:
1841
       - zephyrfs-node
1942
     networks:
2043
       - zephyrfs
44
+    restart: unless-stopped
45
+    healthcheck:
46
+      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"]
47
+      interval: 30s
48
+      timeout: 10s
49
+      retries: 3
50
+      start_period: 40s
2151
 
52
+  # ZephyrFS core node
2253
   zephyrfs-node:
2354
     image: zephyrfs-node:latest
2455
     ports:
2556
       - "8080:8080"
2657
     volumes:
2758
       - zephyrfs-data:/data
59
+      - node-logs:/logs
60
+    environment:
61
+      - RUST_LOG=info
62
+      - ZEPHYRFS_DATA_DIR=/data
63
+    networks:
64
+      - zephyrfs
65
+    restart: unless-stopped
66
+    healthcheck:
67
+      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
68
+      interval: 30s
69
+      timeout: 10s
70
+      retries: 3
71
+      start_period: 60s
72
+
73
+  # Reverse proxy with TLS termination
74
+  nginx-proxy:
75
+    image: nginx:alpine
76
+    ports:
77
+      - "443:443"
78
+      - "80:80"
79
+    volumes:
80
+      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
81
+      - ./nginx/ssl:/etc/nginx/ssl:ro
82
+      - nginx-logs:/var/log/nginx
83
+    depends_on:
84
+      - web-client
2885
     networks:
2986
       - zephyrfs
87
+    restart: unless-stopped
88
+    healthcheck:
89
+      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/health"]
90
+      interval: 30s
91
+      timeout: 10s
92
+      retries: 3
3093
 
3194
 volumes:
3295
   zephyrfs-data:
96
+    driver: local
97
+  web-logs:
98
+    driver: local
99
+  node-logs:
100
+    driver: local
101
+  nginx-logs:
102
+    driver: local
33103
 
34104
 networks:
35105
   zephyrfs:
36
-    driver: bridge
106
+    driver: bridge
107
+    ipam:
108
+      config:
109
+        - subnet: 172.20.0.0/16
monitoring/grafana/dashboards/dashboard.ymladded
@@ -0,0 +1,13 @@
1
+apiVersion: 1
2
+
3
+providers:
4
+  - name: 'default'
5
+    orgId: 1
6
+    folder: ''
7
+    type: file
8
+    disableDeletion: false
9
+    editable: true
10
+    updateIntervalSeconds: 10
11
+    allowUiUpdates: true
12
+    options:
13
+      path: /etc/grafana/provisioning/dashboards
monitoring/grafana/datasources/prometheus.ymladded
@@ -0,0 +1,21 @@
1
+apiVersion: 1
2
+
3
+datasources:
4
+  - name: Prometheus
5
+    type: prometheus
6
+    access: proxy
7
+    url: http://prometheus:9090
8
+    isDefault: true
9
+    jsonData:
10
+      httpMethod: POST
11
+      prometheusType: Prometheus
12
+      prometheusVersion: 2.40.0
13
+    editable: true
14
+
15
+  - name: Loki
16
+    type: loki
17
+    access: proxy
18
+    url: http://loki:3100
19
+    jsonData:
20
+      maxLines: 1000
21
+    editable: true
monitoring/loki.ymladded
@@ -0,0 +1,64 @@
1
+auth_enabled: false
2
+
3
+server:
4
+  http_listen_port: 3100
5
+  grpc_listen_port: 9096
6
+
7
+common:
8
+  path_prefix: /loki
9
+  storage:
10
+    filesystem:
11
+      chunks_directory: /loki/chunks
12
+      rules_directory: /loki/rules
13
+  replication_factor: 1
14
+  ring:
15
+    instance_addr: 127.0.0.1
16
+    kvstore:
17
+      store: inmemory
18
+
19
+schema_config:
20
+  configs:
21
+    - from: 2020-10-24
22
+      store: boltdb-shipper
23
+      object_store: filesystem
24
+      schema: v11
25
+      index:
26
+        prefix: index_
27
+        period: 24h
28
+
29
+storage_config:
30
+  boltdb_shipper:
31
+    active_index_directory: /loki/boltdb-shipper-active
32
+    cache_location: /loki/boltdb-shipper-cache
33
+    shared_store: filesystem
34
+  filesystem:
35
+    directory: /loki/chunks
36
+
37
+compactor:
38
+  working_directory: /loki/boltdb-shipper-compactor
39
+  shared_store: filesystem
40
+
41
+limits_config:
42
+  reject_old_samples: true
43
+  reject_old_samples_max_age: 168h
44
+  ingestion_rate_mb: 16
45
+  ingestion_burst_size_mb: 32
46
+
47
+chunk_store_config:
48
+  max_look_back_period: 0s
49
+
50
+table_manager:
51
+  retention_deletes_enabled: false
52
+  retention_period: 0s
53
+
54
+ruler:
55
+  storage:
56
+    type: local
57
+    local:
58
+      directory: /loki/rules
59
+  rule_path: /loki/rules
60
+  alertmanager_url: http://alertmanager:9093
61
+  ring:
62
+    kvstore:
63
+      store: inmemory
64
+  enable_api: true
monitoring/prometheus.ymladded
@@ -0,0 +1,49 @@
1
+global:
2
+  scrape_interval: 15s
3
+  evaluation_interval: 15s
4
+
5
+rule_files:
6
+  - "alert_rules.yml"
7
+
8
+alerting:
9
+  alertmanagers:
10
+    - static_configs:
11
+        - targets:
12
+          - alertmanager:9093
13
+
14
+scrape_configs:
15
+  - job_name: 'prometheus'
16
+    static_configs:
17
+      - targets: ['localhost:9090']
18
+
19
+  - job_name: 'zephyrfs-web-server'
20
+    static_configs:
21
+      - targets: ['web-server:3000']
22
+    scrape_interval: 30s
23
+    metrics_path: '/api/metrics'
24
+    scheme: http
25
+
26
+  - job_name: 'zephyrfs-node'
27
+    static_configs:
28
+      - targets: ['zephyrfs-node:8080']
29
+    scrape_interval: 30s
30
+    metrics_path: '/metrics'
31
+    scheme: http
32
+
33
+  - job_name: 'nginx-proxy'
34
+    static_configs:
35
+      - targets: ['nginx-proxy:80']
36
+    scrape_interval: 60s
37
+    metrics_path: '/nginx_status'
38
+    scheme: http
39
+
40
+  - job_name: 'node-exporter'
41
+    static_configs:
42
+      - targets: ['node-exporter:9100']
43
+    scrape_interval: 30s
44
+
45
+  - job_name: 'cadvisor'
46
+    static_configs:
47
+      - targets: ['cadvisor:8080']
48
+    scrape_interval: 30s
49
+    metrics_path: '/metrics'
monitoring/promtail.ymladded
@@ -0,0 +1,79 @@
1
+server:
2
+  http_listen_port: 9080
3
+  grpc_listen_port: 0
4
+
5
+positions:
6
+  filename: /tmp/positions.yaml
7
+
8
+clients:
9
+  - url: http://loki:3100/loki/api/v1/push
10
+
11
+scrape_configs:
12
+  - job_name: system
13
+    static_configs:
14
+      - targets:
15
+          - localhost
16
+        labels:
17
+          job: varlogs
18
+          __path__: /var/log/*log
19
+
20
+  - job_name: zephyrfs-web-logs
21
+    static_configs:
22
+      - targets:
23
+          - localhost
24
+        labels:
25
+          job: zephyrfs-web
26
+          service: web-server
27
+          __path__: /var/log/web/*.log
28
+
29
+  - job_name: zephyrfs-node-logs
30
+    static_configs:
31
+      - targets:
32
+          - localhost
33
+        labels:
34
+          job: zephyrfs-node
35
+          service: storage-node
36
+          __path__: /var/log/node/*.log
37
+
38
+  - job_name: nginx-logs
39
+    static_configs:
40
+      - targets:
41
+          - localhost
42
+        labels:
43
+          job: nginx
44
+          service: proxy
45
+          __path__: /var/log/nginx/*.log
46
+
47
+pipeline_stages:
48
+  - match:
49
+      selector: '{job="zephyrfs-web"}'
50
+      stages:
51
+        - json:
52
+            expressions:
53
+              level: level
54
+              timestamp: time
55
+              message: msg
56
+              method: method
57
+              url: url
58
+              status: statusCode
59
+              response_time: responseTime
60
+        - labels:
61
+            level:
62
+            method:
63
+            status:
64
+        - timestamp:
65
+            source: timestamp
66
+            format: RFC3339
67
+
68
+  - match:
69
+      selector: '{job="nginx"}'
70
+      stages:
71
+        - regex:
72
+            expression: '^(?P<remote_addr>\S+) \S+ \S+ \[(?P<timestamp>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) \S+" (?P<status>\d+) (?P<body_bytes_sent>\d+) "(?P<referer>[^"]*)" "(?P<user_agent>[^"]*)" (?P<response_time>\S+)'
73
+        - labels:
74
+            method:
75
+            status:
76
+            remote_addr:
77
+        - timestamp:
78
+            source: timestamp
79
+            format: 02/Jan/2006:15:04:05 -0700
nginx/nginx.confadded
@@ -0,0 +1,160 @@
1
+user nginx;
2
+worker_processes auto;
3
+error_log /var/log/nginx/error.log warn;
4
+pid /var/run/nginx.pid;
5
+
6
+events {
7
+    worker_connections 1024;
8
+    use epoll;
9
+    multi_accept on;
10
+}
11
+
12
+http {
13
+    include /etc/nginx/mime.types;
14
+    default_type application/octet-stream;
15
+
16
+    # Logging
17
+    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
18
+                    '$status $body_bytes_sent "$http_referer" '
19
+                    '"$http_user_agent" "$http_x_forwarded_for" '
20
+                    'rt=$request_time uct="$upstream_connect_time" '
21
+                    'uht="$upstream_header_time" urt="$upstream_response_time"';
22
+
23
+    access_log /var/log/nginx/access.log main;
24
+
25
+    # Performance settings
26
+    sendfile on;
27
+    tcp_nopush on;
28
+    tcp_nodelay on;
29
+    keepalive_timeout 65;
30
+    types_hash_max_size 2048;
31
+    client_max_body_size 1G;
32
+    server_tokens off;
33
+
34
+    # Gzip compression
35
+    gzip on;
36
+    gzip_vary on;
37
+    gzip_min_length 1024;
38
+    gzip_proxied any;
39
+    gzip_comp_level 6;
40
+    gzip_types
41
+        text/plain
42
+        text/css
43
+        text/xml
44
+        text/javascript
45
+        application/json
46
+        application/javascript
47
+        application/xml+rss
48
+        application/atom+xml
49
+        image/svg+xml;
50
+
51
+    # Rate limiting
52
+    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
53
+    limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/s;
54
+
55
+    # Upstream backend
56
+    upstream web-backend {
57
+        server web-client:8080;
58
+        keepalive 32;
59
+    }
60
+
61
+    # HTTP to HTTPS redirect
62
+    server {
63
+        listen 80;
64
+        listen [::]:80;
65
+        server_name _;
66
+
67
+        # Health check endpoint (allow HTTP)
68
+        location /health {
69
+            return 200 "healthy\n";
70
+            add_header Content-Type text/plain;
71
+            access_log off;
72
+        }
73
+
74
+        # Redirect all other traffic to HTTPS
75
+        location / {
76
+            return 301 https://$host$request_uri;
77
+        }
78
+    }
79
+
80
+    # HTTPS server
81
+    server {
82
+        listen 443 ssl http2;
83
+        listen [::]:443 ssl http2;
84
+        server_name _;
85
+
86
+        # SSL configuration
87
+        ssl_certificate /etc/nginx/ssl/cert.pem;
88
+        ssl_certificate_key /etc/nginx/ssl/key.pem;
89
+        ssl_session_timeout 1d;
90
+        ssl_session_cache shared:SSL:50m;
91
+        ssl_session_tickets off;
92
+
93
+        # Modern SSL configuration
94
+        ssl_protocols TLSv1.2 TLSv1.3;
95
+        ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
96
+        ssl_prefer_server_ciphers off;
97
+
98
+        # HSTS (optional, uncomment if using a real domain)
99
+        # add_header Strict-Transport-Security "max-age=63072000" always;
100
+
101
+        # Security headers
102
+        add_header X-Frame-Options DENY always;
103
+        add_header X-Content-Type-Options nosniff always;
104
+        add_header X-XSS-Protection "1; mode=block" always;
105
+        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
106
+        add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' ws: wss:; media-src 'self' blob:;" always;
107
+
108
+        # Main application
109
+        location / {
110
+            proxy_pass http://web-backend;
111
+            proxy_http_version 1.1;
112
+            proxy_set_header Upgrade $http_upgrade;
113
+            proxy_set_header Connection 'upgrade';
114
+            proxy_set_header Host $host;
115
+            proxy_set_header X-Real-IP $remote_addr;
116
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
117
+            proxy_set_header X-Forwarded-Proto $scheme;
118
+            proxy_cache_bypass $http_upgrade;
119
+            proxy_read_timeout 300;
120
+            proxy_connect_timeout 300;
121
+            proxy_send_timeout 300;
122
+
123
+            # Rate limiting
124
+            limit_req zone=api burst=20 nodelay;
125
+        }
126
+
127
+        # Authentication endpoints with stricter rate limiting
128
+        location ~ ^/api/auth/ {
129
+            proxy_pass http://web-backend;
130
+            proxy_http_version 1.1;
131
+            proxy_set_header Host $host;
132
+            proxy_set_header X-Real-IP $remote_addr;
133
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
134
+            proxy_set_header X-Forwarded-Proto $scheme;
135
+
136
+            # Stricter rate limiting for auth
137
+            limit_req zone=auth burst=10 nodelay;
138
+        }
139
+
140
+        # Health check
141
+        location /health {
142
+            access_log off;
143
+            return 200 "healthy\n";
144
+            add_header Content-Type text/plain;
145
+        }
146
+
147
+        # Security: Block access to sensitive files
148
+        location ~ /\. {
149
+            deny all;
150
+            access_log off;
151
+            log_not_found off;
152
+        }
153
+
154
+        location ~ ^/(package\.json|Dockerfile|docker-compose\.yml)$ {
155
+            deny all;
156
+            access_log off;
157
+            log_not_found off;
158
+        }
159
+    }
160
+}
nginx/production.confadded
@@ -0,0 +1,212 @@
1
+worker_processes auto;
2
+worker_rlimit_nofile 65535;
3
+
4
+events {
5
+    worker_connections 4096;
6
+    use epoll;
7
+    multi_accept on;
8
+}
9
+
10
+http {
11
+    charset utf-8;
12
+    sendfile on;
13
+    tcp_nopush on;
14
+    tcp_nodelay on;
15
+    server_tokens off;
16
+    log_not_found off;
17
+    types_hash_max_size 4096;
18
+    client_max_body_size 1G;
19
+
20
+    # MIME types
21
+    include /etc/nginx/mime.types;
22
+    default_type application/octet-stream;
23
+
24
+    # Logging
25
+    access_log /var/log/nginx/access.log;
26
+    error_log /var/log/nginx/error.log warn;
27
+
28
+    # Rate limiting
29
+    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
30
+    limit_req_zone $binary_remote_addr zone=upload:10m rate=2r/s;
31
+
32
+    # SSL Configuration
33
+    ssl_protocols TLSv1.2 TLSv1.3;
34
+    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
35
+    ssl_prefer_server_ciphers off;
36
+    ssl_session_cache shared:SSL:10m;
37
+    ssl_session_timeout 1d;
38
+
39
+    # Security headers
40
+    add_header X-Frame-Options "SAMEORIGIN" always;
41
+    add_header X-Content-Type-Options "nosniff" always;
42
+    add_header X-XSS-Protection "1; mode=block" always;
43
+    add_header Referrer-Policy "no-referrer-when-downgrade" always;
44
+    add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
45
+
46
+    # Gzip compression
47
+    gzip on;
48
+    gzip_vary on;
49
+    gzip_proxied any;
50
+    gzip_comp_level 6;
51
+    gzip_types
52
+        text/plain
53
+        text/css
54
+        text/xml
55
+        text/javascript
56
+        application/json
57
+        application/javascript
58
+        application/xml+rss
59
+        application/atom+xml
60
+        image/svg+xml;
61
+
62
+    # Upstream backends
63
+    upstream web_backend {
64
+        least_conn;
65
+        server web-server:3000 max_fails=3 fail_timeout=30s;
66
+        keepalive 32;
67
+    }
68
+
69
+    upstream zephyrfs_node {
70
+        server zephyrfs-node:8080 max_fails=3 fail_timeout=30s;
71
+        keepalive 16;
72
+    }
73
+
74
+    # HTTP to HTTPS redirect
75
+    server {
76
+        listen 80;
77
+        server_name _;
78
+        return 301 https://$host$request_uri;
79
+    }
80
+
81
+    # Main HTTPS server
82
+    server {
83
+        listen 443 ssl http2;
84
+        server_name _;
85
+
86
+        # SSL certificates
87
+        ssl_certificate /etc/nginx/ssl/cert.pem;
88
+        ssl_certificate_key /etc/nginx/ssl/key.pem;
89
+
90
+        # Root and index
91
+        root /var/www/html;
92
+        index index.html;
93
+
94
+        # Health check endpoint
95
+        location /health {
96
+            access_log off;
97
+            return 200 "healthy\n";
98
+            add_header Content-Type text/plain;
99
+        }
100
+
101
+        # API proxy with rate limiting
102
+        location /api/ {
103
+            limit_req zone=api burst=20 nodelay;
104
+
105
+            proxy_pass http://web_backend;
106
+            proxy_http_version 1.1;
107
+            proxy_set_header Upgrade $http_upgrade;
108
+            proxy_set_header Connection "upgrade";
109
+            proxy_set_header Host $host;
110
+            proxy_set_header X-Real-IP $remote_addr;
111
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
112
+            proxy_set_header X-Forwarded-Proto $scheme;
113
+
114
+            # Timeouts
115
+            proxy_connect_timeout 60s;
116
+            proxy_send_timeout 60s;
117
+            proxy_read_timeout 60s;
118
+
119
+            # Buffer settings
120
+            proxy_buffering on;
121
+            proxy_buffer_size 4k;
122
+            proxy_buffers 8 4k;
123
+        }
124
+
125
+        # File upload endpoint with special handling
126
+        location /api/files/upload {
127
+            limit_req zone=upload burst=5 nodelay;
128
+
129
+            client_max_body_size 1G;
130
+            client_body_timeout 300s;
131
+
132
+            proxy_pass http://web_backend;
133
+            proxy_http_version 1.1;
134
+            proxy_set_header Host $host;
135
+            proxy_set_header X-Real-IP $remote_addr;
136
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
137
+            proxy_set_header X-Forwarded-Proto $scheme;
138
+
139
+            # Extended timeouts for large uploads
140
+            proxy_connect_timeout 300s;
141
+            proxy_send_timeout 300s;
142
+            proxy_read_timeout 300s;
143
+
144
+            # Disable buffering for streaming uploads
145
+            proxy_request_buffering off;
146
+            proxy_buffering off;
147
+        }
148
+
149
+        # WebSocket support
150
+        location /ws {
151
+            proxy_pass http://web_backend;
152
+            proxy_http_version 1.1;
153
+            proxy_set_header Upgrade $http_upgrade;
154
+            proxy_set_header Connection "upgrade";
155
+            proxy_set_header Host $host;
156
+            proxy_set_header X-Real-IP $remote_addr;
157
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
158
+            proxy_set_header X-Forwarded-Proto $scheme;
159
+
160
+            # WebSocket specific settings
161
+            proxy_read_timeout 86400s;
162
+            proxy_send_timeout 86400s;
163
+        }
164
+
165
+        # Direct ZephyrFS node access (internal only)
166
+        location /node/ {
167
+            internal;
168
+            proxy_pass http://zephyrfs_node/;
169
+            proxy_http_version 1.1;
170
+            proxy_set_header Host $host;
171
+            proxy_set_header X-Real-IP $remote_addr;
172
+        }
173
+
174
+        # Static files with caching
175
+        location / {
176
+            try_files $uri $uri/ /index.html;
177
+
178
+            # Cache static assets
179
+            location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
180
+                expires 1y;
181
+                add_header Cache-Control "public, immutable";
182
+                add_header Vary "Accept-Encoding";
183
+            }
184
+
185
+            # Cache HTML with shorter expiry
186
+            location ~* \.html$ {
187
+                expires 1h;
188
+                add_header Cache-Control "public, no-cache, must-revalidate";
189
+            }
190
+        }
191
+
192
+        # Monitoring endpoints
193
+        location /nginx_status {
194
+            stub_status on;
195
+            access_log off;
196
+            allow 127.0.0.1;
197
+            allow 10.0.0.0/8;
198
+            allow 172.16.0.0/12;
199
+            allow 192.168.0.0/16;
200
+            deny all;
201
+        }
202
+
203
+        # Security - deny access to sensitive files
204
+        location ~ /\. {
205
+            deny all;
206
+        }
207
+
208
+        location ~ ~$ {
209
+            deny all;
210
+        }
211
+    }
212
+}
scripts/deploy.shadded
@@ -0,0 +1,209 @@
1
+#!/bin/bash
2
+
3
+set -euo pipefail
4
+
5
+DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+STACK_NAME="zephyrfs"
7
+COMPOSE_FILE="$DEPLOY_DIR/deploy/production.yml"
8
+ENV_FILE="$DEPLOY_DIR/.env.production"
9
+
10
+log() {
11
+    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
12
+}
13
+
14
+error() {
15
+    log "ERROR: $*" >&2
16
+    exit 1
17
+}
18
+
19
+check_prerequisites() {
20
+    log "Checking prerequisites..."
21
+
22
+    command -v docker >/dev/null 2>&1 || error "Docker is not installed"
23
+    command -v docker-compose >/dev/null 2>&1 || error "Docker Compose is not installed"
24
+
25
+    if ! docker info >/dev/null 2>&1; then
26
+        error "Docker daemon is not running"
27
+    fi
28
+
29
+    if ! docker swarm ca >/dev/null 2>&1; then
30
+        log "Initializing Docker Swarm..."
31
+        docker swarm init
32
+    fi
33
+
34
+    if [[ ! -f "$ENV_FILE" ]]; then
35
+        error "Environment file not found: $ENV_FILE"
36
+    fi
37
+
38
+    log "Prerequisites check passed"
39
+}
40
+
41
+build_images() {
42
+    log "Building Docker images..."
43
+
44
+    # Build client image
45
+    docker build -t zephyrfs/web-client:latest \
46
+        -f "$DEPLOY_DIR/client/Dockerfile" \
47
+        "$DEPLOY_DIR/client"
48
+
49
+    # Build server image
50
+    docker build -t zephyrfs/web-server:latest \
51
+        -f "$DEPLOY_DIR/server/Dockerfile" \
52
+        "$DEPLOY_DIR/server"
53
+
54
+    log "Images built successfully"
55
+}
56
+
57
+setup_secrets() {
58
+    log "Setting up Docker secrets..."
59
+
60
+    if ! docker secret inspect jwt_secret >/dev/null 2>&1; then
61
+        if [[ -n "${JWT_SECRET:-}" ]]; then
62
+            echo "$JWT_SECRET" | docker secret create jwt_secret -
63
+        else
64
+            openssl rand -base64 32 | docker secret create jwt_secret -
65
+        fi
66
+        log "JWT secret created"
67
+    fi
68
+}
69
+
70
+setup_volumes() {
71
+    log "Setting up volumes and directories..."
72
+
73
+    local data_path="${DATA_PATH:-/opt/zephyrfs/data}"
74
+    local ssl_path="${SSL_CERTS_PATH:-/opt/zephyrfs/ssl}"
75
+
76
+    sudo mkdir -p "$data_path" "$ssl_path"
77
+    sudo chown -R 1000:1000 "$data_path"
78
+
79
+    log "Volumes configured"
80
+}
81
+
82
+deploy_stack() {
83
+    log "Deploying Docker stack..."
84
+
85
+    docker stack deploy \
86
+        --compose-file "$COMPOSE_FILE" \
87
+        --with-registry-auth \
88
+        "$STACK_NAME"
89
+
90
+    log "Stack deployment initiated"
91
+}
92
+
93
+wait_for_services() {
94
+    log "Waiting for services to be ready..."
95
+
96
+    local max_wait=300
97
+    local wait_time=0
98
+
99
+    while [[ $wait_time -lt $max_wait ]]; do
100
+        if docker service ls --filter name="${STACK_NAME}_" --format "{{.Replicas}}" | grep -q "0/"; then
101
+            log "Services still starting... (${wait_time}s)"
102
+            sleep 10
103
+            wait_time=$((wait_time + 10))
104
+        else
105
+            log "All services are running"
106
+            return 0
107
+        fi
108
+    done
109
+
110
+    error "Services failed to start within ${max_wait} seconds"
111
+}
112
+
113
+health_check() {
114
+    log "Performing health checks..."
115
+
116
+    local endpoints=(
117
+        "http://localhost/api/health"
118
+        "http://localhost:9090/-/healthy"
119
+        "http://localhost:3001/api/health"
120
+    )
121
+
122
+    for endpoint in "${endpoints[@]}"; do
123
+        if curl -f -s "$endpoint" >/dev/null; then
124
+            log "Health check passed: $endpoint"
125
+        else
126
+            log "WARNING: Health check failed: $endpoint"
127
+        fi
128
+    done
129
+}
130
+
131
+update_stack() {
132
+    log "Performing rolling update..."
133
+
134
+    # Update images
135
+    build_images
136
+
137
+    # Deploy with updated images
138
+    deploy_stack
139
+
140
+    # Wait for rolling update to complete
141
+    wait_for_services
142
+
143
+    log "Rolling update completed"
144
+}
145
+
146
+rollback_stack() {
147
+    log "Rolling back to previous version..."
148
+
149
+    docker service rollback "${STACK_NAME}_web-client"
150
+    docker service rollback "${STACK_NAME}_web-server"
151
+
152
+    wait_for_services
153
+
154
+    log "Rollback completed"
155
+}
156
+
157
+cleanup() {
158
+    log "Cleaning up unused resources..."
159
+
160
+    docker system prune -f
161
+    docker volume prune -f
162
+
163
+    log "Cleanup completed"
164
+}
165
+
166
+main() {
167
+    local action="${1:-deploy}"
168
+
169
+    case "$action" in
170
+        deploy)
171
+            check_prerequisites
172
+            build_images
173
+            setup_secrets
174
+            setup_volumes
175
+            deploy_stack
176
+            wait_for_services
177
+            health_check
178
+            ;;
179
+        update)
180
+            check_prerequisites
181
+            update_stack
182
+            health_check
183
+            ;;
184
+        rollback)
185
+            check_prerequisites
186
+            rollback_stack
187
+            health_check
188
+            ;;
189
+        cleanup)
190
+            cleanup
191
+            ;;
192
+        *)
193
+            echo "Usage: $0 {deploy|update|rollback|cleanup}"
194
+            echo ""
195
+            echo "Commands:"
196
+            echo "  deploy   - Initial deployment or full redeploy"
197
+            echo "  update   - Rolling update with zero downtime"
198
+            echo "  rollback - Rollback to previous version"
199
+            echo "  cleanup  - Clean up unused Docker resources"
200
+            exit 1
201
+            ;;
202
+    esac
203
+
204
+    log "Operation '$action' completed successfully"
205
+}
206
+
207
+if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
208
+    main "$@"
209
+fi
scripts/generate-ssl.shadded
@@ -0,0 +1,53 @@
1
+#!/bin/bash
2
+
3
+# Script to generate self-signed SSL certificates for development
4
+# For production, use proper certificates from Let's Encrypt or a CA
5
+
6
+set -e
7
+
8
+SSL_DIR="nginx/ssl"
9
+CERT_FILE="$SSL_DIR/cert.pem"
10
+KEY_FILE="$SSL_DIR/key.pem"
11
+
12
+echo "Generating self-signed SSL certificate for ZephyrFS..."
13
+
14
+# Create SSL directory if it doesn't exist
15
+mkdir -p "$SSL_DIR"
16
+
17
+# Generate private key
18
+openssl genrsa -out "$KEY_FILE" 2048
19
+
20
+# Generate certificate signing request
21
+openssl req -new -key "$KEY_FILE" -out "$SSL_DIR/cert.csr" -subj "/C=US/ST=State/L=City/O=ZephyrFS/OU=Development/CN=localhost"
22
+
23
+# Generate self-signed certificate
24
+openssl x509 -req -days 365 -in "$SSL_DIR/cert.csr" -signkey "$KEY_FILE" -out "$CERT_FILE" -extensions v3_req -extfile <(cat <<EOF
25
+[v3_req]
26
+keyUsage = keyEncipherment, dataEncipherment
27
+extendedKeyUsage = serverAuth
28
+subjectAltName = @alt_names
29
+
30
+[alt_names]
31
+DNS.1 = localhost
32
+DNS.2 = zephyrfs.local
33
+DNS.3 = *.zephyrfs.local
34
+IP.1 = 127.0.0.1
35
+IP.2 = ::1
36
+EOF
37
+)
38
+
39
+# Clean up CSR
40
+rm "$SSL_DIR/cert.csr"
41
+
42
+# Set proper permissions
43
+chmod 600 "$KEY_FILE"
44
+chmod 644 "$CERT_FILE"
45
+
46
+echo "SSL certificate generated successfully!"
47
+echo "Certificate: $CERT_FILE"
48
+echo "Private key: $KEY_FILE"
49
+echo ""
50
+echo "For production deployment:"
51
+echo "1. Replace these files with proper certificates from Let's Encrypt or a CA"
52
+echo "2. Update the certificate paths in nginx/nginx.conf if needed"
53
+echo "3. Ensure proper file permissions (600 for key, 644 for cert)"
scripts/health-check.shadded
@@ -0,0 +1,143 @@
1
+#!/bin/bash
2
+
3
+set -euo pipefail
4
+
5
+log() {
6
+    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
7
+}
8
+
9
+check_response_time() {
10
+    local url="$1"
11
+    local max_time="$2"
12
+    local name="$3"
13
+
14
+    log "Checking response time for $name..."
15
+
16
+    local response_time
17
+    response_time=$(curl -o /dev/null -s -w '%{time_total}' "$url" || echo "999")
18
+
19
+    local response_ms
20
+    response_ms=$(echo "$response_time * 1000" | bc)
21
+
22
+    if (( $(echo "$response_time > $max_time" | bc -l) )); then
23
+        log "FAIL: $name response time ${response_ms}ms > ${max_time}000ms"
24
+        return 1
25
+    else
26
+        log "PASS: $name response time ${response_ms}ms"
27
+        return 0
28
+    fi
29
+}
30
+
31
+check_service_health() {
32
+    local service="$1"
33
+    local url="$2"
34
+
35
+    log "Checking health of $service..."
36
+
37
+    if curl -f -s "$url" >/dev/null; then
38
+        log "PASS: $service is healthy"
39
+        return 0
40
+    else
41
+        log "FAIL: $service health check failed"
42
+        return 1
43
+    fi
44
+}
45
+
46
+check_docker_services() {
47
+    log "Checking Docker services status..."
48
+
49
+    local failed_services=0
50
+
51
+    while IFS= read -r line; do
52
+        local service_name=$(echo "$line" | awk '{print $2}')
53
+        local replicas=$(echo "$line" | awk '{print $4}')
54
+
55
+        if [[ "$replicas" == *"0/"* ]]; then
56
+            log "FAIL: Service $service_name has 0 running replicas"
57
+            ((failed_services++))
58
+        else
59
+            log "PASS: Service $service_name is running ($replicas)"
60
+        fi
61
+    done < <(docker service ls --filter name="zephyrfs_" --format "table {{.ID}}\t{{.Name}}\t{{.Mode}}\t{{.Replicas}}")
62
+
63
+    return $failed_services
64
+}
65
+
66
+run_performance_test() {
67
+    log "Running performance test..."
68
+
69
+    local url="http://localhost/api/health"
70
+    local concurrent_requests=10
71
+    local total_requests=100
72
+
73
+    log "Testing with $concurrent_requests concurrent requests ($total_requests total)..."
74
+
75
+    local output
76
+    output=$(ab -n $total_requests -c $concurrent_requests -q "$url" 2>/dev/null || echo "Test failed")
77
+
78
+    if [[ "$output" == "Test failed" ]]; then
79
+        log "FAIL: Performance test failed"
80
+        return 1
81
+    fi
82
+
83
+    local mean_time
84
+    mean_time=$(echo "$output" | grep "Time per request:" | head -1 | awk '{print $4}')
85
+
86
+    if [[ -n "$mean_time" ]]; then
87
+        log "Mean response time: ${mean_time}ms"
88
+
89
+        if (( $(echo "$mean_time > 500" | bc -l) )); then
90
+            log "FAIL: Mean response time exceeds 500ms"
91
+            return 1
92
+        else
93
+            log "PASS: Mean response time is acceptable"
94
+            return 0
95
+        fi
96
+    else
97
+        log "FAIL: Could not parse performance test results"
98
+        return 1
99
+    fi
100
+}
101
+
102
+main() {
103
+    local failed_tests=0
104
+
105
+    log "Starting comprehensive health check..."
106
+
107
+    # Check Docker services
108
+    if ! check_docker_services; then
109
+        ((failed_tests++))
110
+    fi
111
+
112
+    # Health checks
113
+    check_service_health "Web Frontend" "http://localhost/health" || ((failed_tests++))
114
+    check_service_health "API Server" "http://localhost/api/health" || ((failed_tests++))
115
+    check_service_health "Prometheus" "http://localhost:9090/-/healthy" || ((failed_tests++))
116
+    check_service_health "Grafana" "http://localhost:3001/api/health" || ((failed_tests++))
117
+
118
+    # Response time checks (sub-500ms requirement)
119
+    check_response_time "http://localhost/" 0.5 "Frontend" || ((failed_tests++))
120
+    check_response_time "http://localhost/api/health" 0.5 "API Health" || ((failed_tests++))
121
+    check_response_time "http://localhost/api/files" 0.5 "File Listing" || ((failed_tests++))
122
+
123
+    # Performance test
124
+    if command -v ab >/dev/null 2>&1; then
125
+        run_performance_test || ((failed_tests++))
126
+    else
127
+        log "WARNING: Apache Bench (ab) not available, skipping performance test"
128
+    fi
129
+
130
+    log "Health check completed"
131
+
132
+    if [[ $failed_tests -eq 0 ]]; then
133
+        log "SUCCESS: All tests passed"
134
+        exit 0
135
+    else
136
+        log "FAILURE: $failed_tests test(s) failed"
137
+        exit 1
138
+    fi
139
+}
140
+
141
+if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
142
+    main "$@"
143
+fi
server/src/index.tsmodified
@@ -8,6 +8,8 @@ import { loadConfig } from './config.js';
88
 import { registerRoutes } from './routes/index.js';
99
 import { errorHandler } from './middleware/error-handler.js';
1010
 import { authMiddleware } from './middleware/auth.js';
11
+import { performanceMiddleware } from './middleware/performance.js';
12
+import { cacheMiddleware } from './middleware/cache.js';
1113
 import { ZephyrFSClient } from './integration/zephyrfs-client.js';
1214
 import path from 'node:path';
1315
 import { fileURLToPath } from 'node:url';
@@ -68,6 +70,8 @@ async function createServer() {
6870
 
6971
   // Register middleware
7072
   fastify.setErrorHandler(errorHandler);
73
+  await fastify.register(performanceMiddleware);
74
+  await fastify.register(cacheMiddleware);
7175
   await fastify.register(authMiddleware);
7276
 
7377
   // Register routes
server/src/middleware/cache.tsadded
@@ -0,0 +1,349 @@
1
+import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2
+import { createHash } from 'crypto';
3
+
4
+interface CacheEntry {
5
+  data: any;
6
+  headers: Record<string, string>;
7
+  statusCode: number;
8
+  timestamp: number;
9
+  ttl: number;
10
+  size: number;
11
+}
12
+
13
+class MemoryCache {
14
+  private cache = new Map<string, CacheEntry>();
15
+  private maxSize: number;
16
+  private currentSize = 0;
17
+  private maxEntrySize: number;
18
+  private defaultTTL: number;
19
+
20
+  constructor(options: {
21
+    maxSize?: number;
22
+    maxEntrySize?: number;
23
+    defaultTTL?: number;
24
+  } = {}) {
25
+    this.maxSize = options.maxSize || 100 * 1024 * 1024; // 100MB
26
+    this.maxEntrySize = options.maxEntrySize || 10 * 1024 * 1024; // 10MB per entry
27
+    this.defaultTTL = options.defaultTTL || 5 * 60 * 1000; // 5 minutes
28
+  }
29
+
30
+  set(key: string, data: any, headers: Record<string, string>, statusCode: number, ttl?: number): boolean {
31
+    const size = this.estimateSize(data);
32
+
33
+    // Don't cache large entries
34
+    if (size > this.maxEntrySize) {
35
+      return false;
36
+    }
37
+
38
+    // Evict entries if needed
39
+    this.evictIfNeeded(size);
40
+
41
+    const entry: CacheEntry = {
42
+      data,
43
+      headers,
44
+      statusCode,
45
+      timestamp: Date.now(),
46
+      ttl: ttl || this.defaultTTL,
47
+      size,
48
+    };
49
+
50
+    this.cache.set(key, entry);
51
+    this.currentSize += size;
52
+
53
+    return true;
54
+  }
55
+
56
+  get(key: string): CacheEntry | null {
57
+    const entry = this.cache.get(key);
58
+
59
+    if (!entry) {
60
+      return null;
61
+    }
62
+
63
+    // Check if expired
64
+    if (Date.now() - entry.timestamp > entry.ttl) {
65
+      this.delete(key);
66
+      return null;
67
+    }
68
+
69
+    return entry;
70
+  }
71
+
72
+  delete(key: string): boolean {
73
+    const entry = this.cache.get(key);
74
+    if (entry) {
75
+      this.cache.delete(key);
76
+      this.currentSize -= entry.size;
77
+      return true;
78
+    }
79
+    return false;
80
+  }
81
+
82
+  clear(): void {
83
+    this.cache.clear();
84
+    this.currentSize = 0;
85
+  }
86
+
87
+  getStats() {
88
+    return {
89
+      entries: this.cache.size,
90
+      size: this.currentSize,
91
+      maxSize: this.maxSize,
92
+      hitRate: this.calculateHitRate(),
93
+    };
94
+  }
95
+
96
+  private evictIfNeeded(newEntrySize: number): void {
97
+    // Use LRU eviction
98
+    while (this.currentSize + newEntrySize > this.maxSize && this.cache.size > 0) {
99
+      const oldestKey = this.cache.keys().next().value;
100
+      if (oldestKey) {
101
+        this.delete(oldestKey);
102
+      }
103
+    }
104
+  }
105
+
106
+  private estimateSize(data: any): number {
107
+    try {
108
+      return JSON.stringify(data).length * 2; // Rough estimate (UTF-16)
109
+    } catch {
110
+      return 1024; // Default size if can't stringify
111
+    }
112
+  }
113
+
114
+  private hitRate = 0;
115
+  private hits = 0;
116
+  private misses = 0;
117
+
118
+  recordHit(): void {
119
+    this.hits++;
120
+    this.updateHitRate();
121
+  }
122
+
123
+  recordMiss(): void {
124
+    this.misses++;
125
+    this.updateHitRate();
126
+  }
127
+
128
+  private updateHitRate(): void {
129
+    const total = this.hits + this.misses;
130
+    this.hitRate = total > 0 ? (this.hits / total) * 100 : 0;
131
+  }
132
+
133
+  private calculateHitRate(): number {
134
+    return this.hitRate;
135
+  }
136
+}
137
+
138
+const cache = new MemoryCache();
139
+
140
+export async function cacheMiddleware(fastify: FastifyInstance) {
141
+  // Cache configuration
142
+  const cacheConfig = {
143
+    // Routes that should be cached
144
+    cacheable: [
145
+      '/api/files',
146
+      '/api/status/network',
147
+      '/api/status/node',
148
+    ],
149
+    // Routes that should never be cached
150
+    nocache: [
151
+      '/api/auth/',
152
+      '/api/files/upload',
153
+      '/api/files/bulk/',
154
+    ],
155
+    // Default TTL for different route patterns
156
+    ttl: {
157
+      '/api/files': 30 * 1000, // 30 seconds
158
+      '/api/status/': 10 * 1000, // 10 seconds
159
+    },
160
+  };
161
+
162
+  // Pre-handler to check cache
163
+  fastify.addHook('preHandler', async (request: FastifyRequest, reply: FastifyReply) => {
164
+    // Only cache GET requests
165
+    if (request.method !== 'GET') {
166
+      return;
167
+    }
168
+
169
+    // Check if route should be cached
170
+    if (!shouldCacheRoute(request.url, cacheConfig)) {
171
+      return;
172
+    }
173
+
174
+    const cacheKey = generateCacheKey(request);
175
+    const cached = cache.get(cacheKey);
176
+
177
+    if (cached) {
178
+      cache.recordHit();
179
+
180
+      // Set cached headers
181
+      Object.entries(cached.headers).forEach(([key, value]) => {
182
+        reply.header(key, value);
183
+      });
184
+
185
+      // Add cache headers
186
+      reply.header('X-Cache', 'HIT');
187
+      reply.header('X-Cache-Time', new Date(cached.timestamp).toISOString());
188
+
189
+      reply.code(cached.statusCode);
190
+      return reply.send(cached.data);
191
+    } else {
192
+      cache.recordMiss();
193
+      reply.header('X-Cache', 'MISS');
194
+    }
195
+  });
196
+
197
+  // Post-handler to cache responses
198
+  fastify.addHook('onSend', async (request: FastifyRequest, reply: FastifyReply, payload: any) => {
199
+    // Only cache successful GET requests
200
+    if (request.method !== 'GET' || reply.statusCode >= 400) {
201
+      return payload;
202
+    }
203
+
204
+    // Check if route should be cached
205
+    if (!shouldCacheRoute(request.url, cacheConfig)) {
206
+      return payload;
207
+    }
208
+
209
+    // Don't cache if already cached
210
+    if (reply.getHeader('X-Cache') === 'HIT') {
211
+      return payload;
212
+    }
213
+
214
+    const cacheKey = generateCacheKey(request);
215
+    const ttl = getTTLForRoute(request.url, cacheConfig);
216
+
217
+    // Get response headers (excluding some)
218
+    const headers: Record<string, string> = {};
219
+    const excludeHeaders = ['set-cookie', 'authorization', 'x-cache'];
220
+
221
+    Object.entries(reply.getHeaders()).forEach(([key, value]) => {
222
+      if (!excludeHeaders.includes(key.toLowerCase()) && typeof value === 'string') {
223
+        headers[key] = value;
224
+      }
225
+    });
226
+
227
+    // Cache the response
228
+    cache.set(cacheKey, payload, headers, reply.statusCode, ttl);
229
+
230
+    return payload;
231
+  });
232
+
233
+  // Cache management endpoints
234
+  fastify.get('/api/cache/stats', async () => {
235
+    return cache.getStats();
236
+  });
237
+
238
+  fastify.delete('/api/cache/clear', {
239
+    preHandler: fastify.authenticate,
240
+  }, async () => {
241
+    cache.clear();
242
+    return { success: true, message: 'Cache cleared' };
243
+  });
244
+
245
+  fastify.delete('/api/cache/invalidate', {
246
+    preHandler: fastify.authenticate,
247
+    schema: {
248
+      querystring: {
249
+        type: 'object',
250
+        properties: {
251
+          pattern: { type: 'string' },
252
+        },
253
+      },
254
+    },
255
+  }, async (request: FastifyRequest) => {
256
+    const { pattern } = request.query as { pattern?: string };
257
+
258
+    if (!pattern) {
259
+      return { success: false, message: 'Pattern is required' };
260
+    }
261
+
262
+    let invalidated = 0;
263
+    const regex = new RegExp(pattern);
264
+
265
+    for (const key of cache['cache'].keys()) {
266
+      if (regex.test(key)) {
267
+        cache.delete(key);
268
+        invalidated++;
269
+      }
270
+    }
271
+
272
+    return {
273
+      success: true,
274
+      message: `Invalidated ${invalidated} cache entries`,
275
+      invalidated,
276
+    };
277
+  });
278
+
279
+  // Periodic cache cleanup
280
+  setInterval(() => {
281
+    const stats = cache.getStats();
282
+    fastify.log.debug({
283
+      cache: {
284
+        entries: stats.entries,
285
+        sizeMB: Math.round(stats.size / 1024 / 1024),
286
+        hitRate: Math.round(stats.hitRate * 100) / 100,
287
+      },
288
+    }, 'Cache statistics');
289
+
290
+    // Force cleanup if cache is getting full
291
+    if (stats.size > stats.maxSize * 0.9) {
292
+      // Remove oldest 10% of entries
293
+      const keysToRemove = Math.floor(stats.entries * 0.1);
294
+      let removed = 0;
295
+
296
+      for (const key of cache['cache'].keys()) {
297
+        if (removed >= keysToRemove) break;
298
+        cache.delete(key);
299
+        removed++;
300
+      }
301
+
302
+      fastify.log.info({ removedEntries: removed }, 'Cache cleanup performed');
303
+    }
304
+  }, 60 * 1000); // Check every minute
305
+}
306
+
307
+function shouldCacheRoute(url: string, config: any): boolean {
308
+  // Check nocache patterns first
309
+  for (const pattern of config.nocache) {
310
+    if (url.startsWith(pattern)) {
311
+      return false;
312
+    }
313
+  }
314
+
315
+  // Check cacheable patterns
316
+  for (const pattern of config.cacheable) {
317
+    if (url.startsWith(pattern)) {
318
+      return true;
319
+    }
320
+  }
321
+
322
+  return false;
323
+}
324
+
325
+function generateCacheKey(request: FastifyRequest): string {
326
+  const url = request.url;
327
+  const method = request.method;
328
+  const headers = request.headers;
329
+
330
+  // Include relevant headers in cache key
331
+  const relevantHeaders = ['accept', 'accept-encoding'];
332
+  const headerKey = relevantHeaders
333
+    .map(h => `${h}:${headers[h] || ''}`)
334
+    .join('|');
335
+
336
+  const keyString = `${method}:${url}:${headerKey}`;
337
+
338
+  return createHash('md5').update(keyString).digest('hex');
339
+}
340
+
341
+function getTTLForRoute(url: string, config: any): number {
342
+  for (const [pattern, ttl] of Object.entries(config.ttl)) {
343
+    if (url.startsWith(pattern)) {
344
+      return ttl as number;
345
+    }
346
+  }
347
+
348
+  return 5 * 60 * 1000; // Default 5 minutes
349
+}
server/src/middleware/performance.tsadded
@@ -0,0 +1,231 @@
1
+import type { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify';
2
+import { performance } from 'perf_hooks';
3
+
4
+interface PerformanceMetrics {
5
+  requestCount: number;
6
+  averageResponseTime: number;
7
+  p95ResponseTime: number;
8
+  p99ResponseTime: number;
9
+  errorRate: number;
10
+  activeConnections: number;
11
+  memoryUsage: NodeJS.MemoryUsage;
12
+  cpuUsage: NodeJS.CpuUsage;
13
+  responseTimes: number[];
14
+  errors: number;
15
+  lastReset: Date;
16
+}
17
+
18
+const metrics: PerformanceMetrics = {
19
+  requestCount: 0,
20
+  averageResponseTime: 0,
21
+  p95ResponseTime: 0,
22
+  p99ResponseTime: 0,
23
+  errorRate: 0,
24
+  activeConnections: 0,
25
+  memoryUsage: process.memoryUsage(),
26
+  cpuUsage: process.cpuUsage(),
27
+  responseTimes: [],
28
+  errors: 0,
29
+  lastReset: new Date(),
30
+};
31
+
32
+// Keep last 1000 response times for percentile calculations
33
+const MAX_RESPONSE_TIMES = 1000;
34
+
35
+export async function performanceMiddleware(fastify: FastifyInstance) {
36
+  // Request timing
37
+  fastify.addHook('onRequest', async (request: FastifyRequest) => {
38
+    request.startTime = performance.now();
39
+    metrics.activeConnections++;
40
+  });
41
+
42
+  fastify.addHook('onResponse', async (request: FastifyRequest, reply: FastifyReply) => {
43
+    const responseTime = performance.now() - (request.startTime || 0);
44
+
45
+    metrics.requestCount++;
46
+    metrics.activeConnections = Math.max(0, metrics.activeConnections - 1);
47
+
48
+    // Track response times
49
+    metrics.responseTimes.push(responseTime);
50
+    if (metrics.responseTimes.length > MAX_RESPONSE_TIMES) {
51
+      metrics.responseTimes.shift();
52
+    }
53
+
54
+    // Track errors
55
+    if (reply.statusCode >= 400) {
56
+      metrics.errors++;
57
+    }
58
+
59
+    // Update metrics
60
+    updateMetrics();
61
+
62
+    // Log slow requests
63
+    if (responseTime > 5000) {
64
+      fastify.log.warn({
65
+        url: request.url,
66
+        method: request.method,
67
+        responseTime,
68
+        statusCode: reply.statusCode,
69
+      }, 'Slow request detected');
70
+    }
71
+  });
72
+
73
+  fastify.addHook('onError', async (request: FastifyRequest, reply: FastifyReply, error: Error) => {
74
+    metrics.errors++;
75
+    metrics.activeConnections = Math.max(0, metrics.activeConnections - 1);
76
+    updateMetrics();
77
+  });
78
+
79
+  // Metrics endpoint
80
+  fastify.get('/api/metrics', async () => {
81
+    return {
82
+      ...metrics,
83
+      uptime: process.uptime(),
84
+      timestamp: new Date(),
85
+    };
86
+  });
87
+
88
+  // Health check with performance data
89
+  fastify.get('/api/health/detailed', async () => {
90
+    const memUsage = process.memoryUsage();
91
+    const cpuUsage = process.cpuUsage();
92
+
93
+    return {
94
+      status: 'healthy',
95
+      performance: {
96
+        responseTime: {
97
+          average: metrics.averageResponseTime,
98
+          p95: metrics.p95ResponseTime,
99
+          p99: metrics.p99ResponseTime,
100
+        },
101
+        throughput: {
102
+          requestsPerSecond: calculateRequestsPerSecond(),
103
+          activeConnections: metrics.activeConnections,
104
+        },
105
+        errors: {
106
+          rate: metrics.errorRate,
107
+          total: metrics.errors,
108
+        },
109
+        system: {
110
+          memory: {
111
+            used: memUsage.heapUsed,
112
+            total: memUsage.heapTotal,
113
+            external: memUsage.external,
114
+            rss: memUsage.rss,
115
+          },
116
+          cpu: {
117
+            user: cpuUsage.user,
118
+            system: cpuUsage.system,
119
+          },
120
+          uptime: process.uptime(),
121
+        },
122
+      },
123
+      timestamp: new Date(),
124
+    };
125
+  });
126
+
127
+  // Start periodic metrics collection
128
+  startMetricsCollection(fastify);
129
+}
130
+
131
+function updateMetrics() {
132
+  if (metrics.responseTimes.length === 0) return;
133
+
134
+  // Calculate average
135
+  const sum = metrics.responseTimes.reduce((a, b) => a + b, 0);
136
+  metrics.averageResponseTime = sum / metrics.responseTimes.length;
137
+
138
+  // Calculate percentiles
139
+  const sorted = [...metrics.responseTimes].sort((a, b) => a - b);
140
+  metrics.p95ResponseTime = percentile(sorted, 0.95);
141
+  metrics.p99ResponseTime = percentile(sorted, 0.99);
142
+
143
+  // Calculate error rate
144
+  metrics.errorRate = metrics.requestCount > 0 ? (metrics.errors / metrics.requestCount) * 100 : 0;
145
+
146
+  // Update system metrics
147
+  metrics.memoryUsage = process.memoryUsage();
148
+  metrics.cpuUsage = process.cpuUsage();
149
+}
150
+
151
+function percentile(sortedArray: number[], p: number): number {
152
+  if (sortedArray.length === 0) return 0;
153
+
154
+  const index = Math.ceil(sortedArray.length * p) - 1;
155
+  return sortedArray[Math.max(0, index)];
156
+}
157
+
158
+function calculateRequestsPerSecond(): number {
159
+  const now = new Date();
160
+  const timeDiff = (now.getTime() - metrics.lastReset.getTime()) / 1000;
161
+
162
+  if (timeDiff === 0) return 0;
163
+
164
+  return metrics.requestCount / timeDiff;
165
+}
166
+
167
+function startMetricsCollection(fastify: FastifyInstance) {
168
+  // Reset metrics periodically to prevent memory leaks
169
+  setInterval(() => {
170
+    // Keep recent data but reset counters
171
+    const recentResponseTimes = metrics.responseTimes.slice(-100);
172
+
173
+    metrics.responseTimes = recentResponseTimes;
174
+    metrics.requestCount = Math.floor(metrics.requestCount * 0.1); // Keep 10% for trend
175
+    metrics.errors = Math.floor(metrics.errors * 0.1);
176
+    metrics.lastReset = new Date();
177
+
178
+    updateMetrics();
179
+  }, 5 * 60 * 1000); // Reset every 5 minutes
180
+
181
+  // Log metrics periodically
182
+  setInterval(() => {
183
+    fastify.log.info({
184
+      metrics: {
185
+        requestCount: metrics.requestCount,
186
+        averageResponseTime: Math.round(metrics.averageResponseTime),
187
+        p95ResponseTime: Math.round(metrics.p95ResponseTime),
188
+        errorRate: Math.round(metrics.errorRate * 100) / 100,
189
+        activeConnections: metrics.activeConnections,
190
+        memoryUsageMB: Math.round(metrics.memoryUsage.heapUsed / 1024 / 1024),
191
+        requestsPerSecond: Math.round(calculateRequestsPerSecond() * 100) / 100,
192
+      },
193
+    }, 'Performance metrics');
194
+  }, 60 * 1000); // Log every minute
195
+}
196
+
197
+// Declare module augmentation for request
198
+declare module 'fastify' {
199
+  interface FastifyRequest {
200
+    startTime?: number;
201
+  }
202
+}
203
+
204
+// Response time tracking for caching decisions
205
+export function shouldCache(responseTime: number, statusCode: number): boolean {
206
+  // Cache successful responses that are reasonably fast
207
+  if (statusCode >= 200 && statusCode < 300) {
208
+    return responseTime < 1000; // Cache responses under 1 second
209
+  }
210
+
211
+  return false;
212
+}
213
+
214
+// Memory pressure detection
215
+export function isMemoryPressureHigh(): boolean {
216
+  const usage = process.memoryUsage();
217
+  const heapUsedMB = usage.heapUsed / 1024 / 1024;
218
+  const heapTotalMB = usage.heapTotal / 1024 / 1024;
219
+
220
+  // Consider memory pressure high if heap usage > 80%
221
+  return (heapUsedMB / heapTotalMB) > 0.8;
222
+}
223
+
224
+// CPU usage detection
225
+export function isCpuUsageHigh(): boolean {
226
+  const usage = process.cpuUsage();
227
+  const totalUsage = usage.user + usage.system;
228
+
229
+  // This is a simplified check - in production you'd want more sophisticated monitoring
230
+  return totalUsage > 500000; // 500ms of CPU time indicates high usage
231
+}