Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions docs-site/dist/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,16 @@ function statusBadge(statusValue) {
[STATUS_MODEL.FAILED]: 'border-rose-500/40 bg-rose-500/10 text-rose-300',
[STATUS_MODEL.UNTESTED]: 'border-slate-500/40 bg-slate-500/10 text-slate-300'
};
const label = String(statusValue || STATUS_MODEL.UNTESTED).replace(/-/g, ' ');
return `<span class="inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium ${tone[statusValue] || tone[STATUS_MODEL.UNTESTED]}">${label}</span>`;
const labelsFa = {
[STATUS_MODEL.OK]: 'سالم',
[STATUS_MODEL.SLOW]: 'کند',
[STATUS_MODEL.BLOCKED_BY_BROWSER]: 'محدودیت مرورگر',
[STATUS_MODEL.FAILED]: 'ناموفق',
[STATUS_MODEL.UNTESTED]: 'بررسی‌نشده'
};
const rawLabel = String(statusValue || STATUS_MODEL.UNTESTED);
const label = labelsFa[rawLabel] || labelsFa[STATUS_MODEL.UNTESTED];
return `<span title="${rawLabel}" aria-label="${rawLabel}" class="inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium ${tone[statusValue] || tone[STATUS_MODEL.UNTESTED]}">${label}</span>`;
}

