Compare commits
10 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61cf83ad7a | |||
| 859635b45a | |||
| 150e914ed4 | |||
| 80012a7b6a | |||
| d96dfc24f2 | |||
| dc9c1c9787 | |||
| 8574172b5b | |||
| 96a2e2beac | |||
| dd9f5eee28 | |||
| 5045bbfb3f |
12 changed files with 358 additions and 149 deletions
24
.forgejo/workflows/lint.yml
Normal file
24
.forgejo/workflows/lint.yml
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: '1.26'
|
||||||
|
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v9
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- name: Run Biome
|
||||||
|
run: npx @biomejs/biome@latest check .
|
||||||
56
.forgejo/workflows/release.yml
Normal file
56
.forgejo/workflows/release.yml
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
name: Build and Push Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Get version from VERSION file
|
||||||
|
id: version
|
||||||
|
run: echo "VERSION=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Create tag
|
||||||
|
run: |
|
||||||
|
git config user.name forgejo-actions
|
||||||
|
git config user.email forgejo-actions@lonecloud.dev
|
||||||
|
git tag -a v${{ steps.version.outputs.VERSION }} -m "Release v${{ steps.version.outputs.VERSION }}"
|
||||||
|
git push origin v${{ steps.version.outputs.VERSION }}
|
||||||
|
|
||||||
|
- name: Create Forgejo Release
|
||||||
|
run: |
|
||||||
|
curl -s -X POST "https://git.lonecloud.dev/api/v1/repos/${{ github.repository }}/releases" \
|
||||||
|
-H "Authorization: token ${{ secrets.FORGEJO_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\":\"v${{ steps.version.outputs.VERSION }}\",\"name\":\"v${{ steps.version.outputs.VERSION }}\",\"generate_release_notes\":true}"
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
|
- name: Login to Forgejo Container Registry
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: git.lonecloud.dev
|
||||||
|
username: ${{ secrets.FORGEJO_USERNAME }}
|
||||||
|
password: ${{ secrets.FORGEJO_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
no-cache: true
|
||||||
|
tags: |
|
||||||
|
git.lonecloud.dev/lone-cloud/snowflake-dashboard:latest
|
||||||
|
git.lonecloud.dev/lone-cloud/snowflake-dashboard:v${{ steps.version.outputs.VERSION }}
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -4,3 +4,5 @@ server
|
||||||
*.test
|
*.test
|
||||||
*.out
|
*.out
|
||||||
tmp/main
|
tmp/main
|
||||||
|
.agents/
|
||||||
|
.impeccable.md
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ COPY styles.css /app/static/styles.css
|
||||||
COPY dashboard.js /app/static/dashboard.js
|
COPY dashboard.js /app/static/dashboard.js
|
||||||
COPY favicon.svg /app/static/favicon.svg
|
COPY favicon.svg /app/static/favicon.svg
|
||||||
COPY favicon-dark.svg /app/static/favicon-dark.svg
|
COPY favicon-dark.svg /app/static/favicon-dark.svg
|
||||||
|
COPY fonts/ /app/static/fonts/
|
||||||
COPY VERSION /tmp/VERSION
|
COPY VERSION /tmp/VERSION
|
||||||
RUN VERSION=$(cat /tmp/VERSION) && \
|
RUN VERSION=$(cat /tmp/VERSION) && \
|
||||||
sed "s/?v=VERSION/?v=$VERSION/g" /tmp/index.html > /app/static/index.html
|
sed "s/?v=VERSION/?v=$VERSION/g" /tmp/index.html > /app/static/index.html
|
||||||
|
|
|
||||||
5
Makefile
5
Makefile
|
|
@ -20,6 +20,9 @@ test:
|
||||||
go test -v ./...
|
go test -v ./...
|
||||||
|
|
||||||
release:
|
release:
|
||||||
gh workflow run release.yml
|
curl -s -X POST "https://git.lonecloud.dev/api/v1/repos/lone-cloud/snowflake-dashboard/actions/workflows/release.yml/dispatches" \
|
||||||
|
-H "Authorization: token $$(cat ~/.config/forgejo-token)" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"ref":"master"}'
|
||||||
|
|
||||||
.PHONY: lint fix check dev build docker test release
|
.PHONY: lint fix check dev build docker test release
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
1.9.1
|
1.10.2
|
||||||
106
dashboard.js
106
dashboard.js
|
|
@ -1,7 +1,7 @@
|
||||||
function formatBytes(bytes) {
|
function formatBytes(bytes) {
|
||||||
if (bytes === 0) return "0 B";
|
if (bytes === 0) return "0 B";
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const sizes = ["B", "KB", "MB", "GB"];
|
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
||||||
}
|
}
|
||||||
|
|
@ -20,23 +20,25 @@ const regionNames =
|
||||||
? new Intl.DisplayNames([navigator.language || "en"], { type: "region" })
|
? new Intl.DisplayNames([navigator.language || "en"], { type: "region" })
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
function getCountryName(countryCode) {
|
function normalizeCode(countryCode) {
|
||||||
if (!countryCode || countryCode === "Unknown") return "Unknown";
|
if (!countryCode || countryCode === "Unknown") return null;
|
||||||
const normalized = String(countryCode).trim().toUpperCase();
|
const normalized = String(countryCode).trim().toUpperCase();
|
||||||
if (!normalized || normalized.length !== 2) return "Unknown";
|
return normalized.length === 2 ? normalized : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCountryName(countryCode) {
|
||||||
|
const normalized = normalizeCode(countryCode);
|
||||||
|
if (!normalized) return "Unknown";
|
||||||
try {
|
try {
|
||||||
const name = regionNames?.of(normalized);
|
return regionNames?.of(normalized) || normalized;
|
||||||
return name || normalized;
|
|
||||||
} catch {
|
} catch {
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFlag(countryCode) {
|
function getFlag(countryCode) {
|
||||||
if (!countryCode || countryCode === "Unknown") return "🌐";
|
const normalized = normalizeCode(countryCode);
|
||||||
const normalized = String(countryCode).trim().toUpperCase();
|
if (!normalized) return "🌐";
|
||||||
if (normalized.length !== 2) return "🌐";
|
|
||||||
const codePoints = normalized
|
const codePoints = normalized
|
||||||
.split("")
|
.split("")
|
||||||
.map((char) => 127397 + char.charCodeAt(0));
|
.map((char) => 127397 + char.charCodeAt(0));
|
||||||
|
|
@ -68,11 +70,11 @@ async function fetchLogs() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = lines
|
const rows = lines
|
||||||
.map((line) => {
|
.flatMap((line) => {
|
||||||
const match = line.match(
|
const match = line.match(
|
||||||
/(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}).*?(\d+) completed.*?↓ ([\d.]+) KB \(([\d.]+) KB\/s\).*?↑ ([\d.]+) KB \(([\d.]+) KB\/s\)/,
|
/(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}).*?(\d+) completed.*?↓ ([\d.]+) KB \(([\d.]+) KB\/s\).*?↑ ([\d.]+) KB \(([\d.]+) KB\/s\)/,
|
||||||
);
|
);
|
||||||
if (!match) return "";
|
if (!match) return [];
|
||||||
|
|
||||||
const [, timestamp, connections, dlTotal, dlRate, ulTotal, ulRate] =
|
const [, timestamp, connections, dlTotal, dlRate, ulTotal, ulRate] =
|
||||||
match;
|
match;
|
||||||
|
|
@ -90,24 +92,32 @@ async function fetchLogs() {
|
||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
});
|
});
|
||||||
|
|
||||||
return `<tr>
|
const isoDate = utcDate.toISOString();
|
||||||
<td title="${fullDate}">${time}</td>
|
return [
|
||||||
|
`<tr>
|
||||||
|
<td><time datetime="${isoDate}" aria-label="${fullDate}">${time}</time></td>
|
||||||
<td>${connections}</td>
|
<td>${connections}</td>
|
||||||
<td>${formatKB(dlTotal)} <span class="rate">(${formatKB(dlRate)}/s)</span></td>
|
<td>${formatKB(dlTotal)} <span class="rate">(${formatKB(dlRate)}/s)</span></td>
|
||||||
<td>${formatKB(ulTotal)} <span class="rate">(${formatKB(ulRate)}/s)</span></td>
|
<td>${formatKB(ulTotal)} <span class="rate">(${formatKB(ulRate)}/s)</span></td>
|
||||||
</tr>`;
|
</tr>`,
|
||||||
|
];
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
logDiv.innerHTML = `<table class="activity-table">
|
logDiv.innerHTML = `<table class="activity-table" aria-labelledby="activity-heading">
|
||||||
<tr>
|
<thead>
|
||||||
<th>Time</th>
|
<tr>
|
||||||
<th>Conns</th>
|
<th scope="col">Time</th>
|
||||||
<th>Download</th>
|
<th scope="col">Conns</th>
|
||||||
<th>Upload</th>
|
<th scope="col">Download</th>
|
||||||
</tr>
|
<th scope="col">Upload</th>
|
||||||
${rows}
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${rows}
|
||||||
|
</tbody>
|
||||||
</table>`;
|
</table>`;
|
||||||
|
pulse(logDiv);
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
document.getElementById("activity-log").innerHTML =
|
document.getElementById("activity-log").innerHTML =
|
||||||
'<div class="error">Error loading logs</div>';
|
'<div class="error">Error loading logs</div>';
|
||||||
|
|
@ -130,15 +140,26 @@ async function fetchNatType() {
|
||||||
el.textContent = capitalized;
|
el.textContent = capitalized;
|
||||||
|
|
||||||
const natLower = natType.toLowerCase();
|
const natLower = natType.toLowerCase();
|
||||||
|
let natDesc = "";
|
||||||
if (natLower === "unrestricted") {
|
if (natLower === "unrestricted") {
|
||||||
el.title =
|
natDesc =
|
||||||
"Address-independent NAT mapping. Compatible with most other NATs and can connect to restricted proxies.";
|
"Address-independent NAT mapping. Compatible with most other NATs and can connect to restricted proxies.";
|
||||||
} else if (natLower === "restricted") {
|
} else if (natLower === "restricted") {
|
||||||
el.title =
|
natDesc =
|
||||||
"Address-dependent NAT mapping. May have limited compatibility with other restricted NATs.";
|
"Address-dependent NAT mapping. May have limited compatibility with other restricted NATs.";
|
||||||
} else if (natLower === "unknown") {
|
}
|
||||||
el.title =
|
if (natDesc) {
|
||||||
"NAT type could not be determined. Probe test may have failed or not completed yet.";
|
el.title = natDesc;
|
||||||
|
const descId = "nat-type-desc";
|
||||||
|
let descEl = document.getElementById(descId);
|
||||||
|
if (!descEl) {
|
||||||
|
descEl = document.createElement("span");
|
||||||
|
descEl.id = descId;
|
||||||
|
descEl.className = "sr-only";
|
||||||
|
el.insertAdjacentElement("afterend", descEl);
|
||||||
|
}
|
||||||
|
descEl.textContent = natDesc;
|
||||||
|
el.setAttribute("aria-describedby", descId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
|
|
@ -196,31 +217,34 @@ async function fetchStats() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const attempts = total + timeouts;
|
const attempts = total + timeouts;
|
||||||
const successRate =
|
const successRate = attempts > 0 ? (total / attempts) * 100 : 0;
|
||||||
attempts > 0 ? ((total / attempts) * 100).toFixed(1) : 0;
|
|
||||||
const uptime = startTime > 0 ? Date.now() / 1000 - startTime : 0;
|
const uptime = startTime > 0 ? Date.now() / 1000 - startTime : 0;
|
||||||
|
|
||||||
document.getElementById("total").textContent = total;
|
document.getElementById("total").textContent = total;
|
||||||
document.getElementById("timeouts").textContent = timeouts;
|
document.getElementById("timeouts").textContent = timeouts;
|
||||||
const rateEl = document.getElementById("success-rate-small");
|
const rateEl = document.getElementById("success-rate-small");
|
||||||
rateEl.textContent = `${successRate}%`;
|
const statusLabel =
|
||||||
|
successRate >= 70 ? "Good" : successRate >= 40 ? "Warning" : "Critical";
|
||||||
|
rateEl.innerHTML = `${successRate.toFixed(1)}% <span class="sr-only">(${statusLabel})</span>`;
|
||||||
rateEl.className =
|
rateEl.className =
|
||||||
successRate >= 85
|
successRate >= 70
|
||||||
? "success-rate"
|
? "success-rate"
|
||||||
: successRate >= 60
|
: successRate >= 40
|
||||||
? "warn-rate"
|
? "warn-rate"
|
||||||
: "error";
|
: "error";
|
||||||
document.getElementById("tor-traffic").textContent =
|
document.getElementById("tor-traffic").textContent =
|
||||||
`${formatBytes(torInbound)} / ${formatBytes(torOutbound)}`;
|
`${formatBytes(torInbound)} / ${formatBytes(torOutbound)}`;
|
||||||
document.getElementById("memory").textContent = formatBytes(memory);
|
document.getElementById("memory").textContent = formatBytes(memory);
|
||||||
document.getElementById("uptime").textContent = formatUptime(uptime);
|
document.getElementById("uptime").textContent = formatUptime(uptime);
|
||||||
|
pulse(document.getElementById("metrics-stat"));
|
||||||
|
|
||||||
const table = document.getElementById("countries");
|
const table = document.getElementById("countries");
|
||||||
table.innerHTML = "<tr><th>Country</th><th>Count</th><th>%</th></tr>";
|
const tbody = table.querySelector("tbody");
|
||||||
|
tbody.innerHTML = "";
|
||||||
Object.entries(countries)
|
Object.entries(countries)
|
||||||
.sort((a, b) => b[1] - a[1])
|
.sort((a, b) => b[1] - a[1])
|
||||||
.forEach(([country, count]) => {
|
.forEach(([country, count]) => {
|
||||||
const row = table.insertRow();
|
const row = tbody.insertRow();
|
||||||
const displayName = getCountryName(country);
|
const displayName = getCountryName(country);
|
||||||
const percentage = total > 0 ? ((count / total) * 100).toFixed(1) : 0;
|
const percentage = total > 0 ? ((count / total) * 100).toFixed(1) : 0;
|
||||||
|
|
||||||
|
|
@ -235,11 +259,21 @@ async function fetchStats() {
|
||||||
row.insertCell(2).textContent = `${percentage}%`;
|
row.insertCell(2).textContent = `${percentage}%`;
|
||||||
});
|
});
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
document.getElementById("total").innerHTML =
|
for (const id of ["total", "timeouts", "tor-traffic", "memory", "uptime"]) {
|
||||||
'<span class="error">Error</span>';
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.innerHTML = '<span class="error">Error</span>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pulse(el) {
|
||||||
|
if (!el) return;
|
||||||
|
el.classList.remove("refreshed");
|
||||||
|
// force reflow so the animation restarts
|
||||||
|
void el.offsetWidth;
|
||||||
|
el.classList.add("refreshed");
|
||||||
|
}
|
||||||
|
|
||||||
fetchStats();
|
fetchStats();
|
||||||
fetchLogs();
|
fetchLogs();
|
||||||
fetchNatType();
|
fetchNatType();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
services:
|
services:
|
||||||
snowflake-dashboard:
|
snowflake-dashboard:
|
||||||
container_name: snowflake-dashboard
|
container_name: snowflake-dashboard
|
||||||
image: ghcr.io/lone-cloud/snowflake-dashboard:latest
|
image: git.lonecloud.dev/lone-cloud/snowflake-dashboard:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host
|
network_mode: host
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
||||||
BIN
fonts/ibm-plex-sans-latin.woff2
Normal file
BIN
fonts/ibm-plex-sans-latin.woff2
Normal file
Binary file not shown.
2
go.mod
2
go.mod
|
|
@ -1,3 +1,3 @@
|
||||||
module github.com/lone-cloud/snowflake-dashboard
|
module github.com/lone-cloud/snowflake-dashboard
|
||||||
|
|
||||||
go 1.23
|
go 1.26
|
||||||
|
|
|
||||||
113
index.html
113
index.html
|
|
@ -1,5 +1,6 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
@ -8,59 +9,85 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="favicon-dark.svg?v=VERSION" media="(prefers-color-scheme: dark)">
|
<link rel="icon" type="image/svg+xml" href="favicon-dark.svg?v=VERSION" media="(prefers-color-scheme: dark)">
|
||||||
<link rel="stylesheet" href="styles.css?v=VERSION">
|
<link rel="stylesheet" href="styles.css?v=VERSION">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="grid grid-bottom">
|
<main>
|
||||||
<div class="left-column">
|
<div class="grid grid-bottom">
|
||||||
<div class="stat">
|
<div class="left-column">
|
||||||
<div class="label">Metrics</div>
|
<div class="stat" id="metrics-stat">
|
||||||
<div class="metric-row">
|
<h2 class="label">Metrics</h2>
|
||||||
<span title="Percentage of successful connections vs total attempts">Success Rate</span>
|
<div class="metric-row">
|
||||||
<span class="success-rate" id="success-rate-small">-</span>
|
<span tabindex="0" role="term" class="metric-label" aria-describedby="tip-success-rate" title="Percentage of successful connections vs total attempts">Success
|
||||||
|
Rate</span>
|
||||||
|
<span id="tip-success-rate" class="sr-only">Percentage of successful connections vs total
|
||||||
|
attempts</span>
|
||||||
|
<span role="definition" class="success-rate" id="success-rate-small">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-row">
|
||||||
|
<span tabindex="0" role="term" class="metric-label" aria-describedby="tip-uptime" title="How long the Snowflake proxy has been running">Uptime</span>
|
||||||
|
<span id="tip-uptime" class="sr-only">How long the Snowflake proxy has been running</span>
|
||||||
|
<span role="definition" id="uptime">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-row">
|
||||||
|
<span tabindex="0" role="term" class="metric-label" aria-describedby="tip-relay" title="Data relayed from Tor to clients / from clients to Tor">Relay ↓ /
|
||||||
|
↑</span>
|
||||||
|
<span id="tip-relay" class="sr-only">Data relayed from Tor to clients / from clients to
|
||||||
|
Tor</span>
|
||||||
|
<span role="definition" id="tor-traffic">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-row" id="nat-row" hidden>
|
||||||
|
<span tabindex="0" role="term" class="metric-label" aria-describedby="tip-nat" title="Your network's NAT configuration - determines proxy compatibility">NAT Type</span>
|
||||||
|
<span id="tip-nat" class="sr-only">Your network's NAT configuration - determines proxy
|
||||||
|
compatibility</span>
|
||||||
|
<span role="definition" id="nat-type">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-row">
|
||||||
|
<span tabindex="0" role="term" class="metric-label"
|
||||||
|
aria-describedby="tip-connections" title="Successful client connections completed">Connections</span>
|
||||||
|
<span id="tip-connections" class="sr-only">Successful client connections completed</span>
|
||||||
|
<span role="definition" id="total">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-row">
|
||||||
|
<span tabindex="0" role="term" class="metric-label"
|
||||||
|
aria-describedby="tip-timeouts" title="Connection attempts that failed or timed out">Timeouts</span>
|
||||||
|
<span id="tip-timeouts" class="sr-only">Connection attempts that failed or timed out</span>
|
||||||
|
<span role="definition" id="timeouts">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-row">
|
||||||
|
<span tabindex="0" role="term" class="metric-label" aria-describedby="tip-memory" title="Current memory consumption of the proxy process">Memory
|
||||||
|
Usage</span>
|
||||||
|
<span id="tip-memory" class="sr-only">Current memory consumption of the proxy process</span>
|
||||||
|
<span role="definition" id="memory">-</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-row">
|
|
||||||
<span title="How long the Snowflake proxy has been running">Uptime</span>
|
<div class="stat">
|
||||||
<span id="uptime">-</span>
|
<h2 class="label" id="countries-heading">Connections by Country</h2>
|
||||||
</div>
|
<div class="table-scroll">
|
||||||
<div class="metric-row">
|
<table id="countries" aria-labelledby="countries-heading">
|
||||||
<span title="Data relayed from Tor to clients / from clients to Tor">Relay ↓ / ↑</span>
|
<thead>
|
||||||
<span id="tor-traffic">-</span>
|
<tr>
|
||||||
</div>
|
<th scope="col">Country</th>
|
||||||
<div class="metric-row" id="nat-row" hidden>
|
<th scope="col">Count</th>
|
||||||
<span title="Your network's NAT configuration - determines proxy compatibility">NAT Type</span>
|
<th scope="col">%</th>
|
||||||
<span id="nat-type">-</span>
|
</tr>
|
||||||
</div>
|
</thead>
|
||||||
<div class="metric-row">
|
<tbody></tbody>
|
||||||
<span title="Successful client connections completed">Connections</span>
|
</table>
|
||||||
<span id="total">-</span>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="metric-row">
|
|
||||||
<span title="Connection attempts that failed or timed out">Timeouts</span>
|
|
||||||
<span id="timeouts">-</span>
|
|
||||||
</div>
|
|
||||||
<div class="metric-row">
|
|
||||||
<span title="Current memory consumption of the proxy process">Memory Usage</span>
|
|
||||||
<span id="memory">-</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="label">Connections by Country</div>
|
<h2 class="label" id="activity-heading">Recent Activity (Hourly)</h2>
|
||||||
<div class="table-scroll">
|
<div id="activity-log" aria-live="polite" aria-atomic="false">
|
||||||
<table id="countries">
|
<div class="loading">Loading...</div>
|
||||||
<tr><th>Country</th><th>Count</th><th>%</th></tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
<div class="stat">
|
|
||||||
<div class="label">Recent Activity (Hourly)</div>
|
|
||||||
<div id="activity-log">
|
|
||||||
<div class="loading">Loading...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="dashboard.js?v=VERSION"></script>
|
<script src="dashboard.js?v=VERSION"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
188
styles.css
188
styles.css
|
|
@ -1,40 +1,54 @@
|
||||||
:root {
|
:root {
|
||||||
--bg-primary: #0d1117;
|
--bg-primary: oklch(11% 0.013 305);
|
||||||
--bg-secondary: #161b22;
|
--bg-secondary: oklch(20% 0.015 305);
|
||||||
--bg-hover: #1c2128;
|
--bg-hover: oklch(24% 0.015 305);
|
||||||
--text-primary: #c9d1d9;
|
--text-primary: oklch(87% 0.009 305);
|
||||||
--text-secondary: #a8b3c0;
|
--text-secondary: oklch(65% 0.011 305);
|
||||||
--border-color: #30363d;
|
--border-color: oklch(24% 0.017 305);
|
||||||
--accent-color: #58a6ff;
|
--accent-color: oklch(72% 0.18 305);
|
||||||
--success-color: #3fb950;
|
--success-color: oklch(66% 0.19 145);
|
||||||
--warn-color: #d29922;
|
--warn-color: oklch(71% 0.16 73);
|
||||||
--error-color: #f85149;
|
--error-color: oklch(61% 0.22 22);
|
||||||
--bottom-grid-height: clamp(80vh, 95vh, 100vh);
|
--bottom-grid-height: clamp(80vh, 95vh, 100vh);
|
||||||
--scrollbar-track: rgba(255, 255, 255, 0.04);
|
--scrollbar-track: color-mix(in oklch, var(--text-primary) 8%, transparent);
|
||||||
--scrollbar-thumb: rgba(201, 209, 217, 0.22);
|
--scrollbar-thumb: color-mix(in oklch, var(--text-primary) 22%, transparent);
|
||||||
--scrollbar-thumb-hover: rgba(201, 209, 217, 0.35);
|
--scrollbar-thumb-hover: color-mix(
|
||||||
|
in oklch,
|
||||||
|
var(--text-primary) 35%,
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
--font-body: "IBM Plex Sans", system-ui, -apple-system, sans-serif;
|
||||||
|
--text-xs: 0.6875rem;
|
||||||
|
--text-sm: 0.8125rem;
|
||||||
|
--text-base: 0.9375rem;
|
||||||
|
--text-md: 1.125rem;
|
||||||
|
--text-lg: 1.75rem;
|
||||||
|
--space-1: 0.25rem;
|
||||||
|
--space-2: 0.5rem;
|
||||||
|
--space-3: 0.75rem;
|
||||||
|
--space-4: 1rem;
|
||||||
|
--space-6: 1.5rem;
|
||||||
|
--space-8: 2rem;
|
||||||
|
--space-12: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
:root {
|
:root {
|
||||||
--bg-primary: #ffffff;
|
--bg-primary: oklch(97.5% 0.007 305);
|
||||||
--bg-secondary: #f6f8fa;
|
--bg-secondary: oklch(94% 0.009 305);
|
||||||
--bg-hover: #f0f3f6;
|
--bg-hover: oklch(91% 0.011 305);
|
||||||
--text-primary: #24292f;
|
--text-primary: oklch(17% 0.013 305);
|
||||||
--text-secondary: #57606a;
|
--text-secondary: oklch(40% 0.013 305);
|
||||||
--border-color: #d0d7de;
|
--border-color: oklch(84% 0.014 305);
|
||||||
--accent-color: #0969da;
|
--accent-color: oklch(38% 0.18 305);
|
||||||
--success-color: #1a7f37;
|
--success-color: oklch(40% 0.18 145);
|
||||||
--warn-color: #9a6700;
|
--warn-color: oklch(46% 0.16 73);
|
||||||
--error-color: #cf222e;
|
--error-color: oklch(44% 0.22 22);
|
||||||
--scrollbar-track: rgba(0, 0, 0, 0.04);
|
|
||||||
--scrollbar-thumb: rgba(36, 41, 47, 0.18);
|
|
||||||
--scrollbar-thumb-hover: rgba(36, 41, 47, 0.3);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
|
font-family: var(--font-body);
|
||||||
max-width: 75rem;
|
max-width: 75rem;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|
@ -42,6 +56,34 @@ body {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
padding-bottom: 0.625rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent-color);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
@ -62,35 +104,44 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
|
font-family: var(--font-body);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 1rem;
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.0625em;
|
letter-spacing: 0.08em;
|
||||||
margin-bottom: 0.9375rem;
|
margin: 0 0 var(--space-3) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-row {
|
.metric-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(auto, 11.25rem) auto;
|
grid-template-columns: minmax(auto, 11.25rem) auto;
|
||||||
gap: 7.5rem;
|
gap: clamp(var(--space-4), 6vw, 7.5rem);
|
||||||
padding: 0.5rem 0;
|
min-height: 1.875rem;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.125rem 0;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-row > span:first-child {
|
.metric-label {
|
||||||
|
font: inherit;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 0.8125rem;
|
font-size: var(--text-sm);
|
||||||
text-transform: uppercase;
|
align-self: stretch;
|
||||||
letter-spacing: 0.03125em;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
[title] {
|
.metric-label:focus-visible {
|
||||||
cursor: help;
|
outline: 2px solid var(--accent-color);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-row > span:last-child {
|
.metric-row > [role="definition"] {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.9375rem;
|
font-size: var(--text-base);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-row:last-child {
|
.metric-row:last-child {
|
||||||
|
|
@ -151,18 +202,21 @@ table {
|
||||||
|
|
||||||
th,
|
th,
|
||||||
td {
|
td {
|
||||||
padding: 0.75rem;
|
padding: var(--space-3);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
font-size: 0.9375rem;
|
font-size: var(--text-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
font-size: var(--text-xs);
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tr:hover {
|
tr:hover {
|
||||||
|
|
@ -187,7 +241,7 @@ tr:last-child td {
|
||||||
|
|
||||||
.flag {
|
.flag {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
margin-right: 0.5rem;
|
margin-right: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-column {
|
.left-column {
|
||||||
|
|
@ -197,7 +251,7 @@ tr:last-child td {
|
||||||
}
|
}
|
||||||
|
|
||||||
#activity-log {
|
#activity-log {
|
||||||
font-size: 0.8125rem;
|
font-size: var(--text-sm);
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,13 +265,13 @@ tr:last-child td {
|
||||||
|
|
||||||
.activity-table th,
|
.activity-table th,
|
||||||
.activity-table td {
|
.activity-table td {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
|
font-family: var(--font-body);
|
||||||
}
|
}
|
||||||
|
|
||||||
.activity-table td {
|
.activity-table td {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.9375rem;
|
font-size: var(--text-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.activity-table td:first-child {
|
.activity-table td:first-child {
|
||||||
|
|
@ -227,12 +281,22 @@ tr:last-child td {
|
||||||
|
|
||||||
.activity-table .rate {
|
.activity-table .rate {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 0.75rem;
|
font-size: var(--text-xs);
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.activity-table tr:nth-child(even) {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
background: var(--bg-hover);
|
@keyframes data-refresh {
|
||||||
|
from {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.refreshed {
|
||||||
|
animation: data-refresh 350ms cubic-bezier(0.25, 0, 0, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 769px) {
|
@media (min-width: 769px) {
|
||||||
|
|
@ -294,7 +358,7 @@ tr:last-child td {
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-column {
|
.left-column {
|
||||||
gap: 0.625rem;
|
gap: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-bottom {
|
.grid-bottom {
|
||||||
|
|
@ -320,30 +384,29 @@ tr:last-child td {
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat {
|
.stat {
|
||||||
padding: 0.5rem 0.75rem;
|
padding: var(--space-2) var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-size: 0.9375rem;
|
font-size: var(--text-base);
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
th,
|
th,
|
||||||
td {
|
td {
|
||||||
padding: 0.5rem 0.25rem;
|
padding: var(--space-2) var(--space-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#countries td:nth-child(3) {
|
#countries td:nth-child(3) {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 0.8125rem;
|
font-size: var(--text-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
#countries td,
|
#countries td,
|
||||||
.activity-table td {
|
.activity-table td {
|
||||||
font-size: 0.9375rem;
|
font-size: var(--text-base);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-family:
|
font-family: var(--font-body);
|
||||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.activity-table td:first-child {
|
.activity-table td:first-child {
|
||||||
|
|
@ -351,11 +414,10 @@ tr:last-child td {
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-row {
|
.metric-row {
|
||||||
flex-wrap: nowrap;
|
gap: var(--space-4);
|
||||||
gap: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-row > span:last-child {
|
.metric-row > [role="definition"] {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue