diff --git a/README.md b/README.md
index ec0369e..0fd40d2 100644
--- a/README.md
+++ b/README.md
@@ -76,7 +76,7 @@ more.
- **Fonts:** the web theme uses the system sans stack; Monocraft (self-hosted in `public/`) is
used by the desktop theme's SMP menu and the in-desktop terminal.
- **Images:** `next/image`
-- **Version:** `2.2.9` (see `package.json`)
+- **Version:** `2.2.10` (see `package.json`)
- **Dependencies:** `npm audit` reports **0 vulnerabilities**. `next` is kept at a patched
release (`>=16.3.6`) for the `next/og` RCE, and `brace-expansion` is force-upgraded via
`overrides`. `braces` (a transitive ESLint/`fast-glob` dependency) has **no upstream fix**,
@@ -172,7 +172,7 @@ they contend for `:3000` and `.next/dev/lock`.
| Route | Method | What it does |
| --- | --- | --- |
-| `/api/heatmap` | `GET` | Proxies the user's contribution heatmap from `git.dvorakmortonxd.dev`. |
+| `/api/heatmap` | `GET` | Proxies the user's contribution heatmap from `git.dvorakmortonxd.dev`. Returns `502` (and the panel shows a friendly fallback) if the upstream is unreachable. |
| `/api/activities` | `GET` | Fetches the GitDXD activity feed and keeps only the user's own events. |
| `/api/frame?url=…` | `GET` | Header-stripping proxy for a friend's site that refuses framing (`skopec.dev`). **Allow-listed hosts only**; fetches the HTML server-side (so the site's `X-Frame-Options` never reaches the browser), injects a `` for relative assets and a click shim for in-site links. Rejects other hosts, non-http(s) schemes and credentials, and never follows redirects. |
| `/api/ping?url=…` | `GET` | Health-checks an **allow-listed** service (the site's own `*.dvorakmortonxd.dev` hosts and the friends' sites). Handles `minecraft://host:port` via mcstatus.io; treats any non-5xx response as "up". Other hosts and schemes are rejected and redirects are never followed, so it can't be used to probe the LAN (SSRF guard). |
diff --git a/agents.md b/agents.md
index 5a3ce2d..4f5644d 100644
--- a/agents.md
+++ b/agents.md
@@ -34,7 +34,7 @@ When in doubt, prefer updating. A stale README **or** agents.md is treated as a
## Bump the version on EVERY prompt — also do this every time
Another standing requirement: increment the **patch** version (`x.x.+1`, so
-`2.2.8` → `2.2.9` → …) as part of every task, even the small ones. Keep all of
+`2.2.9` → `2.2.10` → …) as part of every task, even the small ones. Keep all of
these in sync:
- `package.json` → `"version"`
@@ -56,7 +56,7 @@ frosted-glass **web** site, and an alternate KDE/Plasma **desktop simulation**
utilities in the components (Tailwind is present via PostCSS but the look is
hand-written CSS).
- Path alias: `@/*` → `src/*`.
-- Version: `2.2.9` (see `package.json`).
+- Version: `2.2.10` (see `package.json`).
### Scripts
```bash
@@ -96,6 +96,7 @@ src/app/
revival-inc/page.tsx → fullscreen iframe embedding /public/revival-inc/index.html
api/
heatmap/route.ts → proxy for git.dvorakmortonxd.dev contribution heatmap
+ (try/catch → 502 on upstream failure; the widget degrades to a fallback)
activities/route.ts → proxy for the user's GitDXD activity feed
ping/route.ts → service health check (HTTP + minecraft:// via mcstatus.io); host allowlist
frame/route.ts → header-stripping proxy so an unframeable friend site loads in a window; host allowlist
diff --git a/package-lock.json b/package-lock.json
index 339dbe6..8155748 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "dvorakmortonxd",
- "version": "2.2.9",
+ "version": "2.2.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dvorakmortonxd",
- "version": "2.2.9",
+ "version": "2.2.10",
"dependencies": {
"next": "^16.3.8",
"react": "^19.3.0",
diff --git a/package.json b/package.json
index e9f45f3..ccb998a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "dvorakmortonxd",
- "version": "2.2.9",
+ "version": "2.2.10",
"private": true,
"scripts": {
"dev": "node scripts/dev-server.mjs",
diff --git a/src/app/api/heatmap/route.ts b/src/app/api/heatmap/route.ts
index 14189bc..5440782 100644
--- a/src/app/api/heatmap/route.ts
+++ b/src/app/api/heatmap/route.ts
@@ -1,5 +1,15 @@
export async function GET() {
- const res = await fetch("https://git.dvorakmortonxd.dev/api/v1/users/dvorakmortonxd/heatmap");
- const data = await res.json();
- return Response.json(data);
-}
\ No newline at end of file
+ try {
+ const res = await fetch(
+ "https://git.dvorakmortonxd.dev/api/v1/users/dvorakmortonxd/heatmap",
+ { headers: { Accept: "application/json" } }
+ );
+ if (!res.ok) {
+ return Response.json({ error: "failed to load" }, { status: 502 });
+ }
+ const data = await res.json();
+ return Response.json(data);
+ } catch {
+ return Response.json({ error: "failed to load" }, { status: 502 });
+ }
+}
diff --git a/src/app/components/ContributionHeatmap.tsx b/src/app/components/ContributionHeatmap.tsx
index 6143b78..9faea3b 100644
--- a/src/app/components/ContributionHeatmap.tsx
+++ b/src/app/components/ContributionHeatmap.tsx
@@ -39,9 +39,16 @@ export default function ContributionHeatmap({ variant = "terminal" }: { variant?
try {
const res = await fetch("/api/heatmap");
if (!res.ok) {
- throw new Error("Failed to fetch");
+ setError(true);
+ setLoading(false);
+ return;
}
const data: HeatmapData[] = await res.json();
+ if (!Array.isArray(data)) {
+ setError(true);
+ setLoading(false);
+ return;
+ }
const map = new Map();
let total = 0;
let maxContribs = 0;
@@ -90,7 +97,7 @@ export default function ContributionHeatmap({ variant = "terminal" }: { variant?
setTotalContribs(total);
setLoading(false);
} catch (err) {
- console.error("Failed to load heatmap:", err);
+ console.warn("Failed to load heatmap:", err);
setError(true);
setLoading(false);
}
@@ -106,7 +113,7 @@ export default function ContributionHeatmap({ variant = "terminal" }: { variant?
setActivities(valid);
}
} catch (err) {
- console.error("Failed to load activities:", err);
+ console.warn("Failed to load activities:", err);
}
}
diff --git a/src/app/components/WebLayout.tsx b/src/app/components/WebLayout.tsx
index 946add8..2fe9261 100644
--- a/src/app/components/WebLayout.tsx
+++ b/src/app/components/WebLayout.tsx
@@ -318,7 +318,7 @@ export default function WebLayout() {
diff --git a/src/app/web.css b/src/app/web.css
index 4a686e9..3cae6a7 100644
--- a/src/app/web.css
+++ b/src/app/web.css
@@ -649,44 +649,44 @@ html[data-theme="web"] body {
.fw-tabs {
display: flex;
align-items: flex-end;
- gap: 6px;
- height: 32px;
- padding: 7px 10px 0;
+ gap: 5px;
+ height: 26px;
+ padding: 5px 10px 0;
}
.fw-tab {
display: inline-flex;
align-items: center;
- gap: 7px;
+ gap: 6px;
max-width: 62%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
- padding: 6px 13px;
- border-radius: 9px 9px 0 0;
+ padding: 3px 11px;
+ border-radius: 8px 8px 0 0;
border: 1px solid var(--btn-border, var(--glass-border));
border-bottom: none;
background: var(--btn-bg, var(--glass-lo));
- font-size: 0.66rem;
+ font-size: 0.62rem;
color: var(--fg);
}
.fw-tab-dot {
flex: none;
- width: 8px;
- height: 8px;
+ width: 7px;
+ height: 7px;
border-radius: 50%;
background: var(--accent);
}
.fw-tab-plus {
- padding: 0 6px;
+ padding: 0 5px;
color: var(--web-muted);
- font-size: 0.9rem;
+ font-size: 0.82rem;
line-height: 1;
}
.fw-toolbar {
display: flex;
align-items: center;
- gap: 8px;
- padding: 8px 12px;
+ gap: 6px;
+ padding: 5px 10px;
border-top: 1px solid var(--glass-border);
border-bottom: 1px solid var(--glass-border);
background: var(--fw-chrome);
@@ -695,12 +695,12 @@ html[data-theme="web"] body {
flex: none;
display: grid;
place-items: center;
- width: 22px;
- height: 22px;
+ width: 20px;
+ height: 20px;
border-radius: 50%;
background: var(--btn-bg, var(--glass-lo));
color: var(--web-muted);
- font-size: 0.85rem;
+ font-size: 0.78rem;
line-height: 1;
}
.fw-url {
@@ -708,13 +708,13 @@ html[data-theme="web"] body {
display: flex;
align-items: center;
gap: 8px;
- height: 26px;
- padding: 0 12px;
+ height: 22px;
+ padding: 0 11px;
border-radius: 999px;
border: 1px solid var(--glass-border);
background: var(--glass-lo);
font-family: var(--font-custom-mono);
- font-size: 0.66rem;
+ font-size: 0.62rem;
color: var(--web-muted);
}
.fw-lock {