function getRegionBadge(raw = {}) {
Expand All @@ -54,9 +62,9 @@ function getRegionBadge(raw = {}) {
const alpha2 = region.length === 2 ? region.toLowerCase() : '';
const allowedFlags = new Set(['ir', 'jp', 'fr', 'cn', 'nl', 'us', 'ru', 'de', 'gb']);
if (alpha2 && allowedFlags.has(alpha2)) {
return `<span class="inline-flex items-center gap-1 rounded-full border border-slate-700 px-2 py-0.5 text-[11px] text-slate-300"><span class="fi fi-${alpha2}"></span>${alpha2.toUpperCase()}</span>`;
return `<span class="technical inline-flex items-center gap-1 rounded-full border border-slate-700 px-2 py-0.5 text-[11px] text-slate-300"><span class="fi fi-${alpha2}"></span>${alpha2.toUpperCase()}</span>`;
}
return `<span class="inline-flex items-center rounded-full border border-slate-700 px-2 py-0.5 text-[11px] text-slate-300">${region}</span>`;
return `<span class="technical inline-flex items-center rounded-full border border-slate-700 px-2 py-0.5 text-[11px] text-slate-300">${region}</span>`;
}

function normalizeMirrorData(rawData) { /* unchanged logic */
Expand Down Expand Up @@ -110,7 +118,7 @@ function renderSummary() { const rows = state.results; const okOrSlow = rows.fil

function renderFilters() { byId('filters').innerHTML = ['all', ...state.ecosystems].map((category) => { const icon = getEcosystemIcon(category); return `<button data-cat="${category}" class="inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs ${state.activeCategory === category ? 'border-primary bg-primary/20' : 'border-slate-700'}">${icon ? `<img src="${icon}" alt="" class="h-4 w-4">` : ''}<span>${category}</span></button>`; }).join(''); byId('filters').querySelectorAll('button').forEach((btn) => { btn.onclick = () => { state.activeCategory = btn.dataset.cat; renderRows(); }; }); }

function renderRows() { const filtered = state.results.filter((r) => state.activeCategory === 'all' || r.ecosystem === state.activeCategory); byId('latencyRows').innerHTML = filtered.map((r) => { const icon = getEcosystemIcon(r.ecosystem); const region = getRegionBadge(r.raw); return `<article class="rounded-lg border border-slate-800 bg-surface/70 p-2.5"><div class="flex items-start justify-between gap-3"><div class="min-w-0"><p class="flex items-center gap-2 font-semibold text-sm">${icon ? `<img src="${icon}" alt="" class="h-4 w-4">` : ''}<span class="truncate">${r.name}</span></p><p class="mt-0.5 text-[11px] text-slate-400">${r.ecosystem}</p><p class="mt-1 truncate text-xs text-slate-300">${r.url}</p></div><div class="shrink-0 text-right">${statusBadge(r.status)}${Number.isFinite(r.latency) ? `<p class="mt-1 text-[11px] text-slate-400">${r.latency}ms</p>` : ''}${region ? `<div class="mt-1">${region}</div>` : ''}</div></div></article>`; }).join(''); }
function renderRows() { const filtered = state.results.filter((r) => state.activeCategory === 'all' || r.ecosystem === state.activeCategory); byId('latencyRows').innerHTML = filtered.map((r) => { const icon = getEcosystemIcon(r.ecosystem); const region = getRegionBadge(r.raw); return `<article class="rounded-lg border border-slate-800 bg-surface/70 p-2.5"><div class="flex items-start justify-between gap-3"><div class="min-w-0"><p class="flex items-center gap-2 font-semibold text-sm">${icon ? `<img src="${icon}" alt="" class="h-4 w-4">` : ''}<span class="truncate">${r.name}</span></p><p class="technical mt-0.5 text-[11px] text-slate-400">${r.ecosystem}</p><p class="technical mt-1 truncate text-xs text-slate-300">${r.url}</p></div><div class="shrink-0 text-right">${statusBadge(r.status)}${Number.isFinite(r.latency) ? `<p class="mt-1 text-[11px] text-slate-400">${r.latency}ms</p>` : ''}${region ? `<div class="mt-1">${region}</div>` : ''}</div></div></article>`; }).join(''); }

function optimizeDockerfile(input) { const lines = input.split(/\r?\n/); const out = []; const inserted = []; let baseImage = null; for (const line of lines) { if (!baseImage && /^\s*FROM\s+(.+)/i.test(line)) baseImage = line.match(/^\s*FROM\s+(.+)/i)[1].trim(); out.push(line);} if (baseImage) { const argLines = ['ARG NPM_CONFIG_REGISTRY', 'ARG PIP_INDEX_URL', 'ARG APT_UBUNTU_MIRROR', 'ARG APT_DEBIAN_MIRROR']; const firstFrom = out.findIndex((l) => /^\s*FROM\s+/i.test(l)); argLines.forEach((arg) => { if (!out.some((l) => l.trim() === arg)) { out.splice(firstFrom + 1, 0, arg); inserted.push(arg); } }); } return { text: out.join('\n'), summary: { baseImage: baseImage || 'not detected', insertedArgs: inserted, warnings: baseImage ? [] : ['No FROM line detected; no ARG lines inserted.'] } }; }

Expand Down
142 changes: 71 additions & 71 deletions docs-site/dist/assets/icons/maven.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs-site/dist/assets/tailwind.css

Large diffs are not rendered by default.

27 changes: 14 additions & 13 deletions docs-site/dist/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en" class="dark">
<html lang="fa" dir="rtl" class="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
Expand All @@ -11,42 +11,43 @@
<main class="mx-auto max-w-7xl p-4 md:p-5 space-y-3">
<header class="rounded-xl border border-slate-800 bg-surface/70 p-3 md:p-4">
<h1 class="font-brand text-3xl md:text-5xl leading-none tracking-wide">MirrorToolkit</h1>
<p class="mt-2 text-slate-300">Check mirrors, copy env snippets, and generate a mirror-aware Dockerfile.</p>
<p class="mt-2 text-slate-300">بررسی سریع میرورها، تولید تنظیمات محیطی، و ساخت Dockerfile سازگار با میرورهای سریع‌تر.</p>
<p class="mt-1 text-xs text-slate-400 ltr technical">Check mirrors → review results → paste Dockerfile → copy optimized output</p>
<div class="mt-2 flex flex-wrap gap-1.5 text-[11px]">
<span class="rounded-full border border-cyan-500/40 bg-cyan-500/10 px-3 py-1">CLI-first</span><span class="rounded-full border border-cyan-500/40 bg-cyan-500/10 px-3 py-1">Static GitHub Pages</span><span class="rounded-full border border-cyan-500/40 bg-cyan-500/10 px-3 py-1">Dockerfile Optimizer</span><span class="rounded-full border border-cyan-500/40 bg-cyan-500/10 px-3 py-1">Offline-friendly</span><span class="rounded-full border border-cyan-500/40 bg-cyan-500/10 px-3 py-1">No backend</span>
</div>
</header>

<section class="sticky top-2 z-20 rounded-xl border border-slate-800/90 bg-slate-900/90 p-2.5 backdrop-blur">
<div class="flex flex-wrap gap-2">
<button id="runBenchmark" class="rounded-lg bg-primary px-3 py-2 text-sm font-semibold text-slate-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary">Run benchmark</button>
<button id="uploadMirrorsBtn" class="rounded-lg bg-slate-700 px-3 py-2 text-sm">Upload mirrors.json</button>
<button id="copyEnvBtn" class="rounded-lg bg-slate-700 px-3 py-2 text-sm">Copy env</button>
<button id="downloadReportBtn" class="rounded-lg bg-slate-700 px-3 py-2 text-sm">Download report</button>
<label class="rounded-lg bg-slate-700 px-3 py-2 text-sm cursor-pointer">Upload Dockerfile<input id="dockerUpload" type="file" class="hidden" aria-label="Upload Dockerfile"></label>
<button id="runBenchmark" class="rounded-lg bg-primary px-3 py-2 text-sm font-semibold text-slate-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary">شروع بررسی میرورها</button>
<button id="uploadMirrorsBtn" class="rounded-lg bg-slate-700 px-3 py-2 text-sm">بارگذاری mirrors.json</button>
<button id="copyEnvBtn" class="rounded-lg bg-slate-700 px-3 py-2 text-sm">کپی تنظیمات env</button>
<button id="downloadReportBtn" class="rounded-lg bg-slate-700 px-3 py-2 text-sm">دانلود گزارش</button>
<label class="rounded-lg bg-slate-700 px-3 py-2 text-sm cursor-pointer">بارگذاری Dockerfile<input id="dockerUpload" type="file" class="hidden" aria-label="بارگذاری Dockerfile"></label>
</div>
<p id="statusMessage" role="status" class="mt-2 text-sm text-slate-300"></p>
<ol id="stepStrip" class="mt-2 flex flex-wrap gap-1.5"></ol>
</section>

<input id="mirrorsUpload" type="file" class="hidden" accept="application/json">
<section id="uploadFallback" class="hidden rounded-xl border border-dashed border-warning/60 bg-warning/10 p-6 text-center">
<h2 class="text-lg font-semibold">mirrors.json not found</h2><p class="mt-2 text-sm text-slate-300">Upload mirrors.json with {"ecosystems":{"name":{"mirrors":[]}}} to continue immediately.</p>
<h2 class="text-lg font-semibold">mirrors.json یافت نشد</h2><p class="mt-2 text-sm text-slate-300">بارگذاری mirrors.json با ساختار {"ecosystems":{"name":{"mirrors":[]}}} to continue immediately.</p>
</section>

<section id="summaryCards" class="grid grid-cols-3 gap-2 md:grid-cols-6"></section>
<section id="filters" class="flex flex-wrap gap-2"></section>
<section id="latencyRows" class="space-y-2"></section>
<section class="rounded-xl border border-slate-800 bg-surface/70 p-3">
<p class="text-xs text-warning">Browser benchmark is approximate because registry CORS policies vary. CLI benchmark remains authoritative.</p>
<textarea id="envOutput" readonly class="mt-2 h-44 w-full rounded-lg border border-slate-700 bg-slate-950 p-3 font-mono text-xs"></textarea>
<textarea id="envOutput" dir="ltr" readonly class="technical mt-2 h-44 w-full rounded-lg border border-slate-700 bg-slate-950 p-3 font-mono text-xs"></textarea>
</section>
<section class="rounded-xl border border-slate-800 bg-surface/70 p-3 space-y-2">
<h2 class="font-semibold">Dockerfile optimizer</h2>
<textarea id="dockerInput" class="h-48 w-full rounded-lg border border-slate-700 bg-slate-950 p-3 font-mono text-xs" placeholder="Paste Dockerfile"></textarea>
<div><button id="optimizeDocker" class="rounded-lg bg-success px-3 py-2 text-sm font-semibold text-slate-900">Optimize</button> <button id="copyDocker" class="rounded-lg bg-slate-700 px-3 py-2 text-sm">Copy optimized Dockerfile</button> <button id="downloadDocker" class="rounded-lg bg-slate-700 px-3 py-2 text-sm">Download optimized Dockerfile</button></div>
<h2 class="font-semibold">بهینه‌ساز Dockerfile</h2>
<textarea id="dockerInput" dir="ltr" class="technical h-48 w-full rounded-lg border border-slate-700 bg-slate-950 p-3 font-mono text-xs" placeholder="Paste Dockerfile"></textarea>
<div><button id="optimizeDocker" class="rounded-lg bg-success px-3 py-2 text-sm font-semibold text-slate-900">بهینه‌سازی</button> <button id="copyDocker" class="rounded-lg bg-slate-700 px-3 py-2 text-sm">کپی Dockerfile خروجی</button> <button id="downloadDocker" class="rounded-lg bg-slate-700 px-3 py-2 text-sm">دانلود Dockerfile خروجی</button></div>
<div id="dockerSummary" class="text-xs text-slate-300 space-y-1"></div>
<pre id="dockerOutput" class="max-h-80 overflow-auto rounded-lg border border-slate-700 bg-slate-950 p-3 text-xs"></pre>
<pre id="dockerOutput" dir="ltr" class="technical max-h-80 overflow-auto rounded-lg border border-slate-700 bg-slate-950 p-3 text-xs"></pre>
</section>
</main>
<script src="./app.js" defer></script>
Expand Down
4 changes: 3 additions & 1 deletion docs-site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"clean": "node build.mjs --clean",
"build:css": "tailwindcss -i ./src/input.css -o ./dist/assets/tailwind.css --minify",
"build": "node build.mjs",
"watch": "tailwindcss -i ./src/input.css -o ./dist/assets/tailwind.css --watch"
"watch": "tailwindcss -i ./src/input.css -o ./dist/assets/tailwind.css --watch",
"check:ui-contract": "node scripts/check-ui-contract.mjs",
"test": "npm run build && npm run check:ui-contract"
},
"devDependencies": {
"autoprefixer": "^10.4.20",
Expand Down
29 changes: 29 additions & 0 deletions docs-site/scripts/check-ui-contract.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { readFile } from 'node:fs/promises';
import path from 'node:path';

const root = new URL('..', import.meta.url).pathname;
const read = (p) => readFile(path.join(root, p), 'utf8');

const failures = [];
const indexHtml = await read('src/index.html');
const inputCss = await read('src/input.css');
const appJs = await read('src/app.js');

if (!/html[^>]*\blang=["']fa["']/i.test(indexHtml)) failures.push('index.html must include lang="fa" on <html>.');
if (!/html[^>]*\bdir=["']rtl["']/i.test(indexHtml)) failures.push('index.html must include dir="rtl" on <html>.');
if (!/href=["'][^"']*assets\/vendor\/flag-icons\/flag-icons\.min\.css["']/.test(indexHtml)) failures.push('index.html must include local flag-icons stylesheet.');
if (!/\.font-brand\b/.test(inputCss)) failures.push('input.css must define .font-brand.');
if (!/\.technical\b/.test(inputCss)) failures.push('input.css must define .technical.');
if (!/function\s+getRegionBadge\s*\(/.test(appJs)) failures.push('app.js must contain getRegionBadge().');
for (const label of ['سالم', 'کند', 'محدودیت مرورگر', 'ناموفق', 'بررسی\u200cنشده']) {
if (!appJs.includes(label)) failures.push(`app.js must contain Persian status label: ${label}`);
}
if (/https:\/\/(?:cdn|unpkg|jsdelivr|fonts\.googleapis)/i.test(indexHtml)) failures.push('index.html contains forbidden CDN URL.');

if (failures.length) {
console.error('UI identity contract check failed:');
failures.forEach((msg, i) => console.error(`${i + 1}. ${msg}`));
process.exit(1);
}

console.log('UI identity contract check passed.');
18 changes: 13 additions & 5 deletions docs-site/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,16 @@ function statusBadge(statusValue) {
[STATUS_MODEL.FAILED]: 'border-rose-500/40 bg-rose-500/10 text-rose-300',
[STATUS_MODEL.UNTESTED]: 'border-slate-500/40 bg-slate-500/10 text-slate-300'
};
const label = String(statusValue || STATUS_MODEL.UNTESTED).replace(/-/g, ' ');
return `<span class="inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium ${tone[statusValue] || tone[STATUS_MODEL.UNTESTED]}">${label}</span>`;
const labelsFa = {
[STATUS_MODEL.OK]: 'سالم',
[STATUS_MODEL.SLOW]: 'کند',
[STATUS_MODEL.BLOCKED_BY_BROWSER]: 'محدودیت مرورگر',
[STATUS_MODEL.FAILED]: 'ناموفق',
[STATUS_MODEL.UNTESTED]: 'بررسی‌نشده'
};
const rawLabel = String(statusValue || STATUS_MODEL.UNTESTED);
const label = labelsFa[rawLabel] || labelsFa[STATUS_MODEL.UNTESTED];
return `<span title="${rawLabel}" aria-label="${rawLabel}" class="inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium ${tone[statusValue] || tone[STATUS_MODEL.UNTESTED]}">${label}</span>`;
}

function getRegionBadge(raw = {}) {
Expand All @@ -54,9 +62,9 @@ function getRegionBadge(raw = {}) {
const alpha2 = region.length === 2 ? region.toLowerCase() : '';
const allowedFlags = new Set(['ir', 'jp', 'fr', 'cn', 'nl', 'us', 'ru', 'de', 'gb']);
if (alpha2 && allowedFlags.has(alpha2)) {
return `<span class="inline-flex items-center gap-1 rounded-full border border-slate-700 px-2 py-0.5 text-[11px] text-slate-300"><span class="fi fi-${alpha2}"></span>${alpha2.toUpperCase()}</span>`;
return `<span class="technical inline-flex items-center gap-1 rounded-full border border-slate-700 px-2 py-0.5 text-[11px] text-slate-300"><span class="fi fi-${alpha2}"></span>${alpha2.toUpperCase()}</span>`;
}
return `<span class="inline-flex items-center rounded-full border border-slate-700 px-2 py-0.5 text-[11px] text-slate-300">${region}</span>`;
return `<span class="technical inline-flex items-center rounded-full border border-slate-700 px-2 py-0.5 text-[11px] text-slate-300">${region}</span>`;
}

function normalizeMirrorData(rawData) { /* unchanged logic */
Expand Down Expand Up @@ -110,7 +118,7 @@ function renderSummary() { const rows = state.results; const okOrSlow = rows.fil

function renderFilters() { byId('filters').innerHTML = ['all', ...state.ecosystems].map((category) => { const icon = getEcosystemIcon(category); return `<button data-cat="${category}" class="inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs ${state.activeCategory === category ? 'border-primary bg-primary/20' : 'border-slate-700'}">${icon ? `<img src="${icon}" alt="" class="h-4 w-4">` : ''}<span>${category}</span></button>`; }).join(''); byId('filters').querySelectorAll('button').forEach((btn) => { btn.onclick = () => { state.activeCategory = btn.dataset.cat; renderRows(); }; }); }

function renderRows() { const filtered = state.results.filter((r) => state.activeCategory === 'all' || r.ecosystem === state.activeCategory); byId('latencyRows').innerHTML = filtered.map((r) => { const icon = getEcosystemIcon(r.ecosystem); const region = getRegionBadge(r.raw); return `<article class="rounded-lg border border-slate-800 bg-surface/70 p-2.5"><div class="flex items-start justify-between gap-3"><div class="min-w-0"><p class="flex items-center gap-2 font-semibold text-sm">${icon ? `<img src="${icon}" alt="" class="h-4 w-4">` : ''}<span class="truncate">${r.name}</span></p><p class="mt-0.5 text-[11px] text-slate-400">${r.ecosystem}</p><p class="mt-1 truncate text-xs text-slate-300">${r.url}</p></div><div class="shrink-0 text-right">${statusBadge(r.status)}${Number.isFinite(r.latency) ? `<p class="mt-1 text-[11px] text-slate-400">${r.latency}ms</p>` : ''}${region ? `<div class="mt-1">${region}</div>` : ''}</div></div></article>`; }).join(''); }
function renderRows() { const filtered = state.results.filter((r) => state.activeCategory === 'all' || r.ecosystem === state.activeCategory); byId('latencyRows').innerHTML = filtered.map((r) => { const icon = getEcosystemIcon(r.ecosystem); const region = getRegionBadge(r.raw); return `<article class="rounded-lg border border-slate-800 bg-surface/70 p-2.5"><div class="flex items-start justify-between gap-3"><div class="min-w-0"><p class="flex items-center gap-2 font-semibold text-sm">${icon ? `<img src="${icon}" alt="" class="h-4 w-4">` : ''}<span class="truncate">${r.name}</span></p><p class="technical mt-0.5 text-[11px] text-slate-400">${r.ecosystem}</p><p class="technical mt-1 truncate text-xs text-slate-300">${r.url}</p></div><div class="shrink-0 text-right">${statusBadge(r.status)}${Number.isFinite(r.latency) ? `<p class="mt-1 text-[11px] text-slate-400">${r.latency}ms</p>` : ''}${region ? `<div class="mt-1">${region}</div>` : ''}</div></div></article>`; }).join(''); }

function optimizeDockerfile(input) { const lines = input.split(/\r?\n/); const out = []; const inserted = []; let baseImage = null; for (const line of lines) { if (!baseImage && /^\s*FROM\s+(.+)/i.test(line)) baseImage = line.match(/^\s*FROM\s+(.+)/i)[1].trim(); out.push(line);} if (baseImage) { const argLines = ['ARG NPM_CONFIG_REGISTRY', 'ARG PIP_INDEX_URL', 'ARG APT_UBUNTU_MIRROR', 'ARG APT_DEBIAN_MIRROR']; const firstFrom = out.findIndex((l) => /^\s*FROM\s+/i.test(l)); argLines.forEach((arg) => { if (!out.some((l) => l.trim() === arg)) { out.splice(firstFrom + 1, 0, arg); inserted.push(arg); } }); } return { text: out.join('\n'), summary: { baseImage: baseImage || 'not detected', insertedArgs: inserted, warnings: baseImage ? [] : ['No FROM line detected; no ARG lines inserted.'] } }; }

Expand Down
Loading
Loading