gitpulse-analytics 0.2.1__tar.gz → 1.0.0__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.
- {gitpulse_analytics-0.2.1 → gitpulse_analytics-1.0.0}/CHANGELOG.md +24 -1
- {gitpulse_analytics-0.2.1 → gitpulse_analytics-1.0.0}/PKG-INFO +15 -15
- {gitpulse_analytics-0.2.1 → gitpulse_analytics-1.0.0}/README.md +14 -14
- gitpulse_analytics-1.0.0/docs/brand/README.md +10 -0
- {gitpulse_analytics-0.2.1 → gitpulse_analytics-1.0.0}/pyproject.toml +1 -1
- {gitpulse_analytics-0.2.1 → gitpulse_analytics-1.0.0}/src/gitpulse/__init__.py +1 -1
- {gitpulse_analytics-0.2.1 → gitpulse_analytics-1.0.0}/src/gitpulse/cli.py +46 -24
- {gitpulse_analytics-0.2.1 → gitpulse_analytics-1.0.0}/src/gitpulse/report.py +141 -115
- {gitpulse_analytics-0.2.1 → gitpulse_analytics-1.0.0}/.gitignore +0 -0
- {gitpulse_analytics-0.2.1 → gitpulse_analytics-1.0.0}/LICENSE +0 -0
- {gitpulse_analytics-0.2.1 → gitpulse_analytics-1.0.0}/src/gitpulse/github_api.py +0 -0
- {gitpulse_analytics-0.2.1 → gitpulse_analytics-1.0.0}/src/gitpulse/org.py +0 -0
- {gitpulse_analytics-0.2.1 → gitpulse_analytics-1.0.0}/src/gitpulse/storage.py +0 -0
- {gitpulse_analytics-0.2.1 → gitpulse_analytics-1.0.0}/tests/test_cli.py +0 -0
- {gitpulse_analytics-0.2.1 → gitpulse_analytics-1.0.0}/tests/test_org.py +0 -0
- {gitpulse_analytics-0.2.1 → gitpulse_analytics-1.0.0}/tests/test_storage.py +0 -0
|
@@ -6,6 +6,28 @@ All notable changes to GitPulse are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.0.0] - 2026-11-08
|
|
10
|
+
|
|
11
|
+
GitPulse is out of beta. From this release, the commands, their options and
|
|
12
|
+
the snapshot database follow semantic versioning: breaking changes wait for 2.0.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- A GitPulse logo in the Full-Stack-Spectrum style: the Layered Core stack with
|
|
17
|
+
a red pulse line. The README opens with a banner, and the dashboard uses the
|
|
18
|
+
icon as its browser-tab icon.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- The HTML dashboard and terminal summary use the Full-Stack-Spectrum brand:
|
|
23
|
+
near-black surfaces with a dot grid, Space Grotesk and JetBrains Mono, and
|
|
24
|
+
the five-colour spectrum (blue, blue-soft, yellow, orange, red) for charts.
|
|
25
|
+
- The dashboard is dark only; the light theme and its toggle are gone.
|
|
26
|
+
- The language breakdown shows the top five languages plus Other, down from
|
|
27
|
+
seven, so each gets a spectrum colour.
|
|
28
|
+
- The dashboard loads its two fonts from Google Fonts and falls back to system
|
|
29
|
+
fonts offline.
|
|
30
|
+
|
|
9
31
|
## [0.2.1] - 2026-11-03
|
|
10
32
|
|
|
11
33
|
### Security
|
|
@@ -54,6 +76,7 @@ First public release.
|
|
|
54
76
|
[`fullstackspectrum/gitpulse`](https://github.com/fullstackspectrum/homebrew-gitpulse)
|
|
55
77
|
Homebrew tap, and as the multi-arch `fullstackspectrum/gitpulse` Docker image.
|
|
56
78
|
|
|
57
|
-
[Unreleased]: https://github.com/fullstackspectrum/gitpulse/compare/
|
|
79
|
+
[Unreleased]: https://github.com/fullstackspectrum/gitpulse/compare/v1.0.0...HEAD
|
|
80
|
+
[1.0.0]: https://github.com/fullstackspectrum/gitpulse/compare/v0.2.1...v1.0.0
|
|
58
81
|
[0.2.1]: https://github.com/fullstackspectrum/gitpulse/compare/v0.2.0...v0.2.1
|
|
59
82
|
[0.2.0]: https://github.com/fullstackspectrum/gitpulse/releases/tag/v0.2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: gitpulse-analytics
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: GitHub traffic, clones, referrers, and release downloads in your terminal, with org-wide HTML dashboards
|
|
5
5
|
Project-URL: Homepage, https://github.com/fullstackspectrum/gitpulse
|
|
6
6
|
Project-URL: Issues, https://github.com/fullstackspectrum/gitpulse/issues
|
|
@@ -9,7 +9,7 @@ Author-email: FullStackSpectrum <dev@fullstackspectrum.com>
|
|
|
9
9
|
License-Expression: MIT
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Keywords: analytics,cli,dashboard,downloads,github,releases,traffic
|
|
12
|
-
Classifier: Development Status ::
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
13
|
Classifier: Environment :: Console
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: Operating System :: OS Independent
|
|
@@ -30,23 +30,25 @@ Requires-Dist: pytest<9,>=8; extra == 'dev'
|
|
|
30
30
|
Requires-Dist: ruff<1,>=0.6; extra == 'dev'
|
|
31
31
|
Description-Content-Type: text/markdown
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
[](https://github.com/fullstackspectrum/gitpulse)
|
|
34
34
|
|
|
35
|
-
[](https://github.com/fullstackspectrum/gitpulse/actions/workflows/ci.yml)
|
|
36
|
-
[](https://github.com/fullstackspectrum/gitpulse/actions/workflows/ci.yml)
|
|
36
|
+
[](https://pypi.org/project/gitpulse-analytics/)
|
|
37
|
+
[](https://pypi.org/project/gitpulse-analytics/)
|
|
38
|
+
[](https://github.com/fullstackspectrum/homebrew-gitpulse)
|
|
39
|
+
[](https://hub.docker.com/r/fullstackspectrum/gitpulse)
|
|
40
|
+
[](LICENSE)
|
|
41
41
|
<br>
|
|
42
|
-
[](https://pepy.tech/projects/gitpulse-analytics)
|
|
43
|
+
[](https://hub.docker.com/r/fullstackspectrum/gitpulse)
|
|
44
44
|
|
|
45
45
|
A local Python CLI for GitHub repository traffic and release asset download stats. GitPulse can build a dashboard for a whole organization, and it keeps SQLite snapshots so you have history beyond GitHub's short traffic window.
|
|
46
46
|
|
|
47
|
+
Part of [Full-Stack-Spectrum](https://fullstackspectrum.com) ↗: small, sharp, open-source utilities that each do one job and install in one line.
|
|
48
|
+
|
|
47
49
|
> **What GitHub can tell you:** aggregate daily views and clones, unique counts, popular paths, referrers, and release-asset download totals. It does **not** identify individual visitors or people who cloned a repository. GitHub's traffic data is generally available for the most recent 14 days. Release asset downloads are not Homebrew formula install counts.
|
|
48
50
|
|
|
49
|
-

|
|
50
52
|
|
|
51
53
|
## Requirements
|
|
52
54
|
|
|
@@ -130,9 +132,7 @@ It also writes a self-contained HTML dashboard to `gitpulse-ORG.html` in the cur
|
|
|
130
132
|
- The most viewed and most starred repositories, a language breakdown, top referrers, and release downloads by repository
|
|
131
133
|
- A sortable, filterable table of every repository with a 14-day views sparkline
|
|
132
134
|
|
|
133
|
-
Styles, charts, and scripts are all inline
|
|
134
|
-
|
|
135
|
-

|
|
135
|
+
Styles, charts, and scripts are all inline. Only the owner's avatar and the Space Grotesk and JetBrains Mono web fonts load from the network; offline, the dashboard falls back to system fonts. It uses the dark [Full-Stack-Spectrum](https://fullstackspectrum.com) style.
|
|
136
136
|
|
|
137
137
|
By default, forks and archived repositories are skipped; `--all` includes them. When your token can't read traffic for a repository, its stars, forks, and downloads still appear, and the dashboard lists which repositories are missing traffic and why. Unique visitor and cloner counts are summed per repository, so one person who visits two repositories is counted twice.
|
|
138
138
|
|
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
[](https://github.com/fullstackspectrum/gitpulse/actions/workflows/ci.yml)
|
|
4
|
-
[](https://github.com/fullstackspectrum/gitpulse)
|
|
2
|
+
|
|
3
|
+
[](https://github.com/fullstackspectrum/gitpulse/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/gitpulse-analytics/)
|
|
5
|
+
[](https://pypi.org/project/gitpulse-analytics/)
|
|
6
|
+
[](https://github.com/fullstackspectrum/homebrew-gitpulse)
|
|
7
|
+
[](https://hub.docker.com/r/fullstackspectrum/gitpulse)
|
|
8
|
+
[](LICENSE)
|
|
9
9
|
<br>
|
|
10
|
-
[](https://pepy.tech/projects/gitpulse-analytics)
|
|
11
|
+
[](https://hub.docker.com/r/fullstackspectrum/gitpulse)
|
|
12
12
|
|
|
13
13
|
A local Python CLI for GitHub repository traffic and release asset download stats. GitPulse can build a dashboard for a whole organization, and it keeps SQLite snapshots so you have history beyond GitHub's short traffic window.
|
|
14
14
|
|
|
15
|
+
Part of [Full-Stack-Spectrum](https://fullstackspectrum.com) ↗: small, sharp, open-source utilities that each do one job and install in one line.
|
|
16
|
+
|
|
15
17
|
> **What GitHub can tell you:** aggregate daily views and clones, unique counts, popular paths, referrers, and release-asset download totals. It does **not** identify individual visitors or people who cloned a repository. GitHub's traffic data is generally available for the most recent 14 days. Release asset downloads are not Homebrew formula install counts.
|
|
16
18
|
|
|
17
|
-

|
|
18
20
|
|
|
19
21
|
## Requirements
|
|
20
22
|
|
|
@@ -98,9 +100,7 @@ It also writes a self-contained HTML dashboard to `gitpulse-ORG.html` in the cur
|
|
|
98
100
|
- The most viewed and most starred repositories, a language breakdown, top referrers, and release downloads by repository
|
|
99
101
|
- A sortable, filterable table of every repository with a 14-day views sparkline
|
|
100
102
|
|
|
101
|
-
Styles, charts, and scripts are all inline
|
|
102
|
-
|
|
103
|
-

|
|
103
|
+
Styles, charts, and scripts are all inline. Only the owner's avatar and the Space Grotesk and JetBrains Mono web fonts load from the network; offline, the dashboard falls back to system fonts. It uses the dark [Full-Stack-Spectrum](https://fullstackspectrum.com) style.
|
|
104
104
|
|
|
105
105
|
By default, forks and archived repositories are skipped; `--all` includes them. When your token can't read traffic for a repository, its stars, forks, and downloads still appear, and the dashboard lists which repositories are missing traffic and why. Unique visitor and cloner counts are summed per repository, so one person who visits two repositories is counted twice.
|
|
106
106
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# GitPulse brand assets
|
|
2
|
+
|
|
3
|
+
GitPulse follows the [Full-Stack-Spectrum](https://fullstackspectrum.com) design system.
|
|
4
|
+
|
|
5
|
+
- `gitpulse-icon.svg`: the mark on a 64×64 `#0a0d12` tile (radius 14), for avatars and app icons. The dashboard inlines it as its tab icon (`ICON` in `src/gitpulse/report.py`); keep the two in sync.
|
|
6
|
+
- `gitpulse-mark.svg`: the mark alone, for dark grounds only (its layers are filled `#0a0d12`).
|
|
7
|
+
- `readme-banner.png`: 2560×640, shown at 1280×320 at the top of the README.
|
|
8
|
+
- `github-social-preview.png`: 1280×640, uploaded under the repository's Settings → Social preview.
|
|
9
|
+
|
|
10
|
+
The mark is Layered Core with the red core replaced by a pulse line: blue and yellow layers behind a `#eef1f6` outline, cool at the back, hot at the front.
|
|
@@ -13,7 +13,7 @@ license-files = ["LICENSE"]
|
|
|
13
13
|
authors = [{ name = "FullStackSpectrum", email = "dev@fullstackspectrum.com" }]
|
|
14
14
|
keywords = ["github", "traffic", "analytics", "dashboard", "cli", "releases", "downloads"]
|
|
15
15
|
classifiers = [
|
|
16
|
-
"Development Status ::
|
|
16
|
+
"Development Status :: 5 - Production/Stable",
|
|
17
17
|
"Environment :: Console",
|
|
18
18
|
"Intended Audience :: Developers",
|
|
19
19
|
"Operating System :: OS Independent",
|
|
@@ -19,6 +19,7 @@ from rich.progress import (
|
|
|
19
19
|
TimeElapsedColumn,
|
|
20
20
|
)
|
|
21
21
|
from rich.table import Table
|
|
22
|
+
from rich.theme import Theme
|
|
22
23
|
|
|
23
24
|
from gitpulse import __version__
|
|
24
25
|
from gitpulse.github_api import GitHubAPIError, GitHubClient
|
|
@@ -31,7 +32,26 @@ app = typer.Typer(
|
|
|
31
32
|
help="Local GitHub repository traffic and release analytics.",
|
|
32
33
|
no_args_is_help=True,
|
|
33
34
|
)
|
|
34
|
-
|
|
35
|
+
# Full-Stack-Spectrum palette (fullstackspectrum.com): red leads, the rest of the spectrum marks data.
|
|
36
|
+
THEME = Theme(
|
|
37
|
+
{
|
|
38
|
+
"brand": "bold #ff4d5e",
|
|
39
|
+
"heading": "bold #8fb4ff",
|
|
40
|
+
"frame": "#2f3a4b",
|
|
41
|
+
"views": "#4f8cff",
|
|
42
|
+
"clones": "#ff8a3d",
|
|
43
|
+
"stars": "#f5c84c",
|
|
44
|
+
"downloads": "#8fb4ff",
|
|
45
|
+
"ok": "#8fb4ff",
|
|
46
|
+
"warn": "#f5c84c",
|
|
47
|
+
"error": "bold #ff4d5e",
|
|
48
|
+
"progress.spinner": "#ff4d5e",
|
|
49
|
+
"bar.complete": "#ff4d5e",
|
|
50
|
+
"bar.finished": "#4f8cff",
|
|
51
|
+
"bar.pulse": "#ff4d5e",
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
console = Console(theme=THEME)
|
|
35
55
|
|
|
36
56
|
|
|
37
57
|
def _version_callback(value: bool) -> None:
|
|
@@ -53,7 +73,7 @@ def _client() -> GitHubClient:
|
|
|
53
73
|
try:
|
|
54
74
|
return GitHubClient()
|
|
55
75
|
except GitHubAPIError as exc:
|
|
56
|
-
console.print(f"[
|
|
76
|
+
console.print(f"[error]Error:[/error] {exc}")
|
|
57
77
|
raise typer.Exit(code=1) from exc
|
|
58
78
|
|
|
59
79
|
|
|
@@ -61,7 +81,7 @@ def _run(action: Any) -> Any:
|
|
|
61
81
|
try:
|
|
62
82
|
return action()
|
|
63
83
|
except GitHubAPIError as exc:
|
|
64
|
-
console.print(f"[
|
|
84
|
+
console.print(f"[error]Error:[/error] {exc}")
|
|
65
85
|
raise typer.Exit(code=1) from exc
|
|
66
86
|
|
|
67
87
|
|
|
@@ -73,7 +93,7 @@ def _repo_parts(repository: str) -> tuple[str, str]:
|
|
|
73
93
|
|
|
74
94
|
|
|
75
95
|
def _traffic_table(owner: str, repo: str, metric: str, data: dict[str, Any]) -> Table:
|
|
76
|
-
table = Table(title=f"{owner}/{repo} · {metric}", header_style="
|
|
96
|
+
table = Table(title=f"{owner}/{repo} · {metric}", header_style="heading")
|
|
77
97
|
if metric in {"views", "clones"}:
|
|
78
98
|
table.add_column("Date")
|
|
79
99
|
table.add_column("Count", justify="right")
|
|
@@ -110,7 +130,7 @@ def list_repos(
|
|
|
110
130
|
repos = _run(_client().list_repositories)
|
|
111
131
|
if not all_repos:
|
|
112
132
|
repos = [repo for repo in repos if not repo.get("fork") and not repo.get("archived")]
|
|
113
|
-
table = Table(title="Your repositories", header_style="
|
|
133
|
+
table = Table(title="Your repositories", header_style="heading")
|
|
114
134
|
table.add_column("Repository")
|
|
115
135
|
table.add_column("Visibility")
|
|
116
136
|
table.add_column("Stars", justify="right")
|
|
@@ -143,7 +163,7 @@ def traffic(
|
|
|
143
163
|
console.print(_traffic_table(owner, repo, metric, data))
|
|
144
164
|
if save and metric in {"views", "clones"}:
|
|
145
165
|
SnapshotStore().save(owner, repo, metric, data)
|
|
146
|
-
console.print("[
|
|
166
|
+
console.print("[ok]Snapshot saved locally.[/ok]")
|
|
147
167
|
console.print("[dim]GitHub provides aggregate traffic data, not visitor identities; traffic history is typically limited to 14 days.[/dim]")
|
|
148
168
|
|
|
149
169
|
|
|
@@ -159,7 +179,7 @@ def snapshot(
|
|
|
159
179
|
data = _run(lambda metric=metric: client.traffic(owner, repo, metric))
|
|
160
180
|
store.save(owner, repo, metric, data)
|
|
161
181
|
console.print(_traffic_table(owner, repo, metric, data))
|
|
162
|
-
console.print("[
|
|
182
|
+
console.print("[ok]Snapshots saved. Run this regularly to build longer-term history.[/ok]")
|
|
163
183
|
|
|
164
184
|
|
|
165
185
|
@app.command()
|
|
@@ -173,7 +193,7 @@ def history(
|
|
|
173
193
|
if metric not in {"views", "clones"}:
|
|
174
194
|
raise typer.BadParameter("History is available for views or clones")
|
|
175
195
|
rows = SnapshotStore().history(owner, repo, metric, limit)
|
|
176
|
-
table = Table(title=f"Local history · {owner}/{repo} · {metric}", header_style="
|
|
196
|
+
table = Table(title=f"Local history · {owner}/{repo} · {metric}", header_style="heading")
|
|
177
197
|
table.add_column("Collected at")
|
|
178
198
|
table.add_column("14-day total", justify="right")
|
|
179
199
|
table.add_column("Sum of daily uniques*", justify="right")
|
|
@@ -199,7 +219,7 @@ def release_downloads(
|
|
|
199
219
|
"""Show GitHub release asset download counts (not Homebrew install counts)."""
|
|
200
220
|
owner, repo = _repo_parts(repository)
|
|
201
221
|
release_count, assets = _run(lambda: _client().release_downloads(owner, repo))
|
|
202
|
-
table = Table(title=f"Release asset downloads · {owner}/{repo}", header_style="
|
|
222
|
+
table = Table(title=f"Release asset downloads · {owner}/{repo}", header_style="heading")
|
|
203
223
|
table.add_column("Release")
|
|
204
224
|
table.add_column("Asset")
|
|
205
225
|
table.add_column("Downloads", justify="right")
|
|
@@ -237,22 +257,24 @@ def _org_summary(report: OrgReport) -> None:
|
|
|
237
257
|
name = report.profile.get("name") or report.owner
|
|
238
258
|
console.print(
|
|
239
259
|
Panel.fit(
|
|
260
|
+
f"[dim]//[/dim] [brand]gitpulse[/brand]\n"
|
|
240
261
|
f"[bold]{name}[/bold] [dim]@{report.owner}[/dim]\n"
|
|
241
262
|
f"{len(report.repos)} repositories · traffic {window}",
|
|
242
|
-
border_style="
|
|
263
|
+
border_style="frame",
|
|
243
264
|
)
|
|
244
265
|
)
|
|
245
266
|
|
|
246
|
-
def tile(label: str, value: int, style: str = "
|
|
247
|
-
|
|
267
|
+
def tile(label: str, value: int, style: str = "") -> Panel:
|
|
268
|
+
number = f"[{style}]{value:,}[/{style}]" if style else f"{value:,}"
|
|
269
|
+
return Panel(f"[bold]{number}[/bold]\n[dim]{label}[/dim]", expand=True, border_style="frame")
|
|
248
270
|
|
|
249
271
|
tiles = [
|
|
250
|
-
tile("views · 14d", report.total("views"), "
|
|
272
|
+
tile("views · 14d", report.total("views"), "views"),
|
|
251
273
|
tile("unique visitors*", report.total("view_uniques")),
|
|
252
|
-
tile("clones · 14d", report.total("clones"), "
|
|
253
|
-
tile("stars", report.total("stars"), "
|
|
274
|
+
tile("clones · 14d", report.total("clones"), "clones"),
|
|
275
|
+
tile("stars", report.total("stars"), "stars"),
|
|
254
276
|
tile("forks", report.total("forks")),
|
|
255
|
-
tile("release downloads", report.total("downloads"), "
|
|
277
|
+
tile("release downloads", report.total("downloads"), "downloads"),
|
|
256
278
|
]
|
|
257
279
|
grid = Table.grid(expand=True, padding=(0, 1))
|
|
258
280
|
for _ in range(3):
|
|
@@ -261,11 +283,11 @@ def _org_summary(report: OrgReport) -> None:
|
|
|
261
283
|
grid.add_row(*tiles[3:])
|
|
262
284
|
console.print(grid)
|
|
263
285
|
if views:
|
|
264
|
-
console.print(f" [dim]daily views[/dim] [
|
|
286
|
+
console.print(f" [dim]daily views[/dim] [views]{_spark([c for _, c in views])}[/views]")
|
|
265
287
|
clones = [c for _, c in report.daily("clones_daily")]
|
|
266
|
-
console.print(f" [dim]daily clones[/dim] [
|
|
288
|
+
console.print(f" [dim]daily clones[/dim] [clones]{_spark(clones)}[/clones]\n")
|
|
267
289
|
|
|
268
|
-
table = Table(header_style="
|
|
290
|
+
table = Table(header_style="heading", box=box.SIMPLE_HEAD, expand=True)
|
|
269
291
|
table.add_column("Repository", overflow="fold")
|
|
270
292
|
table.add_column("Lang", style="dim")
|
|
271
293
|
for column in ("Stars", "Forks", "Views", "Unique", "Clones", "Downloads"):
|
|
@@ -283,13 +305,13 @@ def _org_summary(report: OrgReport) -> None:
|
|
|
283
305
|
f"{repo.view_uniques:,}" if ok else na,
|
|
284
306
|
f"{repo.clones:,}" if ok else na,
|
|
285
307
|
f"{repo.downloads:,}" if repo.releases_error is None else na,
|
|
286
|
-
f"[
|
|
308
|
+
f"[views]{_spark([repo.views_daily.get(d, 0) for d in days])}[/views]" if ok and days else "",
|
|
287
309
|
)
|
|
288
310
|
console.print(table)
|
|
289
311
|
|
|
290
312
|
referrers = report.referrers(limit=5)
|
|
291
313
|
if referrers:
|
|
292
|
-
ref_table = Table(title="Top referrers", header_style="
|
|
314
|
+
ref_table = Table(title="Top referrers", header_style="heading", box=box.SIMPLE_HEAD)
|
|
293
315
|
ref_table.add_column("Source")
|
|
294
316
|
ref_table.add_column("Views", justify="right")
|
|
295
317
|
ref_table.add_column("Unique*", justify="right")
|
|
@@ -300,7 +322,7 @@ def _org_summary(report: OrgReport) -> None:
|
|
|
300
322
|
missing = [repo for repo in report.repos if repo.traffic_error]
|
|
301
323
|
if missing:
|
|
302
324
|
console.print(
|
|
303
|
-
f"[
|
|
325
|
+
f"[warn]Traffic unavailable for {len(missing)} of {len(report.repos)} repositories[/warn] "
|
|
304
326
|
"[dim](the token needs Administration: Read or push access). See the dashboard for details.[/dim]"
|
|
305
327
|
)
|
|
306
328
|
console.print("[dim]*Unique counts are summed per repository, so one person can be counted more than once.[/dim]")
|
|
@@ -354,11 +376,11 @@ def org_dashboard(
|
|
|
354
376
|
for metric, payload in repo.raw_traffic.items():
|
|
355
377
|
store.save(repo.owner, repo.name, metric, payload)
|
|
356
378
|
saved += 1
|
|
357
|
-
console.print(f"[
|
|
379
|
+
console.print(f"[ok]Saved snapshots for {saved} repositories.[/ok]")
|
|
358
380
|
|
|
359
381
|
path = Path(output or f"gitpulse-{report.owner}.html").expanduser().resolve()
|
|
360
382
|
path.write_text(render_html(report), encoding="utf-8")
|
|
361
|
-
console.print(f"\n[
|
|
383
|
+
console.print(f"\n[ok]✓[/ok] Dashboard written to [link={path.as_uri()}]{path}[/link]")
|
|
362
384
|
if open_browser:
|
|
363
385
|
webbrowser.open(path.as_uri())
|
|
364
386
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
"""Render an OrgReport as a self-contained HTML dashboard
|
|
1
|
+
"""Render an OrgReport as a self-contained HTML dashboard in the Full-Stack-Spectrum style.
|
|
2
|
+
|
|
3
|
+
Everything is inline except the two web fonts, which fall back to system fonts offline.
|
|
4
|
+
"""
|
|
2
5
|
|
|
3
6
|
from __future__ import annotations
|
|
4
7
|
|
|
@@ -6,12 +9,22 @@ import json
|
|
|
6
9
|
import math
|
|
7
10
|
from html import escape
|
|
8
11
|
from typing import Any
|
|
12
|
+
from urllib.parse import quote
|
|
9
13
|
|
|
10
14
|
from gitpulse.org import OrgReport, RepoStats
|
|
11
15
|
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
# The GitPulse mark (docs/brand/gitpulse-icon.svg), inlined as the tab icon.
|
|
17
|
+
ICON = (
|
|
18
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#0a0d12"/>'
|
|
19
|
+
'<rect x="25" y="11" width="28" height="28" rx="6" fill="#0a0d12" stroke="#4f8cff" stroke-width="3.5"/>'
|
|
20
|
+
'<rect x="18" y="18" width="28" height="28" rx="6" fill="#0a0d12" stroke="#f5c84c" stroke-width="3.5"/>'
|
|
21
|
+
'<rect x="11" y="25" width="28" height="28" rx="6" fill="#0a0d12" stroke="#eef1f6" stroke-width="3.5"/>'
|
|
22
|
+
'<path d="M16.5 40h4.5l2.75-6.5 4.5 12 2.75-5.5h3.5" fill="none" stroke="#ff4d5e" stroke-width="3" '
|
|
23
|
+
'stroke-linecap="round" stroke-linejoin="round"/></svg>'
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Categorical slots follow the five-colour spectrum, cool to hot; a sixth slot is "Other".
|
|
27
|
+
SERIES = 6
|
|
15
28
|
|
|
16
29
|
|
|
17
30
|
def compact(value: float) -> str:
|
|
@@ -157,7 +170,7 @@ def _languages(report: OrgReport) -> str:
|
|
|
157
170
|
for slot, (name, count) in enumerate(shown, start=1):
|
|
158
171
|
var = "--muted-fill" if name == "Other" else f"--series-{slot}"
|
|
159
172
|
share = count / total * 100
|
|
160
|
-
ink = "
|
|
173
|
+
ink = " other" if name == "Other" else ""
|
|
161
174
|
label = f"{share:.0f}%" if share >= 9 else ""
|
|
162
175
|
blocks.append(
|
|
163
176
|
f'<span class="segment{ink}" tabindex="0" style="flex:{count};background:var({var})" '
|
|
@@ -258,7 +271,7 @@ def render_html(report: OrgReport) -> str:
|
|
|
258
271
|
)
|
|
259
272
|
|
|
260
273
|
line = _line_chart(
|
|
261
|
-
[("Views", "--
|
|
274
|
+
[("Views", "--blue", views_daily), ("Clones", "--orange", clones_daily)]
|
|
262
275
|
)
|
|
263
276
|
return TEMPLATE.format(
|
|
264
277
|
title=escape(f"{display} · GitPulse"),
|
|
@@ -281,6 +294,7 @@ def render_html(report: OrgReport) -> str:
|
|
|
281
294
|
downloads=_bar_list(repo_bars("downloads", "downloads"), "No release asset downloads."),
|
|
282
295
|
rows=rows,
|
|
283
296
|
missing=missing_html,
|
|
297
|
+
icon=_attr("data:image/svg+xml," + quote(ICON)),
|
|
284
298
|
css=CSS,
|
|
285
299
|
js=JS,
|
|
286
300
|
)
|
|
@@ -288,125 +302,139 @@ def render_html(report: OrgReport) -> str:
|
|
|
288
302
|
|
|
289
303
|
CSS = """
|
|
290
304
|
:root {
|
|
291
|
-
color-scheme: light;
|
|
292
|
-
--page: #f9f9f7; --surface: #fcfcfb; --text: #0b0b0b; --text-2: #52514e; --muted: #898781;
|
|
293
|
-
--grid: #e1e0d9; --axis: #c3c2b7; --border: rgba(11,11,11,0.10); --wash: rgba(11,11,11,0.04);
|
|
294
|
-
--accent: #2a78d6; --muted-fill: #b4b2a9;
|
|
295
|
-
--series-1: #2a78d6; --series-2: #eb6834; --series-3: #1baf7a; --series-4: #eda100;
|
|
296
|
-
--series-5: #e87ba4; --series-6: #008300; --series-7: #6250d6; --series-8: #e34948;
|
|
297
|
-
}
|
|
298
|
-
@media (prefers-color-scheme: dark) {
|
|
299
|
-
:root:not([data-theme="light"]) {
|
|
300
|
-
color-scheme: dark;
|
|
301
|
-
--page: #0d0d0d; --surface: #1a1a19; --text: #f0efec; --text-2: #c3c2b7; --muted: #898781;
|
|
302
|
-
--grid: #2c2c2a; --axis: #383835; --border: rgba(255,255,255,0.10); --wash: rgba(255,255,255,0.05);
|
|
303
|
-
--accent: #3987e5; --muted-fill: #5f5e5a;
|
|
304
|
-
--series-1: #3987e5; --series-2: #d95926; --series-3: #199e70; --series-4: #c98500;
|
|
305
|
-
--series-5: #d55181; --series-6: #008300; --series-7: #9085e9; --series-8: #e66767;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
:root[data-theme="dark"] {
|
|
309
305
|
color-scheme: dark;
|
|
310
|
-
|
|
311
|
-
--
|
|
312
|
-
--
|
|
313
|
-
--
|
|
314
|
-
--
|
|
306
|
+
/* Full-Stack-Spectrum tokens (fullstackspectrum.com). Dark only, by design. */
|
|
307
|
+
--bg: #0a0d12; --bg-elev: #10151d; --bg-card: #121821;
|
|
308
|
+
--line: #222b38; --line-strong: #2f3a4b;
|
|
309
|
+
--text: #eef1f6; --text-dim: #9aa4b2; --text-faint: #646f7f;
|
|
310
|
+
--blue: #4f8cff; --blue-soft: #8fb4ff; --yellow: #f5c84c; --orange: #ff8a3d; --red: #ff4d5e;
|
|
311
|
+
--wash: rgba(255,255,255,0.04); --muted-fill: #646f7f;
|
|
312
|
+
/* Categorical slots run the spectrum cool to hot. */
|
|
313
|
+
--series-1: var(--blue); --series-2: var(--blue-soft); --series-3: var(--yellow);
|
|
314
|
+
--series-4: var(--orange); --series-5: var(--red);
|
|
315
|
+
--font-display: "Space Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
316
|
+
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
315
317
|
}
|
|
316
318
|
* { box-sizing: border-box; }
|
|
317
|
-
body { margin: 0;
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
.
|
|
327
|
-
.
|
|
328
|
-
|
|
329
|
-
.
|
|
330
|
-
.
|
|
331
|
-
.
|
|
319
|
+
body { margin: 0; color: var(--text); background-color: var(--bg);
|
|
320
|
+
background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,0.07) 1px, transparent 0);
|
|
321
|
+
background-size: 28px 28px;
|
|
322
|
+
font: 15px/1.6 var(--font-display); -webkit-font-smoothing: antialiased; }
|
|
323
|
+
a { color: inherit; text-decoration: none; transition: color .15s; }
|
|
324
|
+
a:hover { color: var(--blue-soft); }
|
|
325
|
+
:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; border-radius: 4px; }
|
|
326
|
+
::selection { background: var(--red); color: var(--bg); }
|
|
327
|
+
.wrap-page { max-width: 1160px; margin: 0 auto; padding: 48px clamp(16px, 4vw, 40px) 64px; }
|
|
328
|
+
.spectrum { display: flex; gap: 6px; margin-bottom: 28px; }
|
|
329
|
+
.spectrum span { width: 28px; height: 8px; border-radius: 1px; }
|
|
330
|
+
.spectrum span:nth-child(1) { background: var(--blue); }
|
|
331
|
+
.spectrum span:nth-child(2) { background: var(--blue-soft); }
|
|
332
|
+
.spectrum span:nth-child(3) { background: var(--yellow); }
|
|
333
|
+
.spectrum span:nth-child(4) { background: var(--orange); }
|
|
334
|
+
.spectrum span:nth-child(5) { background: var(--red); }
|
|
335
|
+
header { display: flex; align-items: center; gap: 20px; margin-bottom: 32px; }
|
|
336
|
+
.eyebrow { font: 13px/1.4 var(--font-mono); color: var(--red); letter-spacing: .04em; margin-bottom: 8px; }
|
|
337
|
+
.eyebrow::before { content: "// "; color: var(--text-faint); }
|
|
338
|
+
.avatar { width: 64px; height: 64px; border-radius: 10px; border: 1px solid var(--line-strong); }
|
|
339
|
+
h1 { font-size: 40px; line-height: 1.05; margin: 0; font-weight: 700; letter-spacing: -0.03em; }
|
|
340
|
+
h2 { font-size: 19px; line-height: 1.3; margin: 0 0 4px; font-weight: 700; letter-spacing: -0.01em; }
|
|
341
|
+
.sub { color: var(--text-dim); font: 13px/1.6 var(--font-mono); margin-top: 8px; }
|
|
342
|
+
.card { background: var(--bg-card); border: 1px solid var(--line); border-radius: 10px; padding: 24px; }
|
|
343
|
+
.card-sub { color: var(--text-dim); font-size: 13px; margin-bottom: 18px; }
|
|
332
344
|
.overview { display: grid; grid-template-columns: minmax(220px, 1fr) 3fr; gap: 16px; margin-bottom: 16px; }
|
|
333
|
-
.hero-label { color: var(--text-
|
|
334
|
-
.hero-value { font-size:
|
|
335
|
-
.tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--
|
|
336
|
-
border-radius:
|
|
337
|
-
.tile { background: var(--
|
|
338
|
-
.tile-
|
|
339
|
-
.tile-
|
|
340
|
-
.tile-note { color: var(--muted); font-size: 12px; }
|
|
345
|
+
.hero-label, .tile-label { color: var(--text-dim); font: 12px/1.4 var(--font-mono); text-transform: lowercase; }
|
|
346
|
+
.hero-value { font-size: 64px; font-weight: 700; line-height: 1; letter-spacing: -0.04em; margin: 14px 0 10px; }
|
|
347
|
+
.tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--line);
|
|
348
|
+
border-radius: 10px; overflow: hidden; border: 1px solid var(--line); }
|
|
349
|
+
.tile { background: var(--bg-card); padding: 18px 22px; }
|
|
350
|
+
.tile-value { font-size: 28px; font-weight: 700; letter-spacing: -0.02em; margin-top: 4px; }
|
|
351
|
+
.tile-note { color: var(--text-dim); font-size: 12px; }
|
|
341
352
|
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px; }
|
|
342
353
|
.full { margin-top: 16px; }
|
|
343
|
-
.legend { display: flex; gap: 16px; font
|
|
344
|
-
.legend.wrap { flex-wrap: wrap; gap: 6px 16px; margin:
|
|
354
|
+
.legend { display: flex; gap: 16px; font: 12px/1.4 var(--font-mono); color: var(--text-dim); margin-bottom: 12px; }
|
|
355
|
+
.legend.wrap { flex-wrap: wrap; gap: 6px 16px; margin: 14px 0 0; }
|
|
345
356
|
.legend-item { display: inline-flex; align-items: center; gap: 6px; }
|
|
346
357
|
.key-line { width: 14px; height: 2px; border-radius: 1px; }
|
|
347
|
-
.swatch { width: 10px; height: 10px; border-radius:
|
|
348
|
-
.muted { color: var(--
|
|
349
|
-
.empty { color: var(--
|
|
358
|
+
.swatch { width: 10px; height: 10px; border-radius: 2px; }
|
|
359
|
+
.muted { color: var(--text-dim); }
|
|
360
|
+
.empty { color: var(--text-dim); margin: 24px 0; text-align: center; }
|
|
350
361
|
.linechart { display: grid; grid-template-columns: 44px 1fr; grid-template-rows: 240px auto; }
|
|
351
362
|
.y-axis { position: relative; }
|
|
352
|
-
.y-tick { position: absolute;
|
|
353
|
-
|
|
363
|
+
.y-tick, .end-label, .x-tick { position: absolute; font: 11px/1.4 var(--font-mono); color: var(--text-dim);
|
|
364
|
+
font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
365
|
+
.y-tick { right: 8px; transform: translateY(-50%); }
|
|
354
366
|
.plot { position: relative; outline: none; margin-right: 40px; }
|
|
355
367
|
.plot svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
|
356
|
-
.plot .grid { stroke: var(--
|
|
368
|
+
.plot .grid { stroke: var(--line); stroke-width: 1; vector-effect: non-scaling-stroke; }
|
|
357
369
|
.plot .line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round;
|
|
358
370
|
vector-effect: non-scaling-stroke; }
|
|
359
|
-
.plot .area { opacity: 0.
|
|
371
|
+
.plot .area { opacity: 0.12; }
|
|
360
372
|
.end-labels { position: absolute; top: 0; bottom: 0; left: 100%; }
|
|
361
|
-
.end-label {
|
|
362
|
-
color: var(--text-2); font-variant-numeric: tabular-nums; }
|
|
373
|
+
.end-label { left: 8px; transform: translateY(-50%); color: var(--text); }
|
|
363
374
|
.x-axis { grid-column: 2; position: relative; height: 22px; margin-right: 40px; }
|
|
364
|
-
.x-tick {
|
|
365
|
-
|
|
366
|
-
.crosshair { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--axis); pointer-events: none; }
|
|
375
|
+
.x-tick { top: 6px; transform: translateX(-50%); }
|
|
376
|
+
.crosshair { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--line-strong); pointer-events: none; }
|
|
367
377
|
.dot { position: absolute; width: 10px; height: 10px; border-radius: 50%; transform: translate(-50%, -50%);
|
|
368
|
-
box-shadow: 0 0 0 2px var(--
|
|
378
|
+
box-shadow: 0 0 0 2px var(--bg-card); pointer-events: none; }
|
|
369
379
|
.bars { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
|
|
370
380
|
.bar-row { display: grid; grid-template-columns: minmax(90px, min(34%, 220px)) 1fr 52px; align-items: center; gap: 10px;
|
|
371
381
|
padding: 5px 6px; border-radius: 6px; outline: none; }
|
|
372
382
|
.bar-row:hover, .bar-row:focus-visible { background: var(--wash); }
|
|
373
|
-
.bar-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-
|
|
374
|
-
.bar-track { height:
|
|
375
|
-
.bar { display: block; height: 100%; background: var(--
|
|
376
|
-
.bar-value { text-align: right; font-variant-numeric: tabular-nums; }
|
|
383
|
+
.bar-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-dim); font-size: 14px; }
|
|
384
|
+
.bar-track { height: 10px; }
|
|
385
|
+
.bar { display: block; height: 100%; background: var(--blue); border-radius: 0 2px 2px 0; }
|
|
386
|
+
.bar-value { text-align: right; font: 13px/1.4 var(--font-mono); font-variant-numeric: tabular-nums; }
|
|
377
387
|
.segmented { display: flex; gap: 2px; height: 28px; }
|
|
378
|
-
.segment { min-width: 3px; border-radius: 4px; color:
|
|
388
|
+
.segment { min-width: 3px; border-radius: 4px; color: var(--bg); font: 600 12px/1 var(--font-mono);
|
|
379
389
|
padding-left: 8px; display: flex; align-items: center; overflow: hidden; outline: none; }
|
|
380
|
-
.segment.
|
|
390
|
+
.segment.other { color: var(--text); }
|
|
381
391
|
.segment:hover, .segment:focus-visible { filter: brightness(1.08); }
|
|
382
392
|
.table-head { display: flex; align-items: end; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
|
|
383
|
-
.search { background: var(--
|
|
384
|
-
padding:
|
|
385
|
-
.
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
393
|
+
.search { background: var(--bg); color: var(--text); border: 1px solid var(--line-strong); border-radius: 8px;
|
|
394
|
+
padding: 8px 12px; font: 13px/1.4 var(--font-mono); min-width: 240px; }
|
|
395
|
+
.search::placeholder { color: var(--text-dim); }
|
|
396
|
+
.search:focus-visible { border-color: var(--blue); }
|
|
397
|
+
.table-scroll { overflow-x: auto; margin-top: 14px; }
|
|
398
|
+
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
399
|
+
th { text-align: left; font: 500 12px/1.4 var(--font-mono); text-transform: lowercase; color: var(--text-dim);
|
|
400
|
+
padding: 8px 10px; border-bottom: 1px solid var(--line-strong); cursor: pointer; user-select: none; white-space: nowrap; }
|
|
401
|
+
th:hover, th[aria-sort] { color: var(--text); }
|
|
389
402
|
th.num { text-align: right; }
|
|
390
|
-
th[aria-sort]::after { content: " ↓"; color: var(--
|
|
403
|
+
th[aria-sort]::after { content: " ↓"; color: var(--red); }
|
|
391
404
|
th[aria-sort="ascending"]::after { content: " ↑"; }
|
|
392
|
-
td { padding:
|
|
393
|
-
td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
405
|
+
td { padding: 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
406
|
+
td.num { text-align: right; font: 13px/1.6 var(--font-mono); font-variant-numeric: tabular-nums; }
|
|
394
407
|
tr:hover td { background: var(--wash); }
|
|
395
|
-
|
|
408
|
+
td a { font-weight: 600; }
|
|
409
|
+
.desc { color: var(--text-dim); font-size: 13px; max-width: 360px; overflow: hidden; text-overflow: ellipsis;
|
|
396
410
|
white-space: nowrap; }
|
|
397
|
-
.badge { font
|
|
398
|
-
padding:
|
|
399
|
-
.nowrap { white-space: nowrap; }
|
|
411
|
+
.badge { font: 600 10px/1.3 var(--font-mono); letter-spacing: .04em; color: var(--text-dim); background: var(--bg-elev);
|
|
412
|
+
border: 1px solid var(--line); border-radius: 4px; padding: 2px 6px; margin-left: 8px; vertical-align: 2px; }
|
|
413
|
+
.nowrap { white-space: nowrap; font: 13px/1.6 var(--font-mono); color: var(--text-dim); }
|
|
400
414
|
.spark-cell { width: 110px; }
|
|
401
415
|
.spark { width: 100px; height: 24px; display: block; }
|
|
402
|
-
.spark polyline { fill: none; stroke: var(--
|
|
403
|
-
.notice { margin-top: 16px; color: var(--text-
|
|
404
|
-
.notice summary { cursor: pointer; }
|
|
416
|
+
.spark polyline { fill: none; stroke: var(--blue); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
|
|
417
|
+
.notice { margin-top: 16px; color: var(--text-dim); font-size: 14px; }
|
|
418
|
+
.notice summary { cursor: pointer; color: var(--text); }
|
|
419
|
+
.notice summary::marker { color: var(--yellow); }
|
|
405
420
|
.notice li { margin: 4px 0; }
|
|
406
|
-
footer { color: var(--
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
421
|
+
footer { color: var(--text-dim); font-size: 13px; margin-top: 40px; padding-top: 24px; border-top: 1px solid var(--line); }
|
|
422
|
+
footer p { margin: 0 0 20px; max-width: 80ch; }
|
|
423
|
+
.foot-row { display: flex; align-items: center; justify-content: space-between; gap: 12px 24px; flex-wrap: wrap;
|
|
424
|
+
font: 13px/1.4 var(--font-mono); }
|
|
425
|
+
.brand { display: inline-flex; align-items: center; gap: 12px; color: var(--text); font: 700 15px/1 var(--font-display);
|
|
426
|
+
letter-spacing: -0.01em; }
|
|
427
|
+
/* Layered Core mark: the front square with a red core; offset shadows draw the yellow and blue layers behind it. */
|
|
428
|
+
.mark { --s: 18px; --b: 2.25px; --o: 4.5px; --core: 6px;
|
|
429
|
+
width: var(--s); height: var(--s); flex: none; margin: calc(var(--o) * 2) calc(var(--o) * 2) 0 0;
|
|
430
|
+
display: grid; place-items: center; background: var(--bg); border: var(--b) solid var(--text);
|
|
431
|
+
border-radius: calc(var(--s) * 0.214);
|
|
432
|
+
box-shadow: var(--o) calc(var(--o) * -1) 0 calc(var(--b) * -1) var(--bg), var(--o) calc(var(--o) * -1) 0 0 var(--yellow),
|
|
433
|
+
calc(var(--o) * 2) calc(var(--o) * -2) 0 calc(var(--b) * -1) var(--bg), calc(var(--o) * 2) calc(var(--o) * -2) 0 0 var(--blue); }
|
|
434
|
+
.mark::after { content: ""; width: var(--core); height: var(--core); background: var(--red); border-radius: 1px; }
|
|
435
|
+
#tooltip { position: fixed; z-index: 10; pointer-events: none; background: var(--bg-elev); color: var(--text);
|
|
436
|
+
border: 1px solid var(--line-strong); border-radius: 8px; padding: 8px 10px; font: 12px/1.5 var(--font-mono);
|
|
437
|
+
max-width: 300px; }
|
|
410
438
|
#tooltip .tt-row { display: flex; align-items: center; gap: 6px; }
|
|
411
439
|
#tooltip .tt-val { margin-left: auto; padding-left: 12px; font-variant-numeric: tabular-nums; font-weight: 600; }
|
|
412
440
|
@media (max-width: 860px) {
|
|
@@ -414,31 +442,20 @@ footer { color: var(--muted); font-size: 12px; margin-top: 24px; }
|
|
|
414
442
|
.tiles { grid-template-columns: repeat(2, 1fr); }
|
|
415
443
|
}
|
|
416
444
|
@media (max-width: 520px) {
|
|
417
|
-
.wrap-page { padding:
|
|
418
|
-
.hero-value { font-size:
|
|
445
|
+
.wrap-page { padding: 28px 16px 48px; }
|
|
446
|
+
.hero-value { font-size: 52px; }
|
|
419
447
|
.search { min-width: 0; width: 100%; }
|
|
420
|
-
h1 { font-size:
|
|
421
|
-
.avatar { width:
|
|
422
|
-
.theme-toggle { padding: 5px 9px; font-size: 12px; }
|
|
448
|
+
h1 { font-size: 28px; }
|
|
449
|
+
.avatar { width: 44px; height: 44px; }
|
|
423
450
|
.x-tick.alt { display: none; }
|
|
424
451
|
}
|
|
452
|
+
@media (prefers-reduced-motion: reduce) {
|
|
453
|
+
* { transition: none !important; }
|
|
454
|
+
}
|
|
425
455
|
"""
|
|
426
456
|
|
|
427
457
|
JS = r"""
|
|
428
458
|
(() => {
|
|
429
|
-
const root = document.documentElement;
|
|
430
|
-
const toggle = document.querySelector('.theme-toggle');
|
|
431
|
-
const prefersDark = () => matchMedia('(prefers-color-scheme: dark)').matches;
|
|
432
|
-
const current = () => root.dataset.theme || (prefersDark() ? 'dark' : 'light');
|
|
433
|
-
try { const saved = localStorage.getItem('gitpulse-theme'); if (saved) root.dataset.theme = saved; } catch (e) {}
|
|
434
|
-
const label = () => { toggle.textContent = current() === 'dark' ? 'Light mode' : 'Dark mode'; };
|
|
435
|
-
toggle.addEventListener('click', () => {
|
|
436
|
-
root.dataset.theme = current() === 'dark' ? 'light' : 'dark';
|
|
437
|
-
try { localStorage.setItem('gitpulse-theme', root.dataset.theme); } catch (e) {}
|
|
438
|
-
label();
|
|
439
|
-
});
|
|
440
|
-
label();
|
|
441
|
-
|
|
442
459
|
const tip = document.getElementById('tooltip');
|
|
443
460
|
const fmt = (n) => n.toLocaleString();
|
|
444
461
|
const show = (html, x, y) => {
|
|
@@ -530,17 +547,23 @@ TEMPLATE = """<!doctype html>
|
|
|
530
547
|
<meta charset="utf-8" />
|
|
531
548
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
532
549
|
<title>{title}</title>
|
|
550
|
+
<meta name="theme-color" content="#0a0d12" />
|
|
551
|
+
<link rel="icon" type="image/svg+xml" href="{icon}" />
|
|
552
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
553
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
554
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap" />
|
|
533
555
|
<style>{css}</style>
|
|
534
556
|
</head>
|
|
535
557
|
<body>
|
|
536
558
|
<div class="wrap-page">
|
|
559
|
+
<div class="spectrum" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span></div>
|
|
537
560
|
<header>
|
|
538
561
|
{avatar}
|
|
539
562
|
<div>
|
|
563
|
+
<div class="eyebrow">gitpulse</div>
|
|
540
564
|
<h1><a href="{owner_url}" target="_blank" rel="noopener">{display}</a></h1>
|
|
541
565
|
<div class="sub">@{owner} · {repo_count} repositories · traffic {window} · generated {generated}</div>
|
|
542
566
|
</div>
|
|
543
|
-
<button class="theme-toggle" type="button">Dark mode</button>
|
|
544
567
|
</header>
|
|
545
568
|
|
|
546
569
|
<section class="overview">
|
|
@@ -611,9 +634,12 @@ TEMPLATE = """<!doctype html>
|
|
|
611
634
|
{missing}
|
|
612
635
|
|
|
613
636
|
<footer>
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
637
|
+
<p>GitHub reports aggregate traffic for roughly the last 14 days and never identifies individual visitors.
|
|
638
|
+
Unique counts are summed per repository and per day, so one person can be counted more than once.</p>
|
|
639
|
+
<div class="foot-row">
|
|
640
|
+
<a class="brand" href="https://fullstackspectrum.com" target="_blank" rel="noopener"><span class="mark"></span><span>Full-Stack-Spectrum</span></a>
|
|
641
|
+
<a href="https://github.com/fullstackspectrum/gitpulse" target="_blank" rel="noopener">generated by gitpulse ↗</a>
|
|
642
|
+
</div>
|
|
617
643
|
</footer>
|
|
618
644
|
</div>
|
|
619
645
|
<div id="tooltip" role="tooltip" hidden></div>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|