@qwickapps/server 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +157 -0
  2. package/dist/core/control-panel.d.ts.map +1 -1
  3. package/dist/core/control-panel.js +114 -0
  4. package/dist/core/control-panel.js.map +1 -1
  5. package/dist/core/types.d.ts +19 -0
  6. package/dist/core/types.d.ts.map +1 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +4 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/plugins/auth/adapter-wrapper.d.ts +47 -0
  12. package/dist/plugins/auth/adapter-wrapper.d.ts.map +1 -0
  13. package/dist/plugins/auth/adapter-wrapper.js +166 -0
  14. package/dist/plugins/auth/adapter-wrapper.js.map +1 -0
  15. package/dist/plugins/auth/adapter-wrapper.test.d.ts +7 -0
  16. package/dist/plugins/auth/adapter-wrapper.test.d.ts.map +1 -0
  17. package/dist/plugins/auth/adapter-wrapper.test.js +303 -0
  18. package/dist/plugins/auth/adapter-wrapper.test.js.map +1 -0
  19. package/dist/plugins/auth/config-store.d.ts +11 -0
  20. package/dist/plugins/auth/config-store.d.ts.map +1 -0
  21. package/dist/plugins/auth/config-store.js +232 -0
  22. package/dist/plugins/auth/config-store.js.map +1 -0
  23. package/dist/plugins/auth/config-store.test.d.ts +7 -0
  24. package/dist/plugins/auth/config-store.test.d.ts.map +1 -0
  25. package/dist/plugins/auth/config-store.test.js +299 -0
  26. package/dist/plugins/auth/config-store.test.js.map +1 -0
  27. package/dist/plugins/auth/env-config.d.ts +51 -1
  28. package/dist/plugins/auth/env-config.d.ts.map +1 -1
  29. package/dist/plugins/auth/env-config.js +640 -7
  30. package/dist/plugins/auth/env-config.js.map +1 -1
  31. package/dist/plugins/auth/index.d.ts +6 -2
  32. package/dist/plugins/auth/index.d.ts.map +1 -1
  33. package/dist/plugins/auth/index.js +5 -1
  34. package/dist/plugins/auth/index.js.map +1 -1
  35. package/dist/plugins/auth/types.d.ts +106 -0
  36. package/dist/plugins/auth/types.d.ts.map +1 -1
  37. package/dist/plugins/index.d.ts +4 -2
  38. package/dist/plugins/index.d.ts.map +1 -1
  39. package/dist/plugins/index.js +3 -1
  40. package/dist/plugins/index.js.map +1 -1
  41. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts +7 -0
  42. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts.map +1 -0
  43. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js +220 -0
  44. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js.map +1 -0
  45. package/dist/plugins/rate-limit/cleanup.d.ts +40 -0
  46. package/dist/plugins/rate-limit/cleanup.d.ts.map +1 -0
  47. package/dist/plugins/rate-limit/cleanup.js +72 -0
  48. package/dist/plugins/rate-limit/cleanup.js.map +1 -0
  49. package/dist/plugins/rate-limit/env-config.d.ts +91 -0
  50. package/dist/plugins/rate-limit/env-config.d.ts.map +1 -0
  51. package/dist/plugins/rate-limit/env-config.js +318 -0
  52. package/dist/plugins/rate-limit/env-config.js.map +1 -0
  53. package/dist/plugins/rate-limit/index.d.ts +76 -0
  54. package/dist/plugins/rate-limit/index.d.ts.map +1 -0
  55. package/dist/plugins/rate-limit/index.js +79 -0
  56. package/dist/plugins/rate-limit/index.js.map +1 -0
  57. package/dist/plugins/rate-limit/middleware.d.ts +40 -0
  58. package/dist/plugins/rate-limit/middleware.d.ts.map +1 -0
  59. package/dist/plugins/rate-limit/middleware.js +169 -0
  60. package/dist/plugins/rate-limit/middleware.js.map +1 -0
  61. package/dist/plugins/rate-limit/rate-limit-plugin.d.ts +44 -0
  62. package/dist/plugins/rate-limit/rate-limit-plugin.d.ts.map +1 -0
  63. package/dist/plugins/rate-limit/rate-limit-plugin.js +354 -0
  64. package/dist/plugins/rate-limit/rate-limit-plugin.js.map +1 -0
  65. package/dist/plugins/rate-limit/rate-limit-service.d.ts +110 -0
  66. package/dist/plugins/rate-limit/rate-limit-service.d.ts.map +1 -0
  67. package/dist/plugins/rate-limit/rate-limit-service.js +172 -0
  68. package/dist/plugins/rate-limit/rate-limit-service.js.map +1 -0
  69. package/dist/plugins/rate-limit/stores/cache-store.d.ts +33 -0
  70. package/dist/plugins/rate-limit/stores/cache-store.d.ts.map +1 -0
  71. package/dist/plugins/rate-limit/stores/cache-store.js +225 -0
  72. package/dist/plugins/rate-limit/stores/cache-store.js.map +1 -0
  73. package/dist/plugins/rate-limit/stores/index.d.ts +8 -0
  74. package/dist/plugins/rate-limit/stores/index.d.ts.map +1 -0
  75. package/dist/plugins/rate-limit/stores/index.js +8 -0
  76. package/dist/plugins/rate-limit/stores/index.js.map +1 -0
  77. package/dist/plugins/rate-limit/stores/postgres-store.d.ts +34 -0
  78. package/dist/plugins/rate-limit/stores/postgres-store.d.ts.map +1 -0
  79. package/dist/plugins/rate-limit/stores/postgres-store.js +320 -0
  80. package/dist/plugins/rate-limit/stores/postgres-store.js.map +1 -0
  81. package/dist/plugins/rate-limit/strategies/fixed-window.d.ts +21 -0
  82. package/dist/plugins/rate-limit/strategies/fixed-window.d.ts.map +1 -0
  83. package/dist/plugins/rate-limit/strategies/fixed-window.js +97 -0
  84. package/dist/plugins/rate-limit/strategies/fixed-window.js.map +1 -0
  85. package/dist/plugins/rate-limit/strategies/index.d.ts +14 -0
  86. package/dist/plugins/rate-limit/strategies/index.d.ts.map +1 -0
  87. package/dist/plugins/rate-limit/strategies/index.js +27 -0
  88. package/dist/plugins/rate-limit/strategies/index.js.map +1 -0
  89. package/dist/plugins/rate-limit/strategies/sliding-window.d.ts +22 -0
  90. package/dist/plugins/rate-limit/strategies/sliding-window.d.ts.map +1 -0
  91. package/dist/plugins/rate-limit/strategies/sliding-window.js +122 -0
  92. package/dist/plugins/rate-limit/strategies/sliding-window.js.map +1 -0
  93. package/dist/plugins/rate-limit/strategies/token-bucket.d.ts +28 -0
  94. package/dist/plugins/rate-limit/strategies/token-bucket.d.ts.map +1 -0
  95. package/dist/plugins/rate-limit/strategies/token-bucket.js +121 -0
  96. package/dist/plugins/rate-limit/strategies/token-bucket.js.map +1 -0
  97. package/dist/plugins/rate-limit/types.d.ts +265 -0
  98. package/dist/plugins/rate-limit/types.d.ts.map +1 -0
  99. package/dist/plugins/rate-limit/types.js +9 -0
  100. package/dist/plugins/rate-limit/types.js.map +1 -0
  101. package/dist-ui/assets/index-D7DoZ9rL.js +478 -0
  102. package/dist-ui/assets/index-D7DoZ9rL.js.map +1 -0
  103. package/dist-ui/index.html +1 -1
  104. package/dist-ui-lib/api/controlPanelApi.d.ts +141 -0
  105. package/dist-ui-lib/dashboard/widgets/AuthStatusWidget.d.ts +9 -0
  106. package/dist-ui-lib/dashboard/widgets/IntegrationStatusWidget.d.ts +9 -0
  107. package/dist-ui-lib/dashboard/widgets/index.d.ts +2 -0
  108. package/dist-ui-lib/index.js +3332 -2343
  109. package/dist-ui-lib/index.js.map +1 -1
  110. package/dist-ui-lib/pages/IntegrationsPage.d.ts +1 -0
  111. package/dist-ui-lib/pages/RateLimitPage.d.ts +1 -0
  112. package/package.json +1 -1
  113. package/src/core/control-panel.ts +128 -0
  114. package/src/core/types.ts +17 -0
  115. package/src/index.ts +38 -0
  116. package/src/plugins/auth/adapter-wrapper.test.ts +395 -0
  117. package/src/plugins/auth/adapter-wrapper.ts +205 -0
  118. package/src/plugins/auth/config-store.test.ts +417 -0
  119. package/src/plugins/auth/config-store.ts +305 -0
  120. package/src/plugins/auth/env-config.ts +714 -7
  121. package/src/plugins/auth/index.ts +22 -1
  122. package/src/plugins/auth/types.ts +138 -0
  123. package/src/plugins/index.ts +49 -0
  124. package/src/plugins/rate-limit/__tests__/rate-limit-plugin.test.ts +259 -0
  125. package/src/plugins/rate-limit/cleanup.ts +117 -0
  126. package/src/plugins/rate-limit/env-config.ts +400 -0
  127. package/src/plugins/rate-limit/index.ts +128 -0
  128. package/src/plugins/rate-limit/middleware.ts +212 -0
  129. package/src/plugins/rate-limit/rate-limit-plugin.ts +400 -0
  130. package/src/plugins/rate-limit/rate-limit-service.ts +228 -0
  131. package/src/plugins/rate-limit/stores/cache-store.ts +261 -0
  132. package/src/plugins/rate-limit/stores/index.ts +8 -0
  133. package/src/plugins/rate-limit/stores/postgres-store.ts +402 -0
  134. package/src/plugins/rate-limit/strategies/fixed-window.ts +116 -0
  135. package/src/plugins/rate-limit/strategies/index.ts +30 -0
  136. package/src/plugins/rate-limit/strategies/sliding-window.ts +157 -0
  137. package/src/plugins/rate-limit/strategies/token-bucket.ts +154 -0
  138. package/src/plugins/rate-limit/types.ts +338 -0
  139. package/ui/src/App.tsx +32 -14
  140. package/ui/src/api/controlPanelApi.ts +226 -0
  141. package/ui/src/dashboard/builtInWidgets.tsx +5 -1
  142. package/ui/src/dashboard/widgets/AuthStatusWidget.tsx +143 -0
  143. package/ui/src/dashboard/widgets/IntegrationStatusWidget.tsx +135 -0
  144. package/ui/src/dashboard/widgets/index.ts +2 -0
  145. package/ui/src/pages/AuthPage.tsx +986 -142
  146. package/ui/src/pages/IntegrationsPage.tsx +288 -0
  147. package/ui/src/pages/RateLimitPage.tsx +292 -0
  148. package/dist-ui/assets/index-BY8OxNgO.js +0 -465
  149. package/dist-ui/assets/index-BY8OxNgO.js.map +0 -1
