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.
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/CHANGELOG.md +24 -1
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/PKG-INFO +1 -1
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/pyproject.toml +1 -1
- cli_consumption-0.4.2/src/cli_consumption/INTER_FONT_LICENSE.txt +93 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/api.py +23 -3
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/cli.py +16 -2
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/dashboard.py +37 -1
- cli_consumption-0.4.2/src/cli_consumption/dashboard_layouts.py +345 -0
- cli_consumption-0.4.2/src/cli_consumption/dashboard_react.css +2 -0
- cli_consumption-0.4.2/src/cli_consumption/dashboard_react.js +9 -0
- cli_consumption-0.4.2/src/cli_consumption/migrations/versions/v0006_dashboard_layouts.py +28 -0
- cli_consumption-0.4.2/src/cli_consumption/migrations/versions/v0007_dashboard_layout_revision.py +39 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/reporting_api.py +117 -18
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/schema.py +83 -8
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/storage.py +18 -0
- cli_consumption-0.4.0/src/cli_consumption/dashboard_react.css +0 -2
- cli_consumption-0.4.0/src/cli_consumption/dashboard_react.js +0 -9
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/.gitignore +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/LICENSE +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/NOTICE +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/README.md +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/__init__.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/__main__.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/__init__.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/_shared.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/aider.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/amazon_q.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/amp.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/base.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/claude.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/cline.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/codex.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/continue_cli.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/copilot.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/crush.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/cursor.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/gemini.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/goose.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/grok.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/kilo.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/kimi.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/mistral_vibe.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/opencode.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/openhands.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/pi.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/plandex.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/qwen.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/adapters/registry.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/exporting.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/migrations/__init__.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/migrations/env.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/migrations/versions/__init__.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/migrations/versions/v0001_baseline.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/migrations/versions/v0002_minimize_subagents.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/migrations/versions/v0003_canonical_timestamps.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/migrations/versions/v0004_subagent_scope_freshness.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/migrations/versions/v0005_sync_receipts.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/models.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/py.typed +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/qualifications.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/reporting.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/retention.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/snapshot_extraction.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/snapshot_files.py +0 -0
- {cli_consumption-0.4.0 → cli_consumption-0.4.2}/src/cli_consumption/sync.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
@@ -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
|
-
|
|
359
|
-
if
|
|
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(
|
|
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
|
|
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>
|