@littlebearapps/platform-admin-sdk 2.0.0 → 2.1.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.
- package/README.md +2 -2
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +86 -2
- package/package.json +1 -1
- package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
- package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
- package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
- package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
- package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
- package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
- package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
- package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
- package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
- package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
- package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
- package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
- package/templates/full/dashboard/src/lib/search/api.ts +258 -0
- package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
- package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
- package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
- package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
- package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
- package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
- package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
- package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
- package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
- package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
- package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
- package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
- package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
- package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
- package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
- package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
- package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
- package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
- package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
- package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
- package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
- package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
- package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
- package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
- package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
- package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
- package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
- package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
- package/templates/shared/tests/helpers/mock-storage.ts +166 -0
- package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
- package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
- package/templates/shared/tests/unit/billing.test.ts +331 -0
- package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
- package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
- package/templates/shared/tests/unit/control.test.ts +226 -0
- package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
- package/templates/shared/tests/unit/economics.test.ts +365 -0
- package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
- package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
- package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
- package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
- package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
- package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
- package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
- package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
- package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
- package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
- package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
- package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
- package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
- package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
- package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
- package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
- package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
- package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
- package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
- package/templates/standard/tests/unit/error-collector/github.test.ts +187 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search API Client
|
|
3
|
+
*
|
|
4
|
+
* Functions for interacting with the platform-search worker.
|
|
5
|
+
* Used by dashboard components for platform-wide search.
|
|
6
|
+
*
|
|
7
|
+
* @module dashboard/lib/search/api
|
|
8
|
+
* @created 2026-02-03
|
|
9
|
+
* @task task-303.1
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
SearchResponse,
|
|
14
|
+
SearchStatsResponse,
|
|
15
|
+
IndexDocumentRequest,
|
|
16
|
+
BulkIndexRequest,
|
|
17
|
+
BulkIndexResponse,
|
|
18
|
+
SearchQueryParams,
|
|
19
|
+
SearchContentType,
|
|
20
|
+
ContentTypeConfig,
|
|
21
|
+
} from './types';
|
|
22
|
+
|
|
23
|
+
/** Base URL for search API (via service binding proxy) */
|
|
24
|
+
const API_BASE = '/api/search';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Perform a search query
|
|
28
|
+
*/
|
|
29
|
+
export async function search(params: SearchQueryParams): Promise<SearchResponse> {
|
|
30
|
+
const searchParams = new URLSearchParams();
|
|
31
|
+
searchParams.set('q', params.q);
|
|
32
|
+
if (params.type) searchParams.set('type', params.type);
|
|
33
|
+
if (params.project) searchParams.set('project', params.project);
|
|
34
|
+
if (params.limit) searchParams.set('limit', String(params.limit));
|
|
35
|
+
if (params.offset) searchParams.set('offset', String(params.offset));
|
|
36
|
+
|
|
37
|
+
const url = `${API_BASE}?${searchParams.toString()}`;
|
|
38
|
+
const response = await fetch(url);
|
|
39
|
+
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error(`Search failed: ${response.statusText}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return response.json();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Index a single document
|
|
49
|
+
*/
|
|
50
|
+
export async function indexDocument(
|
|
51
|
+
doc: IndexDocumentRequest
|
|
52
|
+
): Promise<{ success: boolean; id: string }> {
|
|
53
|
+
const response = await fetch(`${API_BASE}/index`, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
body: JSON.stringify(doc),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
throw new Error(`Failed to index document: ${response.statusText}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return response.json();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Bulk index multiple documents
|
|
68
|
+
*/
|
|
69
|
+
export async function bulkIndexDocuments(data: BulkIndexRequest): Promise<BulkIndexResponse> {
|
|
70
|
+
const response = await fetch(`${API_BASE}/index/bulk`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify(data),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
throw new Error(`Bulk index failed: ${response.statusText}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return response.json();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Clear and reindex all documents of a content type
|
|
85
|
+
*/
|
|
86
|
+
export async function reindexContentType(
|
|
87
|
+
contentType: SearchContentType
|
|
88
|
+
): Promise<{ success: boolean; content_type: string; deleted: number; message: string }> {
|
|
89
|
+
const response = await fetch(`${API_BASE}/reindex/${contentType}`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
throw new Error(`Reindex failed: ${response.statusText}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return response.json();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Delete a document from the index
|
|
102
|
+
*/
|
|
103
|
+
export async function deleteDocument(id: string): Promise<{ success: boolean; deleted: string }> {
|
|
104
|
+
const response = await fetch(`${API_BASE}/index/${encodeURIComponent(id)}`, {
|
|
105
|
+
method: 'DELETE',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
if (response.status === 404) {
|
|
110
|
+
throw new Error(`Document not found: ${id}`);
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`Failed to delete document: ${response.statusText}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return response.json();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get search index statistics
|
|
120
|
+
*/
|
|
121
|
+
export async function getSearchStats(): Promise<SearchStatsResponse> {
|
|
122
|
+
const response = await fetch(`${API_BASE}/stats`);
|
|
123
|
+
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
throw new Error(`Failed to fetch search stats: ${response.statusText}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return response.json();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Content type configuration for UI display */
|
|
132
|
+
export const CONTENT_TYPE_CONFIG: Record<SearchContentType, ContentTypeConfig> = {
|
|
133
|
+
error: {
|
|
134
|
+
label: 'Errors',
|
|
135
|
+
icon: 'alert-circle',
|
|
136
|
+
color: 'text-red-500 dark:text-red-400',
|
|
137
|
+
},
|
|
138
|
+
pattern: {
|
|
139
|
+
label: 'Patterns',
|
|
140
|
+
icon: 'cpu',
|
|
141
|
+
color: 'text-purple-500 dark:text-purple-400',
|
|
142
|
+
},
|
|
143
|
+
setting: {
|
|
144
|
+
label: 'Settings',
|
|
145
|
+
icon: 'settings',
|
|
146
|
+
color: 'text-gray-500 dark:text-gray-400',
|
|
147
|
+
},
|
|
148
|
+
page: {
|
|
149
|
+
label: 'Pages',
|
|
150
|
+
icon: 'file-text',
|
|
151
|
+
color: 'text-blue-500 dark:text-blue-400',
|
|
152
|
+
},
|
|
153
|
+
service: {
|
|
154
|
+
label: 'Services',
|
|
155
|
+
icon: 'server',
|
|
156
|
+
color: 'text-green-500 dark:text-green-400',
|
|
157
|
+
},
|
|
158
|
+
opportunity: {
|
|
159
|
+
label: 'Opportunities',
|
|
160
|
+
icon: 'target',
|
|
161
|
+
color: 'text-orange-500 dark:text-orange-400',
|
|
162
|
+
},
|
|
163
|
+
draft: {
|
|
164
|
+
label: 'Drafts',
|
|
165
|
+
icon: 'edit',
|
|
166
|
+
color: 'text-cyan-500 dark:text-cyan-400',
|
|
167
|
+
},
|
|
168
|
+
project: {
|
|
169
|
+
label: 'Projects',
|
|
170
|
+
icon: 'folder',
|
|
171
|
+
color: 'text-indigo-500 dark:text-indigo-400',
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get content type configuration
|
|
177
|
+
*/
|
|
178
|
+
export function getContentTypeConfig(contentType: SearchContentType): ContentTypeConfig {
|
|
179
|
+
return (
|
|
180
|
+
CONTENT_TYPE_CONFIG[contentType] || {
|
|
181
|
+
label: contentType,
|
|
182
|
+
icon: 'file',
|
|
183
|
+
color: 'text-gray-500',
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Highlight search terms in text
|
|
190
|
+
*/
|
|
191
|
+
export function highlightSearchTerms(text: string, query: string): string {
|
|
192
|
+
if (!query.trim()) return text;
|
|
193
|
+
|
|
194
|
+
const terms = query
|
|
195
|
+
.toLowerCase()
|
|
196
|
+
.split(/\s+/)
|
|
197
|
+
.filter((t) => t.length > 2);
|
|
198
|
+
|
|
199
|
+
if (terms.length === 0) return text;
|
|
200
|
+
|
|
201
|
+
// Create regex pattern for all terms
|
|
202
|
+
const pattern = new RegExp(`(${terms.map(escapeRegex).join('|')})`, 'gi');
|
|
203
|
+
|
|
204
|
+
return text.replace(
|
|
205
|
+
pattern,
|
|
206
|
+
'<mark class="bg-yellow-200 dark:bg-yellow-800 rounded px-0.5">$1</mark>'
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Escape special regex characters
|
|
212
|
+
*/
|
|
213
|
+
function escapeRegex(str: string): string {
|
|
214
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Store recent searches in localStorage
|
|
219
|
+
*/
|
|
220
|
+
export function addRecentSearch(query: string): void {
|
|
221
|
+
if (typeof window === 'undefined') return;
|
|
222
|
+
|
|
223
|
+
const key = 'platform-search-recent';
|
|
224
|
+
const maxRecent = 10;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const existing = JSON.parse(localStorage.getItem(key) || '[]') as string[];
|
|
228
|
+
const filtered = existing.filter((q) => q !== query);
|
|
229
|
+
const updated = [query, ...filtered].slice(0, maxRecent);
|
|
230
|
+
localStorage.setItem(key, JSON.stringify(updated));
|
|
231
|
+
} catch {
|
|
232
|
+
// Ignore localStorage errors
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get recent searches from localStorage
|
|
238
|
+
*/
|
|
239
|
+
export function getRecentSearches(): string[] {
|
|
240
|
+
if (typeof window === 'undefined') return [];
|
|
241
|
+
|
|
242
|
+
const key = 'platform-search-recent';
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
return JSON.parse(localStorage.getItem(key) || '[]') as string[];
|
|
246
|
+
} catch {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Clear recent searches
|
|
253
|
+
*/
|
|
254
|
+
export function clearRecentSearches(): void {
|
|
255
|
+
if (typeof window === 'undefined') return;
|
|
256
|
+
|
|
257
|
+
localStorage.removeItem('platform-search-recent');
|
|
258
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Types
|
|
3
|
+
*
|
|
4
|
+
* TypeScript types for the platform search system.
|
|
5
|
+
*
|
|
6
|
+
* @module dashboard/lib/search/types
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Valid content types for search */
|
|
10
|
+
export type SearchContentType =
|
|
11
|
+
| 'error'
|
|
12
|
+
| 'pattern'
|
|
13
|
+
| 'setting'
|
|
14
|
+
| 'page'
|
|
15
|
+
| 'service'
|
|
16
|
+
| 'project';
|
|
17
|
+
|
|
18
|
+
/** Project scope for search results */
|
|
19
|
+
export type SearchProject = 'platform' | '{{projectSlug}}' | null;
|
|
20
|
+
|
|
21
|
+
/** A search document in the index */
|
|
22
|
+
export interface SearchDocument {
|
|
23
|
+
id: string;
|
|
24
|
+
content_type: SearchContentType;
|
|
25
|
+
project: SearchProject;
|
|
26
|
+
title: string;
|
|
27
|
+
content: string;
|
|
28
|
+
url: string;
|
|
29
|
+
metadata: string | null;
|
|
30
|
+
indexed_at: number;
|
|
31
|
+
source_updated_at: number | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A search result with ranking and snippet */
|
|
35
|
+
export interface SearchResult extends SearchDocument {
|
|
36
|
+
rank: number;
|
|
37
|
+
snippet: string;
|
|
38
|
+
parsed_metadata?: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Search results grouped by content type */
|
|
42
|
+
export interface GroupedSearchResults {
|
|
43
|
+
[contentType: string]: SearchResult[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Search response */
|
|
47
|
+
export interface SearchResponse {
|
|
48
|
+
results: SearchResult[];
|
|
49
|
+
grouped: GroupedSearchResults;
|
|
50
|
+
count: number;
|
|
51
|
+
query: string;
|
|
52
|
+
filters: {
|
|
53
|
+
type: SearchContentType | null;
|
|
54
|
+
project: string | null;
|
|
55
|
+
};
|
|
56
|
+
error?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Request to index a document */
|
|
60
|
+
export interface IndexDocumentRequest {
|
|
61
|
+
id: string;
|
|
62
|
+
content_type: SearchContentType;
|
|
63
|
+
project?: SearchProject;
|
|
64
|
+
title: string;
|
|
65
|
+
content: string;
|
|
66
|
+
url: string;
|
|
67
|
+
metadata?: Record<string, unknown>;
|
|
68
|
+
source_updated_at?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Bulk index request */
|
|
72
|
+
export interface BulkIndexRequest {
|
|
73
|
+
documents: IndexDocumentRequest[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Bulk index response */
|
|
77
|
+
export interface BulkIndexResponse {
|
|
78
|
+
success: boolean;
|
|
79
|
+
indexed: number;
|
|
80
|
+
failed: number;
|
|
81
|
+
total: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Index statistics response */
|
|
85
|
+
export interface SearchStatsResponse {
|
|
86
|
+
total: number;
|
|
87
|
+
by_type: Array<{
|
|
88
|
+
content_type: string;
|
|
89
|
+
count: number;
|
|
90
|
+
}>;
|
|
91
|
+
by_project: Array<{
|
|
92
|
+
project: string;
|
|
93
|
+
count: number;
|
|
94
|
+
}>;
|
|
95
|
+
index_range: {
|
|
96
|
+
oldest: string | null;
|
|
97
|
+
newest: string | null;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Query parameters for search */
|
|
102
|
+
export interface SearchQueryParams {
|
|
103
|
+
q: string;
|
|
104
|
+
type?: SearchContentType;
|
|
105
|
+
project?: string;
|
|
106
|
+
limit?: number;
|
|
107
|
+
offset?: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Content type configuration for UI */
|
|
111
|
+
export interface ContentTypeConfig {
|
|
112
|
+
label: string;
|
|
113
|
+
icon: string;
|
|
114
|
+
color: string;
|
|
115
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings API Client
|
|
3
|
+
*
|
|
4
|
+
* Functions for interacting with the platform-settings worker.
|
|
5
|
+
* Used by dashboard components to read and update settings.
|
|
6
|
+
*
|
|
7
|
+
* @module dashboard/lib/settings/api
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
Setting,
|
|
12
|
+
ParsedSetting,
|
|
13
|
+
SettingsListResponse,
|
|
14
|
+
CategorySettingsResponse,
|
|
15
|
+
UpdateSettingRequest,
|
|
16
|
+
BulkUpdateRequest,
|
|
17
|
+
BulkUpdateResponse,
|
|
18
|
+
SettingsQueryParams,
|
|
19
|
+
SettingsProject,
|
|
20
|
+
SettingsCategory,
|
|
21
|
+
} from './types';
|
|
22
|
+
|
|
23
|
+
/** Base URL for settings API (via service binding proxy) */
|
|
24
|
+
const API_BASE = '/api/settings';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Fetch all settings with optional filters
|
|
28
|
+
*/
|
|
29
|
+
export async function getSettings(params: SettingsQueryParams = {}): Promise<SettingsListResponse> {
|
|
30
|
+
const searchParams = new URLSearchParams();
|
|
31
|
+
if (params.project) searchParams.set('project', params.project);
|
|
32
|
+
if (params.category) searchParams.set('category', params.category);
|
|
33
|
+
|
|
34
|
+
const url = searchParams.toString() ? `${API_BASE}?${searchParams.toString()}` : API_BASE;
|
|
35
|
+
const response = await fetch(url);
|
|
36
|
+
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error(`Failed to fetch settings: ${response.statusText}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return response.json();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get settings for a specific project and category
|
|
46
|
+
*/
|
|
47
|
+
export async function getCategorySettings(
|
|
48
|
+
project: SettingsProject,
|
|
49
|
+
category: SettingsCategory
|
|
50
|
+
): Promise<CategorySettingsResponse> {
|
|
51
|
+
const response = await fetch(`${API_BASE}/${project}/${category}`);
|
|
52
|
+
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
throw new Error(`Failed to fetch category settings: ${response.statusText}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return response.json();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get a single setting by project, category, and key
|
|
62
|
+
*/
|
|
63
|
+
export async function getSetting(
|
|
64
|
+
project: SettingsProject,
|
|
65
|
+
category: SettingsCategory,
|
|
66
|
+
key: string
|
|
67
|
+
): Promise<ParsedSetting> {
|
|
68
|
+
const response = await fetch(`${API_BASE}/${project}/${category}/${key}`);
|
|
69
|
+
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
if (response.status === 404) {
|
|
72
|
+
throw new Error(`Setting not found: ${project}/${category}/${key}`);
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`Failed to fetch setting: ${response.statusText}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return response.json();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Update a setting
|
|
82
|
+
*/
|
|
83
|
+
export async function updateSetting(
|
|
84
|
+
project: SettingsProject,
|
|
85
|
+
category: SettingsCategory,
|
|
86
|
+
key: string,
|
|
87
|
+
data: UpdateSettingRequest
|
|
88
|
+
): Promise<Setting> {
|
|
89
|
+
const response = await fetch(`${API_BASE}/${project}/${category}/${key}`, {
|
|
90
|
+
method: 'PUT',
|
|
91
|
+
headers: { 'Content-Type': 'application/json' },
|
|
92
|
+
body: JSON.stringify(data),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
throw new Error(`Failed to update setting: ${response.statusText}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return response.json();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Delete a setting
|
|
104
|
+
*/
|
|
105
|
+
export async function deleteSetting(
|
|
106
|
+
project: SettingsProject,
|
|
107
|
+
category: SettingsCategory,
|
|
108
|
+
key: string
|
|
109
|
+
): Promise<{ success: boolean; deleted: string }> {
|
|
110
|
+
const response = await fetch(`${API_BASE}/${project}/${category}/${key}`, {
|
|
111
|
+
method: 'DELETE',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
if (response.status === 404) {
|
|
116
|
+
throw new Error(`Setting not found: ${project}/${category}/${key}`);
|
|
117
|
+
}
|
|
118
|
+
throw new Error(`Failed to delete setting: ${response.statusText}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return response.json();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Bulk update multiple settings
|
|
126
|
+
*/
|
|
127
|
+
export async function bulkUpdateSettings(data: BulkUpdateRequest): Promise<BulkUpdateResponse> {
|
|
128
|
+
const response = await fetch(`${API_BASE}/bulk`, {
|
|
129
|
+
method: 'PUT',
|
|
130
|
+
headers: { 'Content-Type': 'application/json' },
|
|
131
|
+
body: JSON.stringify(data),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
throw new Error(`Failed to bulk update settings: ${response.statusText}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return response.json();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Parse a JSON-encoded setting value with type safety
|
|
143
|
+
*/
|
|
144
|
+
export function parseSettingValue<T>(setting: Setting, defaultValue: T): T {
|
|
145
|
+
try {
|
|
146
|
+
const parsed = JSON.parse(setting.value);
|
|
147
|
+
return parsed as T;
|
|
148
|
+
} catch {
|
|
149
|
+
return defaultValue;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get project display name
|
|
155
|
+
*/
|
|
156
|
+
export function getProjectDisplayName(project: SettingsProject): string {
|
|
157
|
+
switch (project) {
|
|
158
|
+
case 'global':
|
|
159
|
+
return 'Global';
|
|
160
|
+
case 'platform':
|
|
161
|
+
return 'Platform';
|
|
162
|
+
case '{{projectSlug}}':
|
|
163
|
+
return '{{projectName}}';
|
|
164
|
+
default:
|
|
165
|
+
return project;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get category display name
|
|
171
|
+
*/
|
|
172
|
+
export function getCategoryDisplayName(category: SettingsCategory): string {
|
|
173
|
+
switch (category) {
|
|
174
|
+
case 'notifications':
|
|
175
|
+
return 'Notifications';
|
|
176
|
+
case 'thresholds':
|
|
177
|
+
return 'Thresholds';
|
|
178
|
+
case 'display':
|
|
179
|
+
return 'Display';
|
|
180
|
+
case 'api':
|
|
181
|
+
return 'API';
|
|
182
|
+
case 'features':
|
|
183
|
+
return 'Features';
|
|
184
|
+
default:
|
|
185
|
+
return category;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Format updated timestamp for display
|
|
191
|
+
*/
|
|
192
|
+
export function formatUpdatedAt(timestamp: number): string {
|
|
193
|
+
const date = new Date(timestamp * 1000);
|
|
194
|
+
return date.toLocaleDateString('en-AU', {
|
|
195
|
+
day: 'numeric',
|
|
196
|
+
month: 'short',
|
|
197
|
+
year: 'numeric',
|
|
198
|
+
hour: '2-digit',
|
|
199
|
+
minute: '2-digit',
|
|
200
|
+
});
|
|
201
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Types
|
|
3
|
+
*
|
|
4
|
+
* TypeScript types for the platform settings system.
|
|
5
|
+
*
|
|
6
|
+
* @module dashboard/lib/settings/types
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Valid project scopes for settings */
|
|
10
|
+
export type SettingsProject = 'global' | 'platform' | '{{projectSlug}}';
|
|
11
|
+
|
|
12
|
+
/** Valid setting categories */
|
|
13
|
+
export type SettingsCategory = 'notifications' | 'thresholds' | 'display' | 'api' | 'features';
|
|
14
|
+
|
|
15
|
+
/** A single setting */
|
|
16
|
+
export interface Setting {
|
|
17
|
+
id: string;
|
|
18
|
+
project: SettingsProject;
|
|
19
|
+
category: SettingsCategory;
|
|
20
|
+
key: string;
|
|
21
|
+
value: string; // JSON-encoded
|
|
22
|
+
description: string | null;
|
|
23
|
+
updated_at: number;
|
|
24
|
+
updated_by: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Setting with parsed value */
|
|
28
|
+
export interface ParsedSetting extends Setting {
|
|
29
|
+
parsed_value: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Settings grouped by project and category */
|
|
33
|
+
export interface GroupedSettings {
|
|
34
|
+
[project: string]: {
|
|
35
|
+
[category: string]: Setting[];
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Settings list response */
|
|
40
|
+
export interface SettingsListResponse {
|
|
41
|
+
settings: Setting[];
|
|
42
|
+
grouped: GroupedSettings;
|
|
43
|
+
count: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Settings for a specific project/category */
|
|
47
|
+
export interface CategorySettingsResponse {
|
|
48
|
+
project: SettingsProject;
|
|
49
|
+
category: SettingsCategory;
|
|
50
|
+
settings: ParsedSetting[];
|
|
51
|
+
count: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Request to update a setting */
|
|
55
|
+
export interface UpdateSettingRequest {
|
|
56
|
+
value: unknown;
|
|
57
|
+
description?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Bulk update request */
|
|
61
|
+
export interface BulkUpdateRequest {
|
|
62
|
+
settings: Array<{
|
|
63
|
+
project: SettingsProject;
|
|
64
|
+
category: SettingsCategory;
|
|
65
|
+
key: string;
|
|
66
|
+
value: unknown;
|
|
67
|
+
description?: string;
|
|
68
|
+
}>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Bulk update response */
|
|
72
|
+
export interface BulkUpdateResponse {
|
|
73
|
+
success: boolean;
|
|
74
|
+
total: number;
|
|
75
|
+
succeeded: number;
|
|
76
|
+
failed: number;
|
|
77
|
+
results: Array<{
|
|
78
|
+
id: string;
|
|
79
|
+
success: boolean;
|
|
80
|
+
error?: string;
|
|
81
|
+
}>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Query parameters for listing settings */
|
|
85
|
+
export interface SettingsQueryParams {
|
|
86
|
+
project?: SettingsProject;
|
|
87
|
+
category?: SettingsCategory;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Common global settings */
|
|
91
|
+
export interface GlobalNotificationSettings {
|
|
92
|
+
email_enabled: boolean;
|
|
93
|
+
slack_enabled: boolean;
|
|
94
|
+
digest_frequency: 'realtime' | 'hourly' | 'daily' | 'weekly';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface GlobalThresholdSettings {
|
|
98
|
+
warning_percent: number;
|
|
99
|
+
critical_percent: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface GlobalDisplaySettings {
|
|
103
|
+
dark_mode: 'system' | 'light' | 'dark';
|
|
104
|
+
}
|