@@ -0,0 +1,288 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import {
3
+ Box,
4
+ Card,
5
+ CardContent,
6
+ Typography,
7
+ Button,
8
+ CircularProgress,
9
+ Alert,
10
+ IconButton,
11
+ Tooltip,
12
+ Chip,
13
+ LinearProgress,
14
+ } from '@mui/material';
15
+ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
16
+ import ErrorIcon from '@mui/icons-material/Error';
17
+ import RefreshIcon from '@mui/icons-material/Refresh';
18
+ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
19
+ import OpenInNewIcon from '@mui/icons-material/OpenInNew';
20
+ import { api } from '../api/controlPanelApi';
21
+
22
+ interface Integration {
23
+ id: string;
24
+ name: string;
25
+ description: string;
26
+ configured: boolean;
27
+ apiKey: string;
28
+ docsUrl: string;
29
+ }
30
+
31
+ interface IntegrationsConfig {
32
+ integrations: Integration[];
33
+ stats: {
34
+ totalRequests: number;
35
+ };
36
+ }
37
+
38
+ interface TestResult {
39
+ success: boolean;
40
+ message: string;
41
+ latency?: number;
42
+ }
43
+
44
+ export function IntegrationsPage() {
45
+ const [config, setConfig] = useState<IntegrationsConfig | null>(null);
46
+ const [loading, setLoading] = useState(true);
47
+ const [error, setError] = useState<string | null>(null);
48
+ const [testingId, setTestingId] = useState<string | null>(null);
49
+ const [testResults, setTestResults] = useState<Record<string, TestResult>>({});
50
+
51
+ const fetchConfig = useCallback(async () => {
52
+ setLoading(true);
53
+ setError(null);
54
+ try {
55
+ const data = await api.fetch<IntegrationsConfig>('/ai-proxy/config');
56
+ setConfig(data);
57
+ } catch (err) {
58
+ setError(err instanceof Error ? err.message : 'Failed to fetch integrations config');
59
+ } finally {
60
+ setLoading(false);
61
+ }
62
+ }, []);
63
+
64
+ useEffect(() => {
65
+ fetchConfig();
66
+ }, [fetchConfig]);
67
+
68
+ const handleTest = async (integrationId: string) => {
69
+ setTestingId(integrationId);
70
+ setTestResults((prev) => ({ ...prev, [integrationId]: { success: false, message: 'Testing...' } }));
71
+
72
+ try {
73
+ const result = await api.fetch<TestResult>(`/ai-proxy/test/${integrationId}`, {
74
+ method: 'POST',
75
+ });
76
+ setTestResults((prev) => ({ ...prev, [integrationId]: result }));
77
+ } catch (err) {
78
+ setTestResults((prev) => ({
79
+ ...prev,
80
+ [integrationId]: {
81
+ success: false,
82
+ message: err instanceof Error ? err.message : 'Test failed',
83
+ },
84
+ }));
85
+ } finally {
86
+ setTestingId(null);
87
+ }
88
+ };
89
+
90
+ if (loading) {
91
+ return (
92
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
93
+ <CircularProgress />
94
+ </Box>
95
+ );
96
+ }
97
+
98
+ return (
99
+ <Box>
100
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
101
+ <Typography variant="h4" sx={{ color: 'var(--theme-text-primary)' }}>
102
+ AI Integrations
103
+ </Typography>
104
+ <Tooltip title="Refresh">
105
+ <IconButton onClick={fetchConfig} sx={{ color: 'var(--theme-text-secondary)' }}>
106
+ <RefreshIcon />
107
+ </IconButton>
108
+ </Tooltip>
109
+ </Box>
110
+ <Typography variant="body2" sx={{ mb: 4, color: 'var(--theme-text-secondary)' }}>
111
+ Manage AI service connections for QwickBot
112
+ </Typography>
113
+
114
+ {error && (
115
+ <Alert severity="error" sx={{ mb: 3 }} onClose={() => setError(null)}>
116
+ {error}
117
+ </Alert>
118
+ )}
119
+
120
+ {/* Stats Card */}
121
+ {config && (
122
+ <Card sx={{ bgcolor: 'var(--theme-surface)', mb: 3 }}>
123
+ <CardContent>
124
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 3 }}>
125
+ <Box>
126
+ <Typography variant="h3" sx={{ color: 'var(--theme-primary)', fontWeight: 600 }}>
127
+ {config.stats.totalRequests}
128
+ </Typography>
129
+ <Typography variant="body2" sx={{ color: 'var(--theme-text-secondary)' }}>
130
+ Total API Requests
131
+ </Typography>
132
+ </Box>
133
+ <Box sx={{ borderLeft: '1px solid var(--theme-border)', pl: 3 }}>
134
+ <Typography variant="h3" sx={{ color: 'var(--theme-success)', fontWeight: 600 }}>
135
+ {config.integrations.filter((i) => i.configured).length}
136
+ </Typography>
137
+ <Typography variant="body2" sx={{ color: 'var(--theme-text-secondary)' }}>
138
+ Configured Services
139
+ </Typography>
140
+ </Box>
141
+ </Box>
142
+ </CardContent>
143
+ </Card>
144
+ )}
145
+
146
+ {/* Integration Cards */}
147
+ <Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 3 }}>
148
+ {config?.integrations.map((integration) => {
149
+ const testResult = testResults[integration.id];
150
+ const isTesting = testingId === integration.id;
151
+
152
+ return (
153
+ <Card key={integration.id} sx={{ bgcolor: 'var(--theme-surface)' }}>
154
+ <CardContent>
155
+ {/* Header */}
156
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
157
+ <Box>
158
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
159
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)' }}>
160
+ {integration.name}
161
+ </Typography>
162
+ <Chip
163
+ label={integration.configured ? 'Configured' : 'Not Configured'}
164
+ size="small"
165
+ sx={{
166
+ bgcolor: integration.configured ? 'var(--theme-success)20' : 'var(--theme-error)20',
167
+ color: integration.configured ? 'var(--theme-success)' : 'var(--theme-error)',
168
+ fontWeight: 500,
169
+ }}
170
+ />
171
+ </Box>
172
+ <Typography variant="body2" sx={{ color: 'var(--theme-text-secondary)' }}>
173
+ {integration.description}
174
+ </Typography>
175
+ </Box>
176
+ {integration.configured ? (
177
+ <CheckCircleIcon sx={{ color: 'var(--theme-success)' }} />
178
+ ) : (
179
+ <ErrorIcon sx={{ color: 'var(--theme-error)' }} />
180
+ )}
181
+ </Box>
182
+
183
+ {/* API Key */}
184
+ <Box sx={{ mb: 2, p: 1.5, bgcolor: 'var(--theme-background)', borderRadius: 1 }}>
185
+ <Typography variant="caption" sx={{ color: 'var(--theme-text-secondary)', display: 'block', mb: 0.5 }}>
186
+ API Key
187
+ </Typography>
188
+ <Typography
189
+ sx={{
190
+ fontFamily: 'monospace',
191
+ fontSize: 13,
192
+ color: integration.configured ? 'var(--theme-text-primary)' : 'var(--theme-text-secondary)',
193
+ }}
194
+ >
195
+ {integration.apiKey}
196
+ </Typography>
197
+ </Box>
198
+
199
+ {/* Test Result */}
200
+ {testResult && (
201
+ <Box sx={{ mb: 2 }}>
202
+ {isTesting ? (
203
+ <LinearProgress sx={{ height: 4, borderRadius: 2 }} />
204
+ ) : (
205
+ <Alert
206
+ severity={testResult.success ? 'success' : 'error'}
207
+ sx={{ py: 0.5, '& .MuiAlert-message': { fontSize: 13 } }}
208
+ >
209
+ {testResult.message}
210
+ {testResult.latency !== undefined && (
211
+ <Typography variant="caption" sx={{ display: 'block', mt: 0.5 }}>
212
+ Latency: {testResult.latency}ms
213
+ </Typography>
214
+ )}
215
+ </Alert>
216
+ )}
217
+ </Box>
218
+ )}
219
+
220
+ {/* Actions */}
221
+ <Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
222
+ <Button
223
+ size="small"
224
+ startIcon={<OpenInNewIcon />}
225
+ href={integration.docsUrl}
226
+ target="_blank"
227
+ sx={{
228
+ color: 'var(--theme-text-secondary)',
229
+ '&:hover': { color: 'var(--theme-primary)' },
230
+ }}
231
+ >
232
+ Docs
233
+ </Button>
234
+ <Button
235
+ size="small"
236
+ variant="outlined"
237
+ startIcon={isTesting ? <CircularProgress size={14} /> : <PlayArrowIcon />}
238
+ onClick={() => handleTest(integration.id)}
239
+ disabled={!integration.configured || isTesting}
240
+ sx={{
241
+ borderColor: 'var(--theme-border)',
242
+ color: 'var(--theme-text-primary)',
243
+ '&:hover': { borderColor: 'var(--theme-primary)' },
244
+ }}
245
+ >
246
+ Test Connection
247
+ </Button>
248
+ </Box>
249
+ </CardContent>
250
+ </Card>
251
+ );
252
+ })}
253
+ </Box>
254
+
255
+ {/* Setup Instructions */}
256
+ {config && config.integrations.some((i) => !i.configured) && (
257
+ <Card sx={{ bgcolor: 'var(--theme-surface)', mt: 3 }}>
258
+ <CardContent>
259
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)', mb: 2 }}>
260
+ Setup Instructions
261
+ </Typography>
262
+ <Typography variant="body2" sx={{ color: 'var(--theme-text-secondary)', mb: 2 }}>
263
+ Add the following environment variables to configure the AI services:
264
+ </Typography>
265
+ <Box
266
+ component="pre"
267
+ sx={{
268
+ p: 2,
269
+ bgcolor: 'var(--theme-background)',
270
+ borderRadius: 1,
271
+ overflow: 'auto',
272
+ fontFamily: 'monospace',
273
+ fontSize: 13,
274
+ color: 'var(--theme-text-primary)',
275
+ }}
276
+ >
277
+ {`# Groq - for chat (https://console.groq.com)
278
+ GROQ_API_KEY=your_groq_api_key
279
+
280
+ # Gemini - for vision (https://ai.google.dev)
281
+ GEMINI_API_KEY=your_gemini_api_key`}
282
+ </Box>
283
+ </CardContent>
284
+ </Card>
285
+ )}
286
+ </Box>
287
+ );
288
+ }
@@ -0,0 +1,292 @@
1
+ import { useState, useEffect } from 'react';
2
+ import {
3
+ Box,
4
+ Card,
5
+ CardContent,
6
+ Typography,
7
+ TextField,
8
+ Select,
9
+ MenuItem,
10
+ FormControl,
11
+ InputLabel,
12
+ Switch,
13
+ FormControlLabel,
14
+ Button,
15
+ CircularProgress,
16
+ Alert,
17
+ IconButton,
18
+ Tooltip,
19
+ Chip,
20
+ } from '@mui/material';
21
+ import RefreshIcon from '@mui/icons-material/Refresh';
22
+ import SaveIcon from '@mui/icons-material/Save';
23
+ import SpeedIcon from '@mui/icons-material/Speed';
24
+ import StorageIcon from '@mui/icons-material/Storage';
25
+ import CachedIcon from '@mui/icons-material/Cached';
26
+ import { api, RateLimitConfig, RateLimitStrategy } from '../api/controlPanelApi';
27
+
28
+ /**
29
+ * Format milliseconds to human-readable duration
30
+ */
31
+ function formatDuration(ms: number): string {
32
+ if (ms < 1000) return `${ms}ms`;
33
+ if (ms < 60000) return `${ms / 1000}s`;
34
+ if (ms < 3600000) return `${ms / 60000}m`;
35
+ return `${ms / 3600000}h`;
36
+ }
37
+
38
+ export function RateLimitPage() {
39
+ const [config, setConfig] = useState<RateLimitConfig | null>(null);
40
+ const [loading, setLoading] = useState(true);
41
+ const [saving, setSaving] = useState(false);
42
+ const [error, setError] = useState<string | null>(null);
43
+ const [success, setSuccess] = useState<string | null>(null);
44
+
45
+ // Editable fields
46
+ const [windowMs, setWindowMs] = useState<number>(60000);
47
+ const [maxRequests, setMaxRequests] = useState<number>(100);
48
+ const [strategy, setStrategy] = useState<RateLimitStrategy>('sliding-window');
49
+ const [cleanupEnabled, setCleanupEnabled] = useState<boolean>(true);
50
+ const [cleanupIntervalMs, setCleanupIntervalMs] = useState<number>(300000);
51
+
52
+ const fetchConfig = async () => {
53
+ setLoading(true);
54
+ setError(null);
55
+ try {
56
+ const data = await api.getRateLimitConfig();
57
+ setConfig(data);
58
+ // Update editable fields
59
+ setWindowMs(data.windowMs);
60
+ setMaxRequests(data.maxRequests);
61
+ setStrategy(data.strategy);
62
+ setCleanupEnabled(data.cleanupEnabled);
63
+ setCleanupIntervalMs(data.cleanupIntervalMs);
64
+ } catch (err) {
65
+ setError(err instanceof Error ? err.message : 'Failed to fetch config');
66
+ } finally {
67
+ setLoading(false);
68
+ }
69
+ };
70
+
71
+ useEffect(() => {
72
+ fetchConfig();
73
+ }, []);
74
+
75
+ const handleSave = async () => {
76
+ setSaving(true);
77
+ setError(null);
78
+ setSuccess(null);
79
+ try {
80
+ const result = await api.updateRateLimitConfig({
81
+ windowMs,
82
+ maxRequests,
83
+ strategy,
84
+ cleanupEnabled,
85
+ cleanupIntervalMs,
86
+ });
87
+ setConfig(result.config);
88
+ setSuccess('Configuration saved successfully');
89
+ setTimeout(() => setSuccess(null), 3000);
90
+ } catch (err) {
91
+ setError(err instanceof Error ? err.message : 'Failed to save config');
92
+ } finally {
93
+ setSaving(false);
94
+ }
95
+ };
96
+
97
+ const hasChanges = config && (
98
+ windowMs !== config.windowMs ||
99
+ maxRequests !== config.maxRequests ||
100
+ strategy !== config.strategy ||
101
+ cleanupEnabled !== config.cleanupEnabled ||
102
+ cleanupIntervalMs !== config.cleanupIntervalMs
103
+ );
104
+
105
+ if (loading) {
106
+ return (
107
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
108
+ <CircularProgress />
109
+ </Box>
110
+ );
111
+ }
112
+
113
+ if (error && !config) {
114
+ return (
115
+ <Card sx={{ bgcolor: 'var(--theme-surface)', border: '1px solid var(--theme-error)' }}>
116
+ <CardContent>
117
+ <Typography color="error">{error}</Typography>
118
+ </CardContent>
119
+ </Card>
120
+ );
121
+ }
122
+
123
+ return (
124
+ <Box>
125
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
126
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
127
+ <SpeedIcon sx={{ color: 'var(--theme-primary)', fontSize: 32 }} />
128
+ <Typography variant="h4" sx={{ color: 'var(--theme-text-primary)' }}>
129
+ Rate Limits
130
+ </Typography>
131
+ </Box>
132
+ <Box sx={{ display: 'flex', gap: 1 }}>
133
+ <Tooltip title="Refresh">
134
+ <IconButton onClick={fetchConfig} sx={{ color: 'var(--theme-text-secondary)' }}>
135
+ <RefreshIcon />
136
+ </IconButton>
137
+ </Tooltip>
138
+ <Button
139
+ variant="contained"
140
+ startIcon={saving ? <CircularProgress size={16} color="inherit" /> : <SaveIcon />}
141
+ onClick={handleSave}
142
+ disabled={saving || !hasChanges}
143
+ sx={{ minWidth: 100 }}
144
+ >
145
+ {saving ? 'Saving...' : 'Save'}
146
+ </Button>
147
+ </Box>
148
+ </Box>
149
+ <Typography variant="body2" sx={{ mb: 4, color: 'var(--theme-text-secondary)' }}>
150
+ Configure rate limiting defaults for your API
151
+ </Typography>
152
+
153
+ {/* Success/Error messages */}
154
+ {success && <Alert severity="success" sx={{ mb: 3 }}>{success}</Alert>}
155
+ {error && config && <Alert severity="error" sx={{ mb: 3 }}>{error}</Alert>}
156
+
157
+ {/* Status Card */}
158
+ <Card sx={{ bgcolor: 'var(--theme-surface)', mb: 3 }}>
159
+ <CardContent>
160
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)', mb: 2 }}>
161
+ System Status
162
+ </Typography>
163
+ <Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
164
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
165
+ <StorageIcon sx={{ color: 'var(--theme-text-secondary)' }} />
166
+ <Box>
167
+ <Typography variant="caption" sx={{ color: 'var(--theme-text-secondary)' }}>Store</Typography>
168
+ <Typography sx={{ color: 'var(--theme-text-primary)' }}>{config?.store}</Typography>
169
+ </Box>
170
+ </Box>
171
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
172
+ <CachedIcon sx={{ color: config?.cacheAvailable ? 'var(--theme-success)' : 'var(--theme-warning)' }} />
173
+ <Box>
174
+ <Typography variant="caption" sx={{ color: 'var(--theme-text-secondary)' }}>Cache</Typography>
175
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
176
+ <Typography sx={{ color: 'var(--theme-text-primary)' }}>{config?.cache}</Typography>
177
+ <Chip
178
+ label={config?.cacheAvailable ? 'Available' : 'Unavailable'}
179
+ size="small"
180
+ sx={{
181
+ bgcolor: config?.cacheAvailable ? 'var(--theme-success)20' : 'var(--theme-warning)20',
182
+ color: config?.cacheAvailable ? 'var(--theme-success)' : 'var(--theme-warning)',
183
+ }}
184
+ />
185
+ </Box>
186
+ </Box>
187
+ </Box>
188
+ </Box>
189
+ </CardContent>
190
+ </Card>
191
+
192
+ {/* Rate Limit Settings */}
193
+ <Card sx={{ bgcolor: 'var(--theme-surface)', mb: 3 }}>
194
+ <CardContent>
195
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)', mb: 3 }}>
196
+ Default Rate Limit Settings
197
+ </Typography>
198
+
199
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
200
+ {/* Strategy */}
201
+ <FormControl fullWidth>
202
+ <InputLabel sx={{ color: 'var(--theme-text-secondary)' }}>Strategy</InputLabel>
203
+ <Select
204
+ value={strategy}
205
+ label="Strategy"
206
+ onChange={(e) => setStrategy(e.target.value as RateLimitStrategy)}
207
+ sx={{
208
+ color: 'var(--theme-text-primary)',
209
+ '& .MuiOutlinedInput-notchedOutline': { borderColor: 'var(--theme-border)' },
210
+ }}
211
+ >
212
+ <MenuItem value="sliding-window">
213
+ Sliding Window - Smooth rate limiting with weighted overlap
214
+ </MenuItem>
215
+ <MenuItem value="fixed-window">
216
+ Fixed Window - Simple discrete time windows
217
+ </MenuItem>
218
+ <MenuItem value="token-bucket">
219
+ Token Bucket - Allows bursts while maintaining average rate
220
+ </MenuItem>
221
+ </Select>
222
+ </FormControl>
223
+
224
+ {/* Window and Max Requests */}
225
+ <Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
226
+ <TextField
227
+ label="Window (ms)"
228
+ type="number"
229
+ value={windowMs}
230
+ onChange={(e) => setWindowMs(Math.max(1000, parseInt(e.target.value) || 60000))}
231
+ helperText={`= ${formatDuration(windowMs)}`}
232
+ sx={{ flex: 1, minWidth: 200 }}
233
+ InputProps={{ inputProps: { min: 1000, step: 1000 } }}
234
+ />
235
+ <TextField
236
+ label="Max Requests"
237
+ type="number"
238
+ value={maxRequests}
239
+ onChange={(e) => setMaxRequests(Math.max(1, parseInt(e.target.value) || 100))}
240
+ helperText="Per window per key"
241
+ sx={{ flex: 1, minWidth: 200 }}
242
+ InputProps={{ inputProps: { min: 1 } }}
243
+ />
244
+ </Box>
245
+
246
+ {/* Summary */}
247
+ <Alert severity="info" sx={{ bgcolor: 'var(--theme-surface)' }}>
248
+ <Typography variant="body2">
249
+ <strong>Current limit:</strong> {maxRequests} requests per {formatDuration(windowMs)} using {strategy} strategy
250
+ </Typography>
251
+ </Alert>
252
+ </Box>
253
+ </CardContent>
254
+ </Card>
255
+
256
+ {/* Cleanup Settings */}
257
+ <Card sx={{ bgcolor: 'var(--theme-surface)' }}>
258
+ <CardContent>
259
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)', mb: 3 }}>
260
+ Cleanup Job
261
+ </Typography>
262
+
263
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
264
+ <FormControlLabel
265
+ control={
266
+ <Switch
267
+ checked={cleanupEnabled}
268
+ onChange={(e) => setCleanupEnabled(e.target.checked)}
269
+ color="primary"
270
+ />
271
+ }
272
+ label="Enable automatic cleanup of expired rate limits"
273
+ sx={{ color: 'var(--theme-text-primary)' }}
274
+ />
275
+
276
+ {cleanupEnabled && (
277
+ <TextField
278
+ label="Cleanup Interval (ms)"
279
+ type="number"
280
+ value={cleanupIntervalMs}
281
+ onChange={(e) => setCleanupIntervalMs(Math.max(60000, parseInt(e.target.value) || 300000))}
282
+ helperText={`= ${formatDuration(cleanupIntervalMs)}`}
283
+ sx={{ maxWidth: 300 }}
284
+ InputProps={{ inputProps: { min: 60000, step: 60000 } }}
285
+ />
286
+ )}
287
+ </Box>
288
+ </CardContent>
289
+ </Card>
290
+ </Box>
291
+ );
292
+ }