@adminforth/dashboard 1.9.0 → 1.11.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.
@@ -25,6 +25,13 @@ export type DashboardWidgetDataResponse = {
25
25
  data: unknown
26
26
  }
27
27
 
28
+ export type DashboardMutationResponse = {
29
+ ok: boolean
30
+ error?: string
31
+ groupId?: string
32
+ widgetId?: string
33
+ }
34
+
28
35
  export type DashboardWidgetDataRequest = {
29
36
  pagination?: {
30
37
  page: number
@@ -119,6 +126,36 @@ async function callDashboardApi(path: string, body: Record<string, unknown>): Pr
119
126
  }
120
127
  }
121
128
 
129
+ async function callDashboardMutationApi(path: string, body: Record<string, unknown>): Promise<DashboardMutationResponse> {
130
+ const rawResponse = await fetch(path, {
131
+ method: 'POST',
132
+ headers: {
133
+ 'Content-Type': 'application/json',
134
+ 'accept-language': localStorage.getItem('af_lang') || 'en',
135
+ },
136
+ body: JSON.stringify(body),
137
+ })
138
+
139
+ const response = await parseDashboardResponse(rawResponse)
140
+
141
+ if (!rawResponse.ok) {
142
+ throw new DashboardApiError(
143
+ response?.error || rawResponse.statusText || `Dashboard request failed (${rawResponse.status})`,
144
+ normalizeValidationErrors(response),
145
+ )
146
+ }
147
+
148
+ if (!response || response.error || response.ok === false) {
149
+ throw new DashboardApiError(response?.error || 'Dashboard request failed', normalizeValidationErrors(response))
150
+ }
151
+
152
+ return {
153
+ ok: true,
154
+ groupId: response.groupId,
155
+ widgetId: response.widgetId,
156
+ }
157
+ }
158
+
122
159
  async function callDashboardWidgetDataApi(
123
160
  path: string,
124
161
  body: Record<string, unknown>,
@@ -156,39 +193,39 @@ export const dashboardApi = {
156
193
  return callDashboardApi('/adminapi/v1/dashboard/get-config', { slug })
157
194
  },
158
195
 
159
- async addDashboardGroup(slug: string): Promise<DashboardResponse> {
160
- return callDashboardApi('/adminapi/v1/dashboard/add_dashboard_group', { slug })
196
+ async addDashboardGroup(slug: string): Promise<DashboardMutationResponse> {
197
+ return callDashboardMutationApi('/adminapi/v1/dashboard/add_dashboard_group', { slug })
161
198
  },
162
199
 
163
200
  async moveDashboardGroup(
164
201
  slug: string,
165
202
  groupId: string,
166
203
  direction: DashboardGroupMoveDirection,
167
- ): Promise<DashboardResponse> {
168
- return callDashboardApi('/adminapi/v1/dashboard/move_dashboard_group', {
204
+ ): Promise<DashboardMutationResponse> {
205
+ return callDashboardMutationApi('/adminapi/v1/dashboard/move_dashboard_group', {
169
206
  slug,
170
207
  groupId,
171
208
  direction,
172
209
  })
173
210
  },
174
211
 
175
- async removeDashboardGroup(slug: string, groupId: string): Promise<DashboardResponse> {
176
- return callDashboardApi('/adminapi/v1/dashboard/remove_dashboard_group', {
212
+ async removeDashboardGroup(slug: string, groupId: string): Promise<DashboardMutationResponse> {
213
+ return callDashboardMutationApi('/adminapi/v1/dashboard/remove_dashboard_group', {
177
214
  slug,
178
215
  groupId,
179
216
  })
180
217
  },
181
218
 
182
- async setDashboardGroupConfig(slug: string, groupId: string, config: EditableDashboardGroupConfig): Promise<DashboardResponse> {
183
- return callDashboardApi('/adminapi/v1/dashboard/set_dashboard_group_config', {
219
+ async setDashboardGroupConfig(slug: string, groupId: string, config: EditableDashboardGroupConfig): Promise<DashboardMutationResponse> {
220
+ return callDashboardMutationApi('/adminapi/v1/dashboard/set_dashboard_group_config', {
184
221
  slug,
185
222
  groupId,
186
223
  config,
187
224
  })
188
225
  },
189
226
 
190
- async addDashboardWidget(slug: string, groupId: string): Promise<DashboardResponse> {
191
- return callDashboardApi('/adminapi/v1/dashboard/add_dashboard_widget', {
227
+ async addDashboardWidget(slug: string, groupId: string): Promise<DashboardMutationResponse> {
228
+ return callDashboardMutationApi('/adminapi/v1/dashboard/add_dashboard_widget', {
192
229
  slug,
193
230
  groupId,
194
231
  })
@@ -198,23 +235,23 @@ export const dashboardApi = {
198
235
  slug: string,
199
236
  widgetId: string,
200
237
  direction: DashboardWidgetMoveDirection,
201
- ): Promise<DashboardResponse> {
202
- return callDashboardApi('/adminapi/v1/dashboard/move_dashboard_widget', {
238
+ ): Promise<DashboardMutationResponse> {
239
+ return callDashboardMutationApi('/adminapi/v1/dashboard/move_dashboard_widget', {
203
240
  slug,
204
241
  widgetId,
205
242
  direction,
206
243
  })
207
244
  },
208
245
 
209
- async removeDashboardWidget(slug: string, widgetId: string): Promise<DashboardResponse> {
210
- return callDashboardApi('/adminapi/v1/dashboard/remove_dashboard_widget', {
246
+ async removeDashboardWidget(slug: string, widgetId: string): Promise<DashboardMutationResponse> {
247
+ return callDashboardMutationApi('/adminapi/v1/dashboard/remove_dashboard_widget', {
211
248
  slug,
212
249
  widgetId,
213
250
  })
214
251
  },
215
252
 
216
- async setWidgetConfig(slug: string, widgetId: string, config: unknown): Promise<DashboardResponse> {
217
- return callDashboardApi('/adminapi/v1/dashboard/set_widget_config', {
253
+ async setWidgetConfig(slug: string, widgetId: string, config: unknown): Promise<DashboardMutationResponse> {
254
+ return callDashboardMutationApi('/adminapi/v1/dashboard/set_widget_config', {
218
255
  slug,
219
256
  widgetId,
220
257
  config,
@@ -225,8 +262,8 @@ export const dashboardApi = {
225
262
  slug: string,
226
263
  widgetId: string,
227
264
  config: ConfigurableTableWidgetConfig,
228
- ): Promise<DashboardResponse> {
229
- return callDashboardApi('/adminapi/v1/dashboard/configure_table_widget', {
265
+ ): Promise<DashboardMutationResponse> {
266
+ return callDashboardMutationApi('/adminapi/v1/dashboard/configure_table_widget', {
230
267
  slug,
231
268
  widgetId,
232
269
  config,
@@ -237,8 +274,8 @@ export const dashboardApi = {
237
274
  slug: string,
238
275
  widgetId: string,
239
276
  config: ConfigurableKpiCardWidgetConfig,
240
- ): Promise<DashboardResponse> {
241
- return callDashboardApi('/adminapi/v1/dashboard/configure_kpi_card_widget', {
277
+ ): Promise<DashboardMutationResponse> {
278
+ return callDashboardMutationApi('/adminapi/v1/dashboard/configure_kpi_card_widget', {
242
279
  slug,
243
280
  widgetId,
244
281
  config,
@@ -249,8 +286,8 @@ export const dashboardApi = {
249
286
  slug: string,
250
287
  widgetId: string,
251
288
  config: ConfigurableGaugeCardWidgetConfig,
252
- ): Promise<DashboardResponse> {
253
- return callDashboardApi('/adminapi/v1/dashboard/configure_gauge_card_widget', {
289
+ ): Promise<DashboardMutationResponse> {
290
+ return callDashboardMutationApi('/adminapi/v1/dashboard/configure_gauge_card_widget', {
254
291
  slug,
255
292
  widgetId,
256
293
  config,
@@ -261,8 +298,8 @@ export const dashboardApi = {
261
298
  slug: string,
262
299
  widgetId: string,
263
300
  config: ConfigurableLineChartWidgetConfig,
264
- ): Promise<DashboardResponse> {
265
- return callDashboardApi('/adminapi/v1/dashboard/configure_line_chart_widget', {
301
+ ): Promise<DashboardMutationResponse> {
302
+ return callDashboardMutationApi('/adminapi/v1/dashboard/configure_line_chart_widget', {
266
303
  slug,
267
304
  widgetId,
268
305
  config,
@@ -273,8 +310,8 @@ export const dashboardApi = {
273
310
  slug: string,
274
311
  widgetId: string,
275
312
  config: ConfigurableBarChartWidgetConfig,
276
- ): Promise<DashboardResponse> {
277
- return callDashboardApi('/adminapi/v1/dashboard/configure_bar_chart_widget', {
313
+ ): Promise<DashboardMutationResponse> {
314
+ return callDashboardMutationApi('/adminapi/v1/dashboard/configure_bar_chart_widget', {
278
315
  slug,
279
316
  widgetId,
280
317
  config,
@@ -285,8 +322,8 @@ export const dashboardApi = {
285
322
  slug: string,
286
323
  widgetId: string,
287
324
  config: ConfigurableStackedBarChartWidgetConfig,
288
- ): Promise<DashboardResponse> {
289
- return callDashboardApi('/adminapi/v1/dashboard/configure_stacked_bar_chart_widget', {
325
+ ): Promise<DashboardMutationResponse> {
326
+ return callDashboardMutationApi('/adminapi/v1/dashboard/configure_stacked_bar_chart_widget', {
290
327
  slug,
291
328
  widgetId,
292
329
  config,
@@ -297,8 +334,8 @@ export const dashboardApi = {
297
334
  slug: string,
298
335
  widgetId: string,
299
336
  config: ConfigurablePieChartWidgetConfig,
300
- ): Promise<DashboardResponse> {
301
- return callDashboardApi('/adminapi/v1/dashboard/configure_pie_chart_widget', {
337
+ ): Promise<DashboardMutationResponse> {
338
+ return callDashboardMutationApi('/adminapi/v1/dashboard/configure_pie_chart_widget', {
302
339
  slug,
303
340
  widgetId,
304
341
  config,
@@ -309,8 +346,8 @@ export const dashboardApi = {
309
346
  slug: string,
310
347
  widgetId: string,
311
348
  config: ConfigurableHistogramChartWidgetConfig,
312
- ): Promise<DashboardResponse> {
313
- return callDashboardApi('/adminapi/v1/dashboard/configure_histogram_chart_widget', {
349
+ ): Promise<DashboardMutationResponse> {
350
+ return callDashboardMutationApi('/adminapi/v1/dashboard/configure_histogram_chart_widget', {
314
351
  slug,
315
352
  widgetId,
316
353
  config,
@@ -321,8 +358,8 @@ export const dashboardApi = {
321
358
  slug: string,
322
359
  widgetId: string,
323
360
  config: ConfigurableFunnelChartWidgetConfig,
324
- ): Promise<DashboardResponse> {
325
- return callDashboardApi('/adminapi/v1/dashboard/configure_funnel_chart_widget', {
361
+ ): Promise<DashboardMutationResponse> {
362
+ return callDashboardMutationApi('/adminapi/v1/dashboard/configure_funnel_chart_widget', {
326
363
  slug,
327
364
  widgetId,
328
365
  config,
@@ -333,8 +370,8 @@ export const dashboardApi = {
333
370
  slug: string,
334
371
  widgetId: string,
335
372
  config: ConfigurablePivotTableWidgetConfig,
336
- ): Promise<DashboardResponse> {
337
- return callDashboardApi('/adminapi/v1/dashboard/configure_pivot_table_widget', {
373
+ ): Promise<DashboardMutationResponse> {
374
+ return callDashboardMutationApi('/adminapi/v1/dashboard/configure_pivot_table_widget', {
338
375
  slug,
339
376
  widgetId,
340
377
  config,
@@ -286,7 +286,8 @@ async function addGroup() {
286
286
  }
287
287
 
288
288
  try {
289
- applyDashboardResponse(await dashboardApi.addDashboardGroup(props.dashboardSlug))
289
+ await dashboardApi.addDashboardGroup(props.dashboardSlug)
290
+ await refreshDashboardConfig()
290
291
  } catch (error) {
291
292
  console.error('Failed to add dashboard group', error)
292
293
  }
@@ -298,7 +299,8 @@ async function addWidget(groupId: string) {
298
299
  }
299
300
 
300
301
  try {
301
- applyDashboardResponse(await dashboardApi.addDashboardWidget(props.dashboardSlug, groupId))
302
+ await dashboardApi.addDashboardWidget(props.dashboardSlug, groupId)
303
+ await refreshDashboardConfig()
302
304
  } catch (error) {
303
305
  console.error('Failed to add dashboard widget', error)
304
306
  }
@@ -310,9 +312,8 @@ async function moveGroup(groupId: string, direction: DashboardGroupMoveDirection
310
312
  }
311
313
 
312
314
  try {
313
- applyDashboardResponse(
314
- await dashboardApi.moveDashboardGroup(props.dashboardSlug, groupId, direction),
315
- )
315
+ await dashboardApi.moveDashboardGroup(props.dashboardSlug, groupId, direction)
316
+ await refreshDashboardConfig()
316
317
  } catch (error) {
317
318
  console.error('Failed to move dashboard group', error)
318
319
  }
@@ -324,7 +325,8 @@ async function removeGroup(groupId: string) {
324
325
  }
325
326
 
326
327
  try {
327
- applyDashboardResponse(await dashboardApi.removeDashboardGroup(props.dashboardSlug, groupId))
328
+ await dashboardApi.removeDashboardGroup(props.dashboardSlug, groupId)
329
+ await refreshDashboardConfig()
328
330
  } catch (error) {
329
331
  console.error('Failed to remove dashboard group', error)
330
332
  }
@@ -348,13 +350,12 @@ async function saveGroupConfig() {
348
350
  try {
349
351
  const groupConfig = parseYaml(groupConfigCode.value) as EditableDashboardGroupConfig
350
352
 
351
- applyDashboardResponse(
352
- await dashboardApi.setDashboardGroupConfig(
353
- props.dashboardSlug,
354
- editingGroupId.value,
355
- groupConfig,
356
- ),
353
+ await dashboardApi.setDashboardGroupConfig(
354
+ props.dashboardSlug,
355
+ editingGroupId.value,
356
+ groupConfig,
357
357
  )
358
+ await refreshDashboardConfig()
358
359
  closeGroupConfigEditor()
359
360
  } catch (error) {
360
361
  groupConfigError.value = error instanceof Error ? error.message : 'Invalid group config'
@@ -373,9 +374,8 @@ async function moveWidget(widgetId: string, direction: DashboardWidgetMoveDirect
373
374
  }
374
375
 
375
376
  try {
376
- applyDashboardResponse(
377
- await dashboardApi.moveDashboardWidget(props.dashboardSlug, widgetId, direction),
378
- )
377
+ await dashboardApi.moveDashboardWidget(props.dashboardSlug, widgetId, direction)
378
+ await refreshDashboardConfig()
379
379
  } catch (error) {
380
380
  console.error('Failed to move dashboard widget', error)
381
381
  }
@@ -387,7 +387,8 @@ async function removeWidget(widgetId: string) {
387
387
  }
388
388
 
389
389
  try {
390
- applyDashboardResponse(await dashboardApi.removeDashboardWidget(props.dashboardSlug, widgetId))
390
+ await dashboardApi.removeDashboardWidget(props.dashboardSlug, widgetId)
391
+ await refreshDashboardConfig()
391
392
  } catch (error) {
392
393
  console.error('Failed to remove dashboard widget', error)
393
394
  }
@@ -410,13 +411,12 @@ async function saveWidgetConfig() {
410
411
  widgetConfigFieldErrors.value = []
411
412
  const widgetConfig = parseYaml(widgetConfigCode.value) as DashboardWidgetConfig
412
413
 
413
- applyDashboardResponse(
414
- await dashboardApi.setWidgetConfig(
415
- props.dashboardSlug,
416
- editingWidgetId.value,
417
- serializeDashboardWidgetConfigForEditor(widgetConfig),
418
- ),
414
+ await dashboardApi.setWidgetConfig(
415
+ props.dashboardSlug,
416
+ editingWidgetId.value,
417
+ serializeDashboardWidgetConfigForEditor(widgetConfig),
419
418
  )
419
+ await refreshDashboardConfig()
420
420
  closeWidgetConfigEditor()
421
421
  } catch (error) {
422
422
  widgetConfigError.value = error instanceof Error ? error.message : 'Invalid widget config'
@@ -431,6 +431,10 @@ function closeWidgetConfigEditor() {
431
431
  widgetConfigFieldErrors.value = []
432
432
  }
433
433
 
434
+ async function refreshDashboardConfig() {
435
+ applyDashboardResponse(await dashboardApi.getDashboardConfig(props.dashboardSlug))
436
+ }
437
+
434
438
  function applyDashboardResponse(response: DashboardResponse) {
435
439
  draftConfig.value = cloneConfig(response.config)
436
440
  currentRevision.value = response.revision
@@ -22,26 +22,320 @@ Dashboard root, groups, and widgets are different entities.
22
22
 
23
23
  ## Tool routing
24
24
 
25
+ - Get dashboard slugs: dashboard_get_slugs
25
26
  - Read dashboard: dashboard_get_config
26
27
  - Add group: dashboard_add_dashboard_group
27
28
  - Rename group: dashboard_set_dashboard_group_config
28
29
  - Add widget slot: dashboard_add_dashboard_widget
29
- - Configure table widget: dashboard_configure_table_widget
30
- - Configure KPI card widget: dashboard_configure_kpi_card_widget
31
- - Configure gauge card widget: dashboard_configure_gauge_card_widget
32
- - Configure line chart widget: dashboard_configure_line_chart_widget
33
- - Configure bar chart widget: dashboard_configure_bar_chart_widget
34
- - Configure stacked bar chart widget: dashboard_configure_stacked_bar_chart_widget
35
- - Configure pie chart widget: dashboard_configure_pie_chart_widget
36
- - Configure histogram chart widget: dashboard_configure_histogram_chart_widget
37
- - Configure funnel chart widget: dashboard_configure_funnel_chart_widget
38
- - Configure pivot table widget: dashboard_configure_pivot_table_widget
30
+ - Configure widget:
31
+ - table: dashboard_configure_table_widget
32
+ - kpi_card: dashboard_configure_kpi_card_widget
33
+ - gauge_card: dashboard_configure_gauge_card_widget
34
+ - pivot_table: dashboard_configure_pivot_table_widget
35
+ - line_chart: dashboard_configure_line_chart_widget
36
+ - bar_chart: dashboard_configure_bar_chart_widget
37
+ - stacked_bar_chart: dashboard_configure_stacked_bar_chart_widget
38
+ - pie_chart: dashboard_configure_pie_chart_widget
39
+ - histogram_chart: dashboard_configure_histogram_chart_widget
40
+ - funnel_chart: dashboard_configure_funnel_chart_widget
39
41
  - Move/remove widget/group: matching move/remove tool
40
42
  - Load widget data: dashboard_get_dashboard_widget_data
41
43
 
42
44
  If a known dashboard tool schema is missing, call fetch_tool_schema for that exact tool.
43
45
  If fetch_tool_schema returns but the intended tool is still not callable, stop and report a tool-routing error. Do not substitute another mutation tool.
44
46
 
47
+ ## Configure schema examples
48
+
49
+ These examples show the expected shape only. Do not copy them one-to-one: adapt resource names, fields, aggregations, labels, filters, formats, and calculations to the actual dashboard request and available resource columns.
50
+
51
+ Important:
52
+ - `config.target` is the widget target, never a resource path.
53
+ - Never use values like `/resource/llm_usage` in `config.target`.
54
+ - Put the data resource in `config.query.resource`, for example `query.resource: "llm_usage"`.
55
+ - For chart widgets, `config.target` is always `chart`; the concrete chart kind is `config.chart.type`.
56
+ - `query.calcs[].calc` is an expression over already selected fields/aliases, not raw SQL. Do not use SQL syntax such as `CASE WHEN`.
57
+
58
+ Example `dashboard_configure_table_widget` config:
59
+
60
+ ```yaml
61
+ target: table
62
+ label: Recent usage
63
+ size: wide
64
+ table:
65
+ columns:
66
+ - field: used_at
67
+ label: Date
68
+ - field: model
69
+ label: Model
70
+ - field: total_tokens
71
+ label: Tokens
72
+ format: integer
73
+ pagination: true
74
+ page_size: 20
75
+ query:
76
+ resource: llm_usage
77
+ select:
78
+ - field: used_at
79
+ - field: model
80
+ - field: total_tokens
81
+ order_by:
82
+ - field: used_at
83
+ direction: desc
84
+ ```
85
+
86
+ Example `dashboard_configure_kpi_card_widget` config:
87
+
88
+ ```yaml
89
+ target: kpi_card
90
+ label: Total spend
91
+ size: medium
92
+ card:
93
+ title: Total spend
94
+ value:
95
+ field: spend
96
+ format: currency
97
+ query:
98
+ resource: llm_usage
99
+ select:
100
+ - agg: sum
101
+ field: cost
102
+ as: spend
103
+ ```
104
+
105
+ Example `dashboard_configure_gauge_card_widget` config:
106
+
107
+ ```yaml
108
+ target: gauge_card
109
+ label: Budget usage
110
+ size: medium
111
+ card:
112
+ title: Budget usage
113
+ value:
114
+ field: spend
115
+ format: currency
116
+ progress:
117
+ value_field: spend
118
+ target_value: 1000
119
+ format: percent
120
+ query:
121
+ resource: llm_usage
122
+ select:
123
+ - agg: sum
124
+ field: cost
125
+ as: spend
126
+ ```
127
+
128
+ Example `dashboard_configure_pivot_table_widget` config:
129
+
130
+ ```yaml
131
+ target: pivot_table
132
+ label: Spend by model and purpose
133
+ size: wide
134
+ pivot:
135
+ rows:
136
+ - field: model
137
+ label: Model
138
+ columns:
139
+ - field: purpose
140
+ label: Purpose
141
+ values:
142
+ - field: spend
143
+ label: Spend
144
+ format: currency
145
+ aggregation: sum
146
+ query:
147
+ resource: llm_usage
148
+ select:
149
+ - field: model
150
+ - field: purpose
151
+ - agg: sum
152
+ field: cost
153
+ as: spend
154
+ group_by:
155
+ - model
156
+ - purpose
157
+ ```
158
+
159
+ Example `dashboard_configure_line_chart_widget` config:
160
+
161
+ ```yaml
162
+ target: chart
163
+ label: Daily spend
164
+ size: wide
165
+ chart:
166
+ type: line
167
+ x:
168
+ field: day
169
+ label: Day
170
+ y:
171
+ - field: spend
172
+ label: Spend
173
+ format: currency
174
+ query:
175
+ resource: llm_usage
176
+ select:
177
+ - field: used_at
178
+ grain: day
179
+ as: day
180
+ - agg: sum
181
+ field: cost
182
+ as: spend
183
+ group_by:
184
+ - field: used_at
185
+ grain: day
186
+ as: day
187
+ order_by:
188
+ - field: day
189
+ direction: asc
190
+ ```
191
+
192
+ Example `dashboard_configure_bar_chart_widget` config:
193
+
194
+ ```yaml
195
+ target: chart
196
+ label: Spend by model
197
+ size: wide
198
+ chart:
199
+ type: bar
200
+ x:
201
+ field: model
202
+ label: Model
203
+ y:
204
+ field: spend
205
+ label: Spend
206
+ format: currency
207
+ query:
208
+ resource: llm_usage
209
+ select:
210
+ - field: model
211
+ - agg: sum
212
+ field: cost
213
+ as: spend
214
+ group_by:
215
+ - model
216
+ ```
217
+
218
+ Example `dashboard_configure_stacked_bar_chart_widget` config:
219
+
220
+ ```yaml
221
+ target: chart
222
+ label: Daily spend by purpose
223
+ size: wide
224
+ chart:
225
+ type: stacked_bar
226
+ x:
227
+ field: day
228
+ label: Day
229
+ y:
230
+ field: spend
231
+ label: Spend
232
+ format: currency
233
+ series:
234
+ field: purpose
235
+ label: Purpose
236
+ query:
237
+ resource: llm_usage
238
+ select:
239
+ - field: used_at
240
+ grain: day
241
+ as: day
242
+ - field: purpose
243
+ - agg: sum
244
+ field: cost
245
+ as: spend
246
+ group_by:
247
+ - field: used_at
248
+ grain: day
249
+ as: day
250
+ - purpose
251
+ order_by:
252
+ - field: day
253
+ direction: asc
254
+ ```
255
+
256
+ Example `dashboard_configure_pie_chart_widget` config:
257
+
258
+ ```yaml
259
+ target: chart
260
+ label: Spend share by model
261
+ size: medium
262
+ chart:
263
+ type: pie
264
+ label:
265
+ field: model
266
+ label: Model
267
+ value:
268
+ field: spend
269
+ label: Spend
270
+ format: currency
271
+ query:
272
+ resource: llm_usage
273
+ select:
274
+ - field: model
275
+ - agg: sum
276
+ field: cost
277
+ as: spend
278
+ group_by:
279
+ - model
280
+ ```
281
+
282
+ Example `dashboard_configure_histogram_chart_widget` config:
283
+
284
+ ```yaml
285
+ target: chart
286
+ label: Request size distribution
287
+ size: wide
288
+ chart:
289
+ type: histogram
290
+ x:
291
+ field: total_tokens
292
+ label: Tokens
293
+ y:
294
+ field: requests
295
+ label: Requests
296
+ query:
297
+ resource: llm_usage
298
+ select:
299
+ - field: total_tokens
300
+ - agg: count
301
+ as: requests
302
+ bucket:
303
+ field: total_tokens
304
+ buckets:
305
+ - label: Small
306
+ max: 1000
307
+ - label: Medium
308
+ min: 1000
309
+ max: 10000
310
+ - label: Large
311
+ min: 10000
312
+ ```
313
+
314
+ Example `dashboard_configure_funnel_chart_widget` config:
315
+
316
+ ```yaml
317
+ target: chart
318
+ label: Request funnel
319
+ size: wide
320
+ chart:
321
+ type: funnel
322
+ label:
323
+ field: stage
324
+ label: Stage
325
+ value:
326
+ field: count
327
+ label: Count
328
+ query:
329
+ resource: llm_usage
330
+ select:
331
+ - field: stage
332
+ - agg: count
333
+ as: count
334
+ group_by:
335
+ - stage
336
+ ```
337
+
338
+
45
339
  ## Group creation guard
46
340
 
47
341
  Before creating a group, call dashboard_get_config and check existing groups.
@@ -148,6 +442,7 @@ Use target, not type.
148
442
  For charts, use target: chart and chart.type for the concrete chart kind.
149
443
  Use query, not dataSource.
150
444
  Use resource, not resourceId.
445
+ Never use AdminForth routes such as /resource/llm_usage as resource or target values.
151
446
 
152
447
  ## Query shape rules
153
448