cli-consumption 0.4.0__tar.gz → 0.4.2__tar.gz

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 (66) hide show
  1. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/CHANGELOG.md +24 -1
  2. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/PKG-INFO +1 -1
  3. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/pyproject.toml +1 -1
  4. cli_consumption-0.4.2/src/cli_consumption/INTER_FONT_LICENSE.txt +93 -0
  5. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/api.py +23 -3
  6. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/cli.py +16 -2
  7. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/dashboard.py +37 -1
  8. cli_consumption-0.4.2/src/cli_consumption/dashboard_layouts.py +345 -0
  9. cli_consumption-0.4.2/src/cli_consumption/dashboard_react.css +2 -0
  10. cli_consumption-0.4.2/src/cli_consumption/dashboard_react.js +9 -0
  11. cli_consumption-0.4.2/src/cli_consumption/migrations/versions/v0006_dashboard_layouts.py +28 -0
  12. cli_consumption-0.4.2/src/cli_consumption/migrations/versions/v0007_dashboard_layout_revision.py +39 -0
  13. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/reporting_api.py +117 -18
  14. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/schema.py +83 -8
  15. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/storage.py +18 -0
  16. cli_consumption-0.4.0/src/cli_consumption/dashboard_react.css +0 -2
  17. cli_consumption-0.4.0/src/cli_consumption/dashboard_react.js +0 -9
  18. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/.gitignore +0 -0
  19. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/LICENSE +0 -0
  20. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/NOTICE +0 -0
  21. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/README.md +0 -0
  22. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/__init__.py +0 -0
  23. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/__main__.py +0 -0
  24. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/__init__.py +0 -0
  25. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/_shared.py +0 -0
  26. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/aider.py +0 -0
  27. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/amazon_q.py +0 -0
  28. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/amp.py +0 -0
  29. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/base.py +0 -0
  30. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/claude.py +0 -0
  31. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/cline.py +0 -0
  32. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/codex.py +0 -0
  33. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/continue_cli.py +0 -0
  34. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/copilot.py +0 -0
  35. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/crush.py +0 -0
  36. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/cursor.py +0 -0
  37. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/gemini.py +0 -0
  38. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/goose.py +0 -0
  39. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/grok.py +0 -0
  40. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/kilo.py +0 -0
  41. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/kimi.py +0 -0
  42. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/mistral_vibe.py +0 -0
  43. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/opencode.py +0 -0
  44. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/openhands.py +0 -0
  45. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/pi.py +0 -0
  46. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/plandex.py +0 -0
  47. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/qwen.py +0 -0
  48. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/registry.py +0 -0
  49. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/exporting.py +0 -0
  50. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/migrations/__init__.py +0 -0
  51. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/migrations/env.py +0 -0
  52. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/migrations/versions/__init__.py +0 -0
  53. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/migrations/versions/v0001_baseline.py +0 -0
  54. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/migrations/versions/v0002_minimize_subagents.py +0 -0
  55. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/migrations/versions/v0003_canonical_timestamps.py +0 -0
  56. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/migrations/versions/v0004_subagent_scope_freshness.py +0 -0
  57. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/migrations/versions/v0005_sync_receipts.py +0 -0
  58. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/models.py +0 -0
  59. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/py.typed +0 -0
  60. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/qualifications.py +0 -0
  61. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/reporting.py +0 -0
  62. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/retention.py +0 -0
  63. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/snapshot_extraction.py +0 -0
  64. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/snapshot_files.py +0 -0
  65. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/sync.py +0 -0
  66. {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/timestamps.py +0 -0
@@ -6,6 +6,27 @@ use [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.2] - 2026-09-05
10
+
11
+ ### Added
12
+
13
+ - Added reviewed autonomous web exports that freeze the visible dashboard layout,
14
+ bounded selection, privacy profile, and theme into a CSP-locked standalone file while
15
+ preserving query-only API and CLI export compatibility.
16
+ - Added the provider-neutral `DashboardLayout v1` contract, a bounded widget registry,
17
+ deterministic defaults and retired-widget recovery, plus SQLite/PostgreSQL
18
+ persistence for the single authenticated dashboard operator.
19
+ - Added an explicit, accessible dashboard edit mode with bounded undo/redo, deterministic
20
+ placement, and ETag/`If-Match` conflict protection for concurrent save and reset.
21
+
22
+ ## [0.4.1] - 2026-09-01
23
+
24
+ ### Changed
25
+
26
+ - Refined the persistent and offline dashboards with a compact side-by-side title
27
+ treatment, borderless page-colored cards, quieter spacing, and fewer decorative
28
+ effects across desktop and mobile layouts.
29
+
9
30
  ## [0.4.0] - 2026-09-01
10
31
 
11
32
  ### Added
@@ -164,7 +185,9 @@ use [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
164
185
 
165
186
  - Refreshed the provider guide for the first minor release ([#26]).
166
187
 
167
- [Unreleased]: https://github.com/Guillaume-Lombardo/cli-consumption/compare/v0.4.0...HEAD
188
+ [Unreleased]: https://github.com/Guillaume-Lombardo/cli-consumption/compare/v0.4.2...HEAD
189
+ [0.4.2]: https://github.com/Guillaume-Lombardo/cli-consumption/compare/v0.4.1...v0.4.2
190
+ [0.4.1]: https://github.com/Guillaume-Lombardo/cli-consumption/compare/v0.4.0...v0.4.1
168
191
  [0.4.0]: https://github.com/Guillaume-Lombardo/cli-consumption/compare/v0.3.3...v0.4.0
169
192
  [0.3.3]: https://github.com/Guillaume-Lombardo/cli-consumption/compare/v0.3.2...v0.3.3
170
193
  [0.3.2]: https://github.com/Guillaume-Lombardo/cli-consumption/compare/v0.3.1...v0.3.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cli-consumption
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Analyze and consolidate AI coding CLI consumption across machines.
5
5
  Project-URL: Homepage, https://github.com/Guillaume-Lombardo/cli-consumption
6
6
  Project-URL: Documentation, https://github.com/Guillaume-Lombardo/cli-consumption#readme
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cli-consumption"
7
- version = "0.4.0"
7
+ version = "0.4.2"
8
8
  description = "Analyze and consolidate AI coding CLI consumption across machines."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,93 @@
1
+ Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -19,6 +19,10 @@ from sqlalchemy.engine import Engine
19
19
  from starlette.types import ASGIApp, Message, Receive, Scope, Send
20
20
 
21
21
  from cli_consumption import __version__
22
+ from cli_consumption.dashboard_layouts import (
23
+ DASHBOARD_LAYOUT_VERSION,
24
+ MAX_DASHBOARD_LAYOUT_BYTES,
25
+ )
22
26
  from cli_consumption.models import (
23
27
  CURRENT_SNAPSHOT_SCHEMA,
24
28
  MAX_SNAPSHOT_RECORDS,
@@ -29,6 +33,7 @@ from cli_consumption.models import (
29
33
  )
30
34
  from cli_consumption.reporting_api import (
31
35
  CACHE_HEADERS,
36
+ EXPORT_REQUEST_BYTES,
32
37
  EXPORT_TIMEOUT_SECONDS,
33
38
  MAX_CONCURRENT_EXPORTS,
34
39
  MAX_CONCURRENT_REPORTS,
@@ -69,6 +74,7 @@ READINESS_TABLES = (
69
74
  "subagent_scopes",
70
75
  "ingestion_runs",
71
76
  "sync_receipts",
77
+ "dashboard_layouts",
72
78
  )
73
79
  READINESS_RESPONSE_TIMEOUT_SECONDS = 2.0
74
80
  READINESS_CONNECT_TIMEOUT_SECONDS = 2
@@ -94,6 +100,7 @@ SAFE_ROUTES = frozenset(
94
100
  "/api/v1/reporting/conversations",
95
101
  "/api/v1/reporting/conversation",
96
102
  "/api/v1/reporting/export",
103
+ "/api/v1/reporting/layout",
97
104
  }
98
105
  )
99
106
  SAFE_EXCEPTION_TYPES = frozenset(
@@ -354,9 +361,12 @@ class RequestSizeLimitMiddleware:
354
361
  if scope["type"] != "http":
355
362
  await self.app(scope, receive, send)
356
363
  return
364
+ path = str(scope.get("path", ""))
357
365
  maximum = (
358
- REPORTING_REQUEST_BYTES
359
- if str(scope.get("path", "")).startswith("/api/v1/reporting/")
366
+ EXPORT_REQUEST_BYTES
367
+ if path == "/api/v1/reporting/export"
368
+ else REPORTING_REQUEST_BYTES
369
+ if path.startswith("/api/v1/reporting/")
360
370
  else self.maximum
361
371
  )
362
372
  headers = dict(scope.get("headers", []))
@@ -405,6 +415,7 @@ def _configured_credentials(
405
415
  api_token: str | None,
406
416
  read_token: str | None,
407
417
  export_token: str | None,
418
+ layout_token: str | None,
408
419
  ) -> list[tuple[str, frozenset[str]]]:
409
420
  credentials: list[tuple[str, frozenset[str]]] = []
410
421
  configured_values: set[str] = set()
@@ -412,6 +423,7 @@ def _configured_credentials(
412
423
  (api_token, frozenset({"ingest"})),
413
424
  (read_token, frozenset({"read"})),
414
425
  (export_token, frozenset({"read", "export"})),
426
+ (layout_token, frozenset({"layout"})),
415
427
  ):
416
428
  if credential is None:
417
429
  continue
@@ -436,8 +448,11 @@ def create_app(
436
448
  *,
437
449
  read_token: str | None = None,
438
450
  export_token: str | None = None,
451
+ layout_token: str | None = None,
439
452
  ) -> SafeExceptionBoundary:
440
- credentials = _configured_credentials(api_token, read_token, export_token)
453
+ credentials = _configured_credentials(
454
+ api_token, read_token, export_token, layout_token
455
+ )
441
456
  initialize_database(engine)
442
457
  if engine.dialect.name == "postgresql":
443
458
  probe_engine = create_postgresql_readiness_engine(
@@ -553,8 +568,12 @@ def create_app(
553
568
  "idempotent_snapshot_uploads": True,
554
569
  "dashboard_query_versions": [1],
555
570
  "dashboard_dataset_versions": [1],
571
+ "dashboard_layout_versions": [DASHBOARD_LAYOUT_VERSION],
572
+ "dashboard_layout_mutation_scope": "layout",
573
+ "max_dashboard_layout_bytes": MAX_DASHBOARD_LAYOUT_BYTES,
556
574
  "cursor_versions": [1],
557
575
  "max_reporting_request_bytes": REPORTING_REQUEST_BYTES,
576
+ "max_export_request_bytes": EXPORT_REQUEST_BYTES,
558
577
  "max_reporting_filter_values": MAX_FILTER_VALUES,
559
578
  "max_reporting_records": MAX_REPORTING_RECORDS,
560
579
  "max_reporting_scalar_bytes": MAX_REPORTING_SCALAR_BYTES,
@@ -599,6 +618,7 @@ def create_app(
599
618
  engine,
600
619
  authorize_read=require_scopes("read"),
601
620
  authorize_export=require_scopes("read", "export"),
621
+ authorize_layout=require_scopes("layout"),
602
622
  )
603
623
 
604
624
  return SafeExceptionBoundary(app)
@@ -957,6 +957,12 @@ def serve(
957
957
  help="Environment variable containing the reporting export bearer token."
958
958
  ),
959
959
  ] = "CLI_CONSUMPTION_EXPORT_TOKEN",
960
+ layout_token_env: Annotated[
961
+ str,
962
+ typer.Option(
963
+ help="Environment variable containing the dashboard layout mutation token."
964
+ ),
965
+ ] = "CLI_CONSUMPTION_LAYOUT_TOKEN",
960
966
  ) -> None:
961
967
  """Run the optional central HTTP collector."""
962
968
  try:
@@ -971,6 +977,7 @@ def serve(
971
977
  token = os.environ.get(token_env)
972
978
  read_token = os.environ.get(read_token_env)
973
979
  export_token = os.environ.get(export_token_env)
980
+ layout_token = os.environ.get(layout_token_env) or None
974
981
  if any(value == "" for value in (token, read_token, export_token)):
975
982
  raise typer.BadParameter(
976
983
  "Configured token environment variables must be non-empty."
@@ -979,13 +986,19 @@ def serve(
979
986
  token is None
980
987
  and read_token is None
981
988
  and export_token is None
989
+ and layout_token is None
982
990
  and host not in {"127.0.0.1", "localhost", "::1"}
983
991
  ):
984
992
  raise typer.BadParameter(
985
993
  "Set at least one configured token environment variable before exposing "
986
994
  "the service beyond localhost."
987
995
  )
988
- if token is None and read_token is None and export_token is None:
996
+ if (
997
+ token is None
998
+ and read_token is None
999
+ and export_token is None
1000
+ and layout_token is None
1001
+ ):
989
1002
  typer.echo(
990
1003
  "Warning: service authentication is disabled on localhost.", err=True
991
1004
  )
@@ -993,12 +1006,13 @@ def serve(
993
1006
  try:
994
1007
  application = (
995
1008
  create_app(engine, token)
996
- if read_token is None and export_token is None
1009
+ if read_token is None and export_token is None and layout_token is None
997
1010
  else create_app(
998
1011
  engine,
999
1012
  token,
1000
1013
  read_token=read_token,
1001
1014
  export_token=export_token,
1015
+ layout_token=layout_token,
1002
1016
  )
1003
1017
  )
1004
1018
  uvicorn.run(application, host=host, port=port, access_log=False)
@@ -11,14 +11,20 @@ from contextlib import contextmanager
11
11
  from dataclasses import dataclass
12
12
  from datetime import UTC, datetime
13
13
  from functools import cache
14
+ from html import escape
14
15
  from importlib.resources import files
15
16
  from pathlib import Path
16
- from typing import Any, TextIO
17
+ from typing import Any, Literal, TextIO
17
18
 
18
19
  from sqlalchemy import select
19
20
  from sqlalchemy.engine import Connection, Engine
20
21
 
21
22
  from cli_consumption.adapters.registry import ADAPTER_SPECS
23
+ from cli_consumption.dashboard_layouts import (
24
+ DEFAULT_DASHBOARD_LAYOUT_V1,
25
+ DashboardLayoutV1,
26
+ revalidate_dashboard_layout,
27
+ )
22
28
  from cli_consumption.reporting import (
23
29
  ExportWindow,
24
30
  ReportEstimate,
@@ -87,6 +93,19 @@ def _react_dashboard_styles() -> str:
87
93
  )
88
94
 
89
95
 
96
+ @cache
97
+ def _inter_font_license_notice() -> str:
98
+ """Load and HTML-escape the bundled Inter OFL notice without network URLs."""
99
+ license_text = (
100
+ files("cli_consumption")
101
+ .joinpath("INTER_FONT_LICENSE.txt")
102
+ .read_text(encoding="utf-8")
103
+ )
104
+ return escape(
105
+ license_text.replace("https://", "").replace("http://", ""), quote=False
106
+ )
107
+
108
+
90
109
  class DashboardLimitError(RuntimeError):
91
110
  """A privacy-safe dashboard generation limit failure."""
92
111
 
@@ -99,7 +118,14 @@ def generate_dashboard(
99
118
  window: ExportWindow | None = None,
100
119
  filters: ReportFilters | None = None,
101
120
  timeout_seconds: float | None = None,
121
+ layout: DashboardLayoutV1 | None = None,
122
+ theme: Literal["dark", "light"] | None = None,
102
123
  ) -> None:
124
+ if theme not in (None, "dark", "light"):
125
+ raise ValueError("invalid_dashboard_theme")
126
+ validated_layout = revalidate_dashboard_layout(
127
+ DEFAULT_DASHBOARD_LAYOUT_V1 if layout is None else layout
128
+ )
103
129
  initialize_database(engine)
104
130
  with _dashboard_snapshot(engine, timeout_seconds=timeout_seconds) as connection:
105
131
  _enforce_estimate(_estimate_selection(connection, window, filters))
@@ -115,6 +141,8 @@ def generate_dashboard(
115
141
  handle,
116
142
  connection,
117
143
  context,
144
+ validated_layout,
145
+ theme,
118
146
  ),
119
147
  )
120
148
 
@@ -552,6 +580,8 @@ def _stream_dashboard(
552
580
  handle: TextIO,
553
581
  connection: Connection,
554
582
  context: _DashboardContext,
583
+ layout: DashboardLayoutV1,
584
+ theme: Literal["dark", "light"] | None,
555
585
  ) -> None:
556
586
  prefix, suffix = _react_document_parts()
557
587
  writer = _BudgetedWriter(handle)
@@ -574,6 +604,10 @@ def _stream_dashboard(
574
604
  first = False
575
605
  writer.write("]")
576
606
  writer.write("}")
607
+ writer.write(";globalThis.__CLI_CONSUMPTION_LAYOUT__=")
608
+ writer.write(_encode_json(layout.model_dump(mode="json")))
609
+ writer.write(";globalThis.__CLI_CONSUMPTION_THEME__=")
610
+ writer.write(_encode_json(theme))
577
611
  writer.write(suffix)
578
612
 
579
613
 
@@ -584,7 +618,9 @@ def _react_document_parts() -> tuple[str, str]:
584
618
  <head>
585
619
  <meta charset="utf-8">
586
620
  <meta name="viewport" content="width=device-width,initial-scale=1">
621
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; font-src data:; img-src data:; connect-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'">
587
622
  <title>CLI Consumption</title>
623
+ <script id="inter-font-license" type="text/plain">{_inter_font_license_notice()}</script>
588
624
  <style>{_react_dashboard_styles()}</style>
589
625
  </head>
590
626
  <body>