@codemieai/code 0.2.0 → 0.2.1

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 (117) hide show
  1. package/README.md +17 -6
  2. package/dist/agents/plugins/claude/plugin/skills/codemie-analytics/SKILL.md +641 -0
  3. package/dist/agents/plugins/claude/plugin/skills/codemie-analytics/references/leaderboard-dashboard-report.md +225 -0
  4. package/dist/agents/plugins/claude/plugin/skills/codemie-analytics/references/people-spending-dashboard-report.md +746 -0
  5. package/dist/agents/plugins/claude/plugin/skills/codemie-analytics/references/people-spending-dashboard-template.html +3270 -0
  6. package/dist/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/analytics-cli.js +893 -0
  7. package/dist/agents/plugins/claude/plugin/skills/codemie-analytics/scripts/inspect-schema.js +211 -0
  8. package/dist/agents/plugins/claude/plugin/skills/codemie-html-report/README.md +39 -0
  9. package/dist/agents/plugins/claude/plugin/skills/codemie-html-report/SKILL.md +117 -26
  10. package/dist/agents/plugins/claude/plugin/skills/codemie-html-report/scripts/inject-css.js +40 -0
  11. package/dist/agents/plugins/claude/plugin/skills/codemie-html-report/scripts/inject-data.js +68 -0
  12. package/dist/agents/plugins/claude/plugin/skills/codemie-html-report/style-guide/css/bundle.css +1 -0
  13. package/dist/agents/plugins/claude/plugin/skills/codemie-sdk/SKILL.md +240 -0
  14. package/dist/agents/plugins/claude/plugin/skills/codemie-sdk/examples/assistants.md +256 -0
  15. package/dist/agents/plugins/claude/plugin/skills/codemie-sdk/examples/categories.md +101 -0
  16. package/dist/agents/plugins/claude/plugin/skills/codemie-sdk/examples/datasources.md +401 -0
  17. package/dist/agents/plugins/claude/plugin/skills/codemie-sdk/examples/integrations.md +242 -0
  18. package/dist/agents/plugins/claude/plugin/skills/codemie-sdk/examples/skills.md +191 -0
  19. package/dist/agents/plugins/claude/plugin/skills/codemie-sdk/examples/users.md +38 -0
  20. package/dist/agents/plugins/claude/plugin/skills/codemie-sdk/examples/workflows.md +151 -0
  21. package/dist/cli/commands/profile/display.d.ts.map +1 -1
  22. package/dist/cli/commands/profile/display.js +1 -0
  23. package/dist/cli/commands/profile/display.js.map +1 -1
  24. package/dist/cli/commands/proxy/connectors/desktop.d.ts.map +1 -1
  25. package/dist/cli/commands/proxy/connectors/desktop.js +20 -10
  26. package/dist/cli/commands/proxy/connectors/desktop.js.map +1 -1
  27. package/dist/cli/commands/sdk/assistants.d.ts +3 -0
  28. package/dist/cli/commands/sdk/assistants.d.ts.map +1 -0
  29. package/dist/cli/commands/sdk/assistants.js +211 -0
  30. package/dist/cli/commands/sdk/assistants.js.map +1 -0
  31. package/dist/cli/commands/sdk/categories.d.ts +3 -0
  32. package/dist/cli/commands/sdk/categories.d.ts.map +1 -0
  33. package/dist/cli/commands/sdk/categories.js +186 -0
  34. package/dist/cli/commands/sdk/categories.js.map +1 -0
  35. package/dist/cli/commands/sdk/datasources.d.ts +3 -0
  36. package/dist/cli/commands/sdk/datasources.d.ts.map +1 -0
  37. package/dist/cli/commands/sdk/datasources.js +276 -0
  38. package/dist/cli/commands/sdk/datasources.js.map +1 -0
  39. package/dist/cli/commands/sdk/index.d.ts +3 -0
  40. package/dist/cli/commands/sdk/index.d.ts.map +1 -0
  41. package/dist/cli/commands/sdk/index.js +23 -0
  42. package/dist/cli/commands/sdk/index.js.map +1 -0
  43. package/dist/cli/commands/sdk/integrations.d.ts +3 -0
  44. package/dist/cli/commands/sdk/integrations.d.ts.map +1 -0
  45. package/dist/cli/commands/sdk/integrations.js +220 -0
  46. package/dist/cli/commands/sdk/integrations.js.map +1 -0
  47. package/dist/cli/commands/sdk/llm.d.ts +3 -0
  48. package/dist/cli/commands/sdk/llm.d.ts.map +1 -0
  49. package/dist/cli/commands/sdk/llm.js +48 -0
  50. package/dist/cli/commands/sdk/llm.js.map +1 -0
  51. package/dist/cli/commands/sdk/services/assistants.d.ts +13 -0
  52. package/dist/cli/commands/sdk/services/assistants.d.ts.map +1 -0
  53. package/dist/cli/commands/sdk/services/assistants.js +60 -0
  54. package/dist/cli/commands/sdk/services/assistants.js.map +1 -0
  55. package/dist/cli/commands/sdk/services/categories.d.ts +8 -0
  56. package/dist/cli/commands/sdk/services/categories.d.ts.map +1 -0
  57. package/dist/cli/commands/sdk/services/categories.js +19 -0
  58. package/dist/cli/commands/sdk/services/categories.js.map +1 -0
  59. package/dist/cli/commands/sdk/services/datasources.d.ts +33 -0
  60. package/dist/cli/commands/sdk/services/datasources.d.ts.map +1 -0
  61. package/dist/cli/commands/sdk/services/datasources.js +268 -0
  62. package/dist/cli/commands/sdk/services/datasources.js.map +1 -0
  63. package/dist/cli/commands/sdk/services/index.d.ts +6 -0
  64. package/dist/cli/commands/sdk/services/index.d.ts.map +1 -0
  65. package/dist/cli/commands/sdk/services/index.js +6 -0
  66. package/dist/cli/commands/sdk/services/index.js.map +1 -0
  67. package/dist/cli/commands/sdk/services/integrations.d.ts +27 -0
  68. package/dist/cli/commands/sdk/services/integrations.d.ts.map +1 -0
  69. package/dist/cli/commands/sdk/services/integrations.js +59 -0
  70. package/dist/cli/commands/sdk/services/integrations.js.map +1 -0
  71. package/dist/cli/commands/sdk/services/llm.d.ts +4 -0
  72. package/dist/cli/commands/sdk/services/llm.d.ts.map +1 -0
  73. package/dist/cli/commands/sdk/services/llm.js +7 -0
  74. package/dist/cli/commands/sdk/services/llm.js.map +1 -0
  75. package/dist/cli/commands/sdk/services/skills.d.ts +23 -0
  76. package/dist/cli/commands/sdk/services/skills.d.ts.map +1 -0
  77. package/dist/cli/commands/sdk/services/skills.js +69 -0
  78. package/dist/cli/commands/sdk/services/skills.js.map +1 -0
  79. package/dist/cli/commands/sdk/services/users.d.ts +4 -0
  80. package/dist/cli/commands/sdk/services/users.d.ts.map +1 -0
  81. package/dist/cli/commands/sdk/services/users.js +7 -0
  82. package/dist/cli/commands/sdk/services/users.js.map +1 -0
  83. package/dist/cli/commands/sdk/services/workflows.d.ts +7 -0
  84. package/dist/cli/commands/sdk/services/workflows.d.ts.map +1 -0
  85. package/dist/cli/commands/sdk/services/workflows.js +34 -0
  86. package/dist/cli/commands/sdk/services/workflows.js.map +1 -0
  87. package/dist/cli/commands/sdk/skills.d.ts +3 -0
  88. package/dist/cli/commands/sdk/skills.d.ts.map +1 -0
  89. package/dist/cli/commands/sdk/skills.js +492 -0
  90. package/dist/cli/commands/sdk/skills.js.map +1 -0
  91. package/dist/cli/commands/sdk/users.d.ts +3 -0
  92. package/dist/cli/commands/sdk/users.d.ts.map +1 -0
  93. package/dist/cli/commands/sdk/users.js +81 -0
  94. package/dist/cli/commands/sdk/users.js.map +1 -0
  95. package/dist/cli/commands/sdk/utils/cli-utils.d.ts +35 -0
  96. package/dist/cli/commands/sdk/utils/cli-utils.d.ts.map +1 -0
  97. package/dist/cli/commands/sdk/utils/cli-utils.js +110 -0
  98. package/dist/cli/commands/sdk/utils/cli-utils.js.map +1 -0
  99. package/dist/cli/commands/sdk/utils/datasource-types.d.ts +9 -0
  100. package/dist/cli/commands/sdk/utils/datasource-types.d.ts.map +1 -0
  101. package/dist/cli/commands/sdk/utils/datasource-types.js +61 -0
  102. package/dist/cli/commands/sdk/utils/datasource-types.js.map +1 -0
  103. package/dist/cli/commands/sdk/utils/file-utils.d.ts +8 -0
  104. package/dist/cli/commands/sdk/utils/file-utils.d.ts.map +1 -0
  105. package/dist/cli/commands/sdk/utils/file-utils.js +21 -0
  106. package/dist/cli/commands/sdk/utils/file-utils.js.map +1 -0
  107. package/dist/cli/commands/sdk/utils/render.d.ts +82 -0
  108. package/dist/cli/commands/sdk/utils/render.d.ts.map +1 -0
  109. package/dist/cli/commands/sdk/utils/render.js +149 -0
  110. package/dist/cli/commands/sdk/utils/render.js.map +1 -0
  111. package/dist/cli/commands/sdk/workflows.d.ts +3 -0
  112. package/dist/cli/commands/sdk/workflows.d.ts.map +1 -0
  113. package/dist/cli/commands/sdk/workflows.js +170 -0
  114. package/dist/cli/commands/sdk/workflows.js.map +1 -0
  115. package/dist/cli/index.js +2 -0
  116. package/dist/cli/index.js.map +1 -1
  117. package/package.json +1 -1
@@ -0,0 +1,746 @@
1
+ # People Spending Dashboard — Reference
2
+
3
+ > **Purpose**: This document describes the canonical spending dashboard built for tracking
4
+ > LiteLLM costs for a specific list of people (e.g. a training cohort, project team, or
5
+ > bootcamp). Use it as the authoritative spec whenever someone asks to build a spending
6
+ > report or cost dashboard for a named list of users.
7
+
8
+ ---
9
+
10
+ ## Overview
11
+
12
+ The people spending dashboard is a **self-contained single-file HTML report** that combines
13
+ direct LiteLLM customer costs with CodeMie platform analytics (leaderboard, CLI insights).
14
+ It is dark-themed, uses the CodeMie design system (inlined CSS), and Chart.js for charts.
15
+
16
+ **Key properties:**
17
+ - No external CSS dependencies — all 8 CodeMie CSS files inlined in `<style>`
18
+ - Google Fonts (`Inter`, `JetBrains Mono`) via `@import`
19
+ - Chart.js 4.4.0 via CDN
20
+ - All data pre-fetched at build time and embedded in JS variables — no runtime API calls
21
+ - Three-account LiteLLM model per user (Web, CLI, Premium) — see account scheme below
22
+ - Fully portable: open in any browser on any machine
23
+
24
+ ---
25
+
26
+ ## LiteLLM Account Scheme (3 accounts per user)
27
+
28
+ Each user in the CodeMie platform has up to **three distinct LiteLLM customer accounts**.
29
+ The email address is the base identity:
30
+
31
+ | Account | `end_user_id` pattern | What it tracks |
32
+ |---------|----------------------|----------------|
33
+ | Web / Platform | `first_last@domain.com` | Conversations via the web UI and assistants |
34
+ | CLI | `first_last@domain.com_codemie_cli` | Claude Code / agent CLI sessions |
35
+ | Premium Models | `first_last@domain.com_codemie_premium_models` | Opus / premium model requests |
36
+
37
+ > **Important**: The LiteLLM customer endpoint requires the `end_user_id` query parameter
38
+ > (not `user_id`). The correct endpoint is:
39
+ > ```
40
+ > GET /customer/info?end_user_id=<email_or_email_suffix>
41
+ > ```
42
+
43
+ Each account independently tracks `spend`, `max_budget`, `soft_budget`, and `budget_duration`.
44
+ The total per-user spend is the sum of all three accounts.
45
+
46
+ ---
47
+
48
+ ## Data Sources
49
+
50
+ | Source | How fetched | What it provides |
51
+ |--------|------------|------------------|
52
+ | LiteLLM API | Python `aiohttp` with concurrency (see below) | Actual spend, budget, blocked status per account |
53
+ | CodeMie Leaderboard | `leaderboard --per-page 500` (paginated, all pages) | Tier, champion score, D1–D6 dimensions, rank, usage intent |
54
+ | CodeMie CLI Insights | `cli-insights-users --per-page 500` | CLI sessions, lines added/removed, classification |
55
+ | Source list | Excel/CSV (`.xlsx` via `openpyxl`) | Participant emails |
56
+
57
+ ---
58
+
59
+ ## Step-by-Step Build Process
60
+
61
+ ### Step 1 — Parse the source list
62
+
63
+ The source list is an Excel file with participant emails.
64
+
65
+ ```python
66
+ import openpyxl
67
+
68
+ wb = openpyxl.load_workbook('participants.xlsx')
69
+ ws = wb['participants'] # sheet name may vary
70
+
71
+ emails = []
72
+ for row in ws.iter_rows(min_row=3, values_only=True): # row 1 = headers, row 2 = totals
73
+ if row[0] and '@' in str(row[0]):
74
+ emails.append(str(row[0]).strip().lower())
75
+ ```
76
+
77
+ > **Row layout note**: Row 1 may be column headers, row 2 may be a TOTAL row.
78
+ > Always start from the first data row (check the file structure).
79
+
80
+ ---
81
+
82
+ ### Step 2 — Fetch LiteLLM spend for all 3 accounts per user
83
+
84
+ Use Python `asyncio` + `aiohttp` for concurrent calls. With 656 users × 3 accounts = 1,968
85
+ calls, use a semaphore of 25 to avoid overwhelming the API.
86
+
87
+ ```python
88
+ import asyncio, aiohttp, json, os
89
+
90
+ LITELLM_URL = os.environ['LITELLM_URL'] # e.g. https://litellm.my-company.com
91
+ LITELLM_KEY = os.environ['LITELLM_KEY'] # master key or admin key
92
+
93
+ SUFFIXES = {
94
+ 'web': '', # plain email
95
+ 'cli': '_codemie_cli',
96
+ 'premium': '_codemie_premium_models',
97
+ }
98
+
99
+ async def fetch_customer(session, sem, email, acct_type):
100
+ user_id = email + SUFFIXES[acct_type]
101
+ url = f'{LITELLM_URL}/customer/info'
102
+ headers = {'Authorization': f'Bearer {LITELLM_KEY}'}
103
+ async with sem:
104
+ try:
105
+ async with session.get(url, params={'end_user_id': user_id},
106
+ headers=headers, ssl=False) as resp:
107
+ if resp.status == 200:
108
+ return acct_type, email, await resp.json()
109
+ return acct_type, email, None
110
+ except Exception:
111
+ return acct_type, email, None
112
+
113
+ async def fetch_all(emails):
114
+ sem = asyncio.Semaphore(25)
115
+ connector = aiohttp.TCPConnector(ssl=False)
116
+ results = {}
117
+ async with aiohttp.ClientSession(connector=connector) as session:
118
+ tasks = [
119
+ fetch_customer(session, sem, email, acct)
120
+ for email in emails
121
+ for acct in ('web', 'cli', 'premium')
122
+ ]
123
+ for coro in asyncio.as_completed(tasks):
124
+ acct_type, email, data = await coro
125
+ results.setdefault(email, {})[acct_type] = data
126
+ return results
127
+
128
+ raw = asyncio.run(fetch_all(emails))
129
+ ```
130
+
131
+ **LiteLLM response shape** (one per `end_user_id`):
132
+
133
+ ```json
134
+ {
135
+ "user_id": "first_last@domain.com_codemie_cli",
136
+ "spend": 151.92,
137
+ "max_budget": 150.0,
138
+ "soft_budget": 100.0,
139
+ "budget_duration": "30d",
140
+ "budget_reset_at": "2026-05-01T00:00:00",
141
+ "blocked": false,
142
+ "allowed_model_region": null,
143
+ "allowed_routes": null
144
+ }
145
+ ```
146
+
147
+ ---
148
+
149
+ ### Step 3 — Build the enriched users array
150
+
151
+ ```python
152
+ def extract_budget(acct_data):
153
+ if not acct_data:
154
+ return None
155
+ return {
156
+ 'soft': acct_data.get('soft_budget'),
157
+ 'max': acct_data.get('max_budget'),
158
+ 'duration': acct_data.get('budget_duration'),
159
+ 'blocked': acct_data.get('blocked', False),
160
+ }
161
+
162
+ users = []
163
+ for email in emails:
164
+ r = raw.get(email, {})
165
+ web_d = r.get('web')
166
+ cli_d = r.get('cli')
167
+ prem_d = r.get('premium')
168
+
169
+ web_spend = float(web_d.get('spend', 0)) if web_d else 0.0
170
+ cli_spend = float(cli_d.get('spend', 0)) if cli_d else 0.0
171
+ prem_spend = float(prem_d.get('spend', 0)) if prem_d else 0.0
172
+
173
+ users.append({
174
+ 'email': email,
175
+ 'web_spend': round(web_spend, 4),
176
+ 'cli_spend': round(cli_spend, 4),
177
+ 'premium_spend': round(prem_spend, 4),
178
+ 'total_spend': round(web_spend + cli_spend + prem_spend, 4),
179
+ 'in_litellm': any([web_d, cli_d, prem_d]),
180
+ 'web_budget': extract_budget(web_d),
181
+ 'cli_budget': extract_budget(cli_d),
182
+ 'premium_budget': extract_budget(prem_d),
183
+ })
184
+
185
+ users.sort(key=lambda u: u['total_spend'], reverse=True)
186
+ ```
187
+
188
+ **Save to intermediate file** (`/tmp/users_v2.json`) so you can re-run the HTML build
189
+ without re-fetching the API:
190
+
191
+ ```python
192
+ with open('/tmp/users_v2.json', 'w') as f:
193
+ json.dump(users, f)
194
+ ```
195
+
196
+ ---
197
+
198
+ ### Step 4 — Fetch CodeMie analytics for modal enrichment
199
+
200
+ These calls are optional — they add AI Champion and CLI sections to the per-user modal.
201
+ Run them **after** the LiteLLM fetch to avoid redundant work.
202
+
203
+ #### 4a. Leaderboard (paginated — fetch ALL pages)
204
+
205
+ ```python
206
+ import subprocess
207
+
208
+ all_lb_rows = []
209
+ CLI = os.path.expanduser('~/.claude/skills/codemie-analytics/scripts/analytics-cli.js')
210
+
211
+ for page in range(1, 100): # break on has_more=false
212
+ result = subprocess.run(
213
+ ['node', CLI, 'leaderboard', '--per-page', '500', '--page', str(page), '--output', 'json'],
214
+ capture_output=True, text=True
215
+ )
216
+ data = json.loads(result.stdout)
217
+ all_lb_rows.extend(data['data']['rows'])
218
+ if not data['pagination']['has_more']:
219
+ break
220
+
221
+ lb_by_email = {r['user_email'].lower(): r for r in all_lb_rows if r.get('user_email')}
222
+ ```
223
+
224
+ **Leaderboard row shape:**
225
+
226
+ ```json
227
+ {
228
+ "rank": 491,
229
+ "user_email": "sergiy_example@epam.com",
230
+ "user_name": "Sergiy Example",
231
+ "total_score": 35.49,
232
+ "tier_name": "practitioner",
233
+ "d1_score": 49.7,
234
+ "d2_score": 17.5,
235
+ "d3_score": 0,
236
+ "d4_score": 0,
237
+ "d5_score": 63.8,
238
+ "d6_score": 29.2,
239
+ "usage_intent": "developer",
240
+ "usage_intent_label": "💻 Developer"
241
+ }
242
+ ```
243
+
244
+ > **Scale note**: The leaderboard has ~12,000+ users. With 500 per page, expect ~25 pages
245
+ > (~30–60 seconds total). Typically 60–65% of a 650-person cohort will appear in the
246
+ > leaderboard (users who haven't engaged at all may not have a record).
247
+
248
+ #### 4b. CLI Insights top spenders
249
+
250
+ ```python
251
+ result = subprocess.run(
252
+ ['node', CLI, 'cli-insights-users', '--time-period', 'last_30_days',
253
+ '--per-page', '500', '--output', 'json'],
254
+ capture_output=True, text=True
255
+ )
256
+ cli_data = json.loads(result.stdout)
257
+ cli_rows = cli_data.get('topBySpend', {}).get('data', {}).get('rows', [])
258
+ cli_by_email = {r['user_email'].lower(): r for r in cli_rows if r.get('user_email')}
259
+ ```
260
+
261
+ **CLI Insights row shape (topBySpend):**
262
+
263
+ ```json
264
+ {
265
+ "user_id": "...",
266
+ "user_name": "Danil Melnikov",
267
+ "user_email": "danil_melnikov@epam.com",
268
+ "classification": "production",
269
+ "total_sessions": 134,
270
+ "total_lines_added": 124675,
271
+ "total_lines_removed": 2666,
272
+ "net_lines": 122009,
273
+ "total_cost": 1251.27
274
+ }
275
+ ```
276
+
277
+ > **Coverage note**: `topBySpend` returns only the top 500 CLI users globally. For cohorts
278
+ > of 600+ people, only ~3–5% of your list will appear here. This is expected — use it for
279
+ > the "power CLI users" section of the modal.
280
+
281
+ #### 4c. Build the analytics lookup
282
+
283
+ ```python
284
+ analytics = {}
285
+ for email in [u['email'] for u in users]:
286
+ entry = {}
287
+
288
+ if email in lb_by_email:
289
+ lb = lb_by_email[email]
290
+ entry['lb'] = {
291
+ 'rank': lb.get('rank'),
292
+ 'tier': lb.get('tier_name'),
293
+ 'score': lb.get('total_score'),
294
+ 'intent': lb.get('usage_intent_label'),
295
+ 'd1': lb.get('d1_score'), 'd2': lb.get('d2_score'),
296
+ 'd3': lb.get('d3_score'), 'd4': lb.get('d4_score'),
297
+ 'd5': lb.get('d5_score'), 'd6': lb.get('d6_score'),
298
+ }
299
+
300
+ if email in cli_by_email:
301
+ c = cli_by_email[email]
302
+ entry['cli'] = {
303
+ 'sessions': c.get('total_sessions'),
304
+ 'lines_added': c.get('total_lines_added'),
305
+ 'lines_removed': c.get('total_lines_removed'),
306
+ 'net_lines': c.get('net_lines'),
307
+ 'classification': c.get('classification'),
308
+ 'cost': c.get('total_cost'),
309
+ }
310
+
311
+ if entry:
312
+ analytics[email] = entry
313
+
314
+ with open('/tmp/analytics_enriched.json', 'w') as f:
315
+ json.dump(analytics, f)
316
+ ```
317
+
318
+ ---
319
+
320
+ ### Step 5 — Compute dashboard KPIs
321
+
322
+ ```python
323
+ total_users = len(users)
324
+ active_users = sum(1 for u in users if u['total_spend'] > 0)
325
+ in_litellm = sum(1 for u in users if u['in_litellm'])
326
+ web_total = round(sum(u['web_spend'] for u in users), 2)
327
+ cli_total = round(sum(u['cli_spend'] for u in users), 2)
328
+ premium_total = round(sum(u['premium_spend'] for u in users), 2)
329
+ grand_total = round(sum(u['total_spend'] for u in users), 2)
330
+
331
+ active_spends = [u['total_spend'] for u in users if u['total_spend'] > 0]
332
+ avg_active = round(sum(active_spends) / len(active_spends), 2) if active_spends else 0
333
+
334
+ # Budget projection
335
+ OVERHEAD_FACTOR = 1.20 # 20% buffer
336
+ budget_projection = round(avg_active * total_users * OVERHEAD_FACTOR, 2)
337
+
338
+ stats = {
339
+ 'total_users': total_users,
340
+ 'active_users': active_users,
341
+ 'in_litellm': in_litellm,
342
+ 'web_total': web_total,
343
+ 'cli_total': cli_total,
344
+ 'premium_total': premium_total,
345
+ 'grand_total': grand_total,
346
+ 'avg_active': avg_active,
347
+ 'budget_projection': budget_projection,
348
+ 'overhead_factor': OVERHEAD_FACTOR,
349
+ }
350
+ ```
351
+
352
+ **Budget projection formula:**
353
+
354
+ ```
355
+ Monthly Program Budget = avg_spend_per_active_user × total_program_users × 1.20
356
+ ```
357
+
358
+ The 1.20 factor adds a 20% buffer for usage spikes and new users ramping up.
359
+ Use this in a "Budget Projection" widget, not as a hard cost figure.
360
+
361
+ ---
362
+
363
+ ### Step 6 — Compute spend distribution histogram
364
+
365
+ Used for the "Spend Distribution" bar chart:
366
+
367
+ ```python
368
+ import math
369
+
370
+ def spend_bucket(s):
371
+ if s == 0: return 'No spend'
372
+ if s < 5: return '$0–$5'
373
+ if s < 20: return '$5–$20'
374
+ if s < 50: return '$20–$50'
375
+ if s < 150: return '$50–$150'
376
+ return '$150+'
377
+
378
+ from collections import Counter
379
+ ranges = Counter(spend_bucket(u['total_spend']) for u in users)
380
+ # Ordered for display:
381
+ ordered_ranges = ['No spend','$0–$5','$5–$20','$20–$50','$50–$150','$150+']
382
+ ranges = {k: ranges.get(k, 0) for k in ordered_ranges}
383
+ ```
384
+
385
+ ---
386
+
387
+ ### Step 7 — Generate HTML (use the template file + token replacement)
388
+
389
+ **There is a ready-made template HTML file** shipped with this skill:
390
+ [`references/people-spending-dashboard-template.html`](people-spending-dashboard-template.html)
391
+
392
+ It is a complete dark-themed dashboard with inlined CSS, Chart.js, header, 8 stat cards,
393
+ budget projection card, 3 charts, paginated user table, and user detail modal (with
394
+ AI Champion and CLI sections) — the full structure with all data replaced by `__TOKEN__`
395
+ placeholders.
396
+
397
+ **Token reference** (17 placeholders, grouped by purpose):
398
+
399
+ | Token | Type | What to fill in |
400
+ |-------|------|-----------------|
401
+ | `__DASHBOARD_TITLE__` | string (×2 — `<title>` + `<h1>`) | e.g. `Bootcamp Spending Dashboard`, `Team Alpha Spending` |
402
+ | `__TOTAL_USERS__` | int (×4 — subtitle, stat card, base estimate, caption) | Total rows in the source list |
403
+ | `__REPORT_DATE__` | string (×1 — subtitle) | Generation date, e.g. `April 21, 2026` |
404
+ | `__SUBTITLE_NOTES__` | HTML (×1 — subtitle) | Empty `''` by default, or a note like `&nbsp;&middot;&nbsp; <span style="color:var(--color-warning-text);">Web/Platform: prior-cycle summed in</span>` |
405
+ | `__GRAND_TOTAL__` | string (×1 — stat card) | e.g. `$8,822.48` |
406
+ | `__WEB_TOTAL__` | string (×1 — stat card) | e.g. `$1,731.87` |
407
+ | `__CLI_TOTAL__` | string (×1 — stat card) | e.g. `$7,023.29` |
408
+ | `__PREMIUM_TOTAL__` | string (×1 — stat card) | e.g. `$67.32` |
409
+ | `__IN_LITELLM__` | int (×1 — stat card) | Users with any LiteLLM record |
410
+ | `__ACTIVE_USERS__` | int (×1 — stat card) | Users with `total_spend > 0` |
411
+ | `__AVG_SPEND__` | string (×4 — stat card + projection block) | e.g. `$16.46` — per active user |
412
+ | `__BASE_ESTIMATE__` | string (×1 — projection block) | `$avg × total_users`, e.g. `$10,798` |
413
+ | `__BUFFER_AMOUNT__` | string (×1 — projection block) | +20% of base, e.g. `+$2,160` |
414
+ | `__BUDGET_PROJECTION__` | string (×1 — projection block) | `avg × users × 1.20`, e.g. `$12,957` |
415
+ | `__ALL_USERS_JSON__` | JSON array | Users array (see Step 3) |
416
+ | `__STATS_JSON__` | JSON object | Computed stats (see Step 5) |
417
+ | `__RANGES_JSON__` | JSON object | Spend bucket counts (see Step 6) |
418
+ | `__ANALYTICS_JSON__` | JSON object | Per-email leaderboard + CLI data (see Step 4c) |
419
+
420
+ **When embedding data in HTML with Python, AVOID f-strings for the script block.**
421
+ JavaScript uses `${...}` template literals, which conflict with Python f-string syntax.
422
+ Use plain string concatenation or `str.replace()` with named tokens (as below):
423
+
424
+ ```python
425
+ import json, shutil
426
+
427
+ # 1. Copy template to output location
428
+ template_path = '~/.claude/skills/codemie-analytics/references/people-spending-dashboard-template.html'
429
+ output_path = 'reports/my-spending-dashboard.html'
430
+ shutil.copy(os.path.expanduser(template_path), output_path)
431
+
432
+ with open(output_path) as f:
433
+ html = f.read()
434
+
435
+ # 2. JSON blobs
436
+ html = html.replace('__ALL_USERS_JSON__', json.dumps(users, separators=(',', ':')))
437
+ html = html.replace('__STATS_JSON__', json.dumps(stats, separators=(',', ':')))
438
+ html = html.replace('__RANGES_JSON__', json.dumps(ranges, separators=(',', ':')))
439
+ html = html.replace('__ANALYTICS_JSON__', json.dumps(analytics, separators=(',', ':')))
440
+
441
+ # 3. Header / title
442
+ html = html.replace('__DASHBOARD_TITLE__', 'Bootcamp Spending Dashboard')
443
+ html = html.replace('__TOTAL_USERS__', str(stats['total_users']))
444
+ html = html.replace('__REPORT_DATE__', 'April 21, 2026')
445
+ html = html.replace('__SUBTITLE_NOTES__', '') # or methodology note if applicable
446
+
447
+ # 4. Stat card values (currency strings pre-formatted with commas)
448
+ html = html.replace('__GRAND_TOTAL__', f"${stats['grand_total']:,.2f}")
449
+ html = html.replace('__WEB_TOTAL__', f"${stats['web_total']:,.2f}")
450
+ html = html.replace('__CLI_TOTAL__', f"${stats['cli_total']:,.2f}")
451
+ html = html.replace('__PREMIUM_TOTAL__', f"${stats['premium_total']:,.2f}")
452
+ html = html.replace('__IN_LITELLM__', str(stats['in_litellm']))
453
+ html = html.replace('__ACTIVE_USERS__', str(stats['active_users']))
454
+ html = html.replace('__AVG_SPEND__', f"${stats['avg_active']:.2f}")
455
+
456
+ # 5. Budget projection block
457
+ base = round(stats['avg_active'] * stats['total_users'])
458
+ buff = round(base * 0.20)
459
+ html = html.replace('__BASE_ESTIMATE__', f'${base:,}')
460
+ html = html.replace('__BUFFER_AMOUNT__', f'+${buff:,}')
461
+ html = html.replace('__BUDGET_PROJECTION__', f"${stats['budget_projection']:,.0f}")
462
+
463
+ with open(output_path, 'w') as f:
464
+ f.write(html)
465
+ ```
466
+
467
+ **Important — leftover tokens check:** after all replacements run,
468
+ `grep -c '__[A-Z_]\+__' output.html` should return **0**. Any remaining `__TOKEN__`
469
+ means a data field was missed.
470
+
471
+ ---
472
+
473
+ ## Page Structure
474
+
475
+ ```
476
+ <body class="p-6">
477
+ <div class="container">
478
+
479
+ [Page header]
480
+ h1 "Bootcamp Spending Dashboard"
481
+ p.text-muted "Generated: <date> · Source: LiteLLM"
482
+
483
+ [Top KPI row — 4 stat cards]
484
+ Grand Total Spend (blue)
485
+ Web / Platform (green)
486
+ CLI (purple)
487
+ Premium Models (orange)
488
+
489
+ [Second KPI row — 4 stat cards]
490
+ Participants (total list size)
491
+ In LiteLLM (users found in API)
492
+ Active Spenders (spend > 0)
493
+ Avg Total Spend (per active user)
494
+
495
+ [Budget Projection card — info border]
496
+ Large $XX,XXX/month figure
497
+ Formula: avg_active × total_users × 1.20
498
+
499
+ [Charts row — side by side, align-items: start]
500
+ .card Spend Distribution (vertical bar, height 250px)
501
+ .card Top 10 by Total Spend (horizontal stacked bar, height 460px)
502
+
503
+ [Spend Breakdown by Type — full width bar chart, height 240px]
504
+ Stacked: web + cli + premium per user (top 20)
505
+
506
+ [User Table card]
507
+ Search input + sortable columns
508
+ Columns: User · Web · CLI · Premium · Total · Status
509
+ Each row is clickable → opens detail modal
510
+ Pagination (20 per page)
511
+
512
+ [User Detail Modal]
513
+ (see modal section below)
514
+
515
+ </div>
516
+ </body>
517
+ ```
518
+
519
+ ---
520
+
521
+ ## Components Used (CodeMie Design System)
522
+
523
+ | Component | Class(es) | Usage |
524
+ |-----------|-----------|-------|
525
+ | Stat cards | `.stat-grid` / `.stat-card` | KPI tiles at top |
526
+ | Card | `.card` / `.card-header` / `.card-body` | All sections |
527
+ | Table | `.table-wrapper` / `.table` | User list |
528
+ | Badge | `.badge .badge-success/error/warning/...` | Status, tier, classification |
529
+ | Alert | `.alert .alert-info` | Budget projection highlight |
530
+ | Progress bar | `.progress-bar-wrap` / `.progress-bar-fill` | Budget usage bars in modal |
531
+ | DL grid | `.dl-grid` | Key-value detail rows in modal |
532
+ | Pagination | `.pagination` / `.page-btn` | Table pagination |
533
+
534
+ ---
535
+
536
+ ## Charts
537
+
538
+ ### 1. Spend Distribution (vertical bar)
539
+ - **Type**: `bar` (vertical, `indexAxis: 'x'`)
540
+ - **Data**: Count of users per spend bucket: `No spend`, `$0–$5`, `$5–$20`, `$20–$50`, `$50–$150`, `$150+`
541
+ - **Colour**: Single colour `#2297F6` (primary blue)
542
+ - **Canvas**: `id="rangeChart"`, `height: 250px`
543
+
544
+ ### 2. Top 10 by Total Spend (horizontal stacked bar)
545
+ - **Type**: `bar`, `indexAxis: 'y'`
546
+ - **Data**: Top 10 users' web + cli + premium spend, stacked
547
+ - **Colours**: Web `#259F4C` (green) · CLI `#C084FC` (purple) · Premium `#F5A534` (orange)
548
+ - **Bar thickness**: `barThickness: 28`
549
+ - **Canvas**: `id="topChart"`, `height: 460px`
550
+ - **Key CSS**: The container grid must use `align-items: start` so charts can have independent heights without stretching to match their neighbour.
551
+
552
+ ```html
553
+ <div class="charts-row" style="align-items: start;">
554
+ <div class="card">...<canvas id="rangeChart" style="height:250px">...</div>
555
+ <div class="card">...<canvas id="topChart" style="height:460px">...</div>
556
+ </div>
557
+ ```
558
+
559
+ ### 3. Spend Breakdown by Type (horizontal stacked bar, full width)
560
+ - **Type**: `bar`, `indexAxis: 'y'`
561
+ - **Data**: Top 20 users by total spend, each bar split into web + cli + premium
562
+ - **Canvas**: `id="breakdownChart"`, `height: 240px`
563
+ - **This chart is separate from the Top 10** — it is full-width, below the side-by-side pair.
564
+
565
+ ---
566
+
567
+ ## User Detail Modal
568
+
569
+ Opens when any table row is clicked. Closes on Escape, close button, or backdrop click.
570
+
571
+ **Layout** (top to bottom):
572
+
573
+ ```
574
+ [Total spend] — Large blue number ($XX.XX)
575
+ [Status badge] — "Active" (green) or "No spend" (gray) + "Combined LiteLLM spend" label
576
+
577
+ [Spend breakdown row] — 3-column grid: Web · CLI · Premium (coloured values)
578
+
579
+ [Budget Details] — Per-account budget blocks (Web, CLI, Premium)
580
+ Each block shows:
581
+ Spend · Soft limit · Hard limit · Cycle duration
582
+ Budget used % progress bar (green/orange/red)
583
+
584
+ [AI Champion Profile] — Only if user appears in the leaderboard (~62% of users)
585
+ Tier badge + usage intent label
586
+ Score (large number) / 100 + Rank #N
587
+ D1–D6 dimension bars (coloured horizontal bars, 0–100 scale)
588
+
589
+ [CLI Activity] — Only if user is in top 500 CLI spenders globally
590
+ Classification badge
591
+ Sessions / Lines Added (+green) / Lines Removed (-red) / Net lines
592
+ ```
593
+
594
+ **Modal max-width**: `580px`. Set `max-height: 90vh; overflow-y: auto` so long modals scroll.
595
+
596
+ ### Tier → badge class mapping
597
+
598
+ | Tier | Badge class | Emoji |
599
+ |------|-------------|-------|
600
+ | pioneer | `badge-advanced` | 🏆 |
601
+ | expert | `badge-in-progress` | ⭐ |
602
+ | advanced | `badge-success` | 🔥 |
603
+ | practitioner | `badge-warning` | 📈 |
604
+ | newcomer | `badge-not-started` | 🌱 |
605
+
606
+ ### D1–D6 dimension bar colours
607
+
608
+ | Dimension | Label | Weight | Colour |
609
+ |-----------|-------|--------|--------|
610
+ | D1 | Platform Usage | 20% | `#2297F6` (blue) |
611
+ | D2 | Platform Creation | 20% | `#C084FC` (purple) |
612
+ | D3 | Workflow Usage | 10% | `#259F4C` (green) |
613
+ | D4 | Workflow Creation | 10% | `#F5A534` (orange) |
614
+ | D5 | CLI & Agentic | 30% | `#06B6D4` (cyan) |
615
+ | D6 | Impact & Knowledge | 10% | `#F9303C` (red) |
616
+
617
+ ### Table click — use event delegation (NOT onclick attributes)
618
+
619
+ ```javascript
620
+ // CORRECT — works for dynamically rendered rows
621
+ document.getElementById('tableBody').addEventListener('click', function(e) {
622
+ const row = e.target.closest('tr[data-email]');
623
+ if (row) openModal(row.getAttribute('data-email'));
624
+ });
625
+
626
+ // Table row HTML
627
+ `<tr data-email="${encodeURIComponent(user.email)}">...</tr>`
628
+
629
+ // In openModal:
630
+ function openModal(enc) {
631
+ const email = decodeURIComponent(enc);
632
+ const u = ALL_USERS.find(x => x.email === email);
633
+ const an = ANALYTICS_DATA[email] || {};
634
+ ...
635
+ }
636
+ ```
637
+
638
+ > **Why not onclick=""?** Python string templating mangles single-quote escaping inside
639
+ > HTML attribute strings, producing broken JS like `openModal('' + encodeURIComponent(...))`.
640
+ > `data-*` attributes + event delegation is immune to this quoting problem.
641
+
642
+ ---
643
+
644
+ ## JavaScript Variables Embedded in HTML
645
+
646
+ | Variable | Type | Source | Size (approx) |
647
+ |----------|------|--------|---------------|
648
+ | `ALL_USERS` | `Array<UserObject>` | LiteLLM `/customer/info` | ~200–400 KB for 600+ users |
649
+ | `STATS` | `Object` | Computed from ALL_USERS | < 1 KB |
650
+ | `RANGES` | `Object` | Spend bucket counts | < 1 KB |
651
+ | `ANALYTICS_DATA` | `Object<email, AnalyticsEntry>` | Leaderboard + CLI insights | ~60–100 KB for 400+ matches |
652
+
653
+ `ANALYTICS_DATA` is keyed by lowercase email. Entries are sparse — only users found in at
654
+ least one analytics source have an entry. Always default to `{}` on lookup:
655
+
656
+ ```javascript
657
+ const an = ANALYTICS_DATA[email] || {};
658
+ const lb = an.lb || null; // leaderboard data (may be null)
659
+ const cli = an.cli || null; // CLI insights data (may be null)
660
+ ```
661
+
662
+ ---
663
+
664
+ ## Status / Budget Colour Logic
665
+
666
+ ```javascript
667
+ function budgetClass(pct) {
668
+ if (pct >= 100) return 'fill-over'; // red
669
+ if (pct >= 75) return 'fill-warn'; // orange
670
+ return 'fill-ok'; // green
671
+ }
672
+ // pct = (spend / soft_budget) * 100, capped at 100
673
+ ```
674
+
675
+ Row-level status badge in the table:
676
+
677
+ | Condition | Badge |
678
+ |-----------|-------|
679
+ | `total_spend === 0` | `badge-not-started` — "No spend" |
680
+ | `total_spend > 0` | `badge-success` — "Active" (with dot) |
681
+
682
+ ---
683
+
684
+ ## Spend Table — Columns and Sorting
685
+
686
+ | Column | Data field | Alignment | Default sort |
687
+ |--------|-----------|-----------|-------------|
688
+ | User (email prefix) | `email` | left | — |
689
+ | Web | `web_spend` | right (`td-number`) | — |
690
+ | CLI | `cli_spend` | right | — |
691
+ | Premium | `premium_spend` | right | — |
692
+ | Total | `total_spend` | right | **desc** (default) |
693
+ | Status | derived from `total_spend` | center | — |
694
+
695
+ Clicking a column header toggles ascending/descending sort. Active column shows
696
+ a caret (`▲`/`▼`). Search filters on email prefix (case-insensitive).
697
+
698
+ Pagination: 20 rows per page. Show "Showing X–Y of Z" counter and page buttons.
699
+
700
+ ---
701
+
702
+ ## File Location Convention
703
+
704
+ ```
705
+ reports/
706
+ ├── bootcamp-spending-dashboard.html ← cohort spending dashboard
707
+ ├── spending-<team>-<YYYY-MM>.html ← team/project variant
708
+ └── spending-<project>-snapshot.html ← snapshot for a specific project
709
+ ```
710
+
711
+ ---
712
+
713
+ ## Replication Checklist
714
+
715
+ When building a new people spending dashboard:
716
+
717
+ 1. **Parse the list** — extract emails from Excel/CSV (check header/total rows).
718
+ 2. **Fetch LiteLLM** — use Python asyncio + aiohttp, semaphore(25), `ssl=False`,
719
+ `end_user_id` param. Fetch 3 accounts per user (web, cli, premium suffixes).
720
+ 3. **Save raw results** to `/tmp/litellm_raw.json` — avoids re-fetching on HTML rebuild.
721
+ 4. **Build users array** — sum the three spend values, extract budget fields.
722
+ 5. **Fetch leaderboard** — paginate all pages (`--per-page 500`), build email lookup.
723
+ 6. **Fetch CLI insights** — `cli-insights-users --per-page 500`, build email lookup.
724
+ 7. **Build analytics dict** — merge leaderboard + CLI into `ANALYTICS_DATA[email]`.
725
+ 8. **Compute KPIs** — totals, averages, spend distribution, budget projection (avg × N × 1.20).
726
+ 9. **Inline CSS** — read all 8 CodeMie CSS files, concatenate into `<style>`.
727
+ 10. **Use token-safe templating** — `str.replace()` with `__TOKEN__` markers, not f-strings.
728
+ 11. **Wire table clicks** — `data-email` attribute + event delegation (no onclick= attributes).
729
+ 12. **Verify** — open in browser, click rows, confirm modal shows spend + analytics sections.
730
+ 13. **Save** to `reports/<descriptive-name>.html`.
731
+
732
+ ---
733
+
734
+ ## Tips and Gotchas
735
+
736
+ | Issue | Cause | Fix |
737
+ |-------|-------|-----|
738
+ | 422 from LiteLLM | Using `user_id` param | Use `end_user_id` param |
739
+ | JS `onclick` broken | Python quote escaping in template strings | Use `data-email` + event delegation |
740
+ | Top 10 chart labels cut off | CSS Grid `align-items: stretch` makes both cards same height | Add `align-items: start` to `.charts-row` container |
741
+ | `KeyError: 'user_email'` in CLI spend | CLI rows use `user_name` key (not `user_email`) | Use correct key per data source |
742
+ | Only ~60% of users in leaderboard | Users with zero activity have no record | Expected — show analytics section only if `an.lb !== null` |
743
+ | CLI insights coverage only ~3% of cohort | `topBySpend` returns top 500 globally; small cohort ranks low | Expected — only show CLI section when data exists |
744
+ | Large HTML file (300+ KB) | ALL_USERS JSON for 600+ users is large | Acceptable for offline report; no performance issue in browser |
745
+ | Python f-string vs JS template literal conflict | `${...}` in JS conflicts with Python f-string syntax | Use plain string `TEMPLATE` with `__TOKEN__` replace() |
746
+ | Refreshed dashboard shows stale top-card values | Original template hardcoded stat-card values into HTML (not reading from `STATS` JS var) | On a refresh, patch BOTH the JS vars AND the hardcoded HTML. Hardcoded spots to update: `<span class="stat-card-value">$X</span>` (×8 cards), budget projection block (`$12,713` figure + base estimate + "+20% buffer" line + "avg $X × 656 × 1.20" caption). Also the `<h1>` date text. |