From 1bde9c9078ecb93109318a383a90ffccab541fcc Mon Sep 17 00:00:00 2001 From: devnull Date: Mon, 5 Jan 2026 15:31:07 +0000 Subject: [PATCH 1/7] chore: add downloaded homepage snapshot --- downloaded_homepage.html | 221 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 downloaded_homepage.html diff --git a/downloaded_homepage.html b/downloaded_homepage.html new file mode 100644 index 000000000..a4acd5757 --- /dev/null +++ b/downloaded_homepage.html @@ -0,0 +1,221 @@ + + + + + + Финансовый Дашборд + + + + + 🌕 +
+
+

💰 Финансовый Дашборд

+ +
+ +
+ Главное фото
+ +
+ +
+
+
💳
+
+

Общий баланс

+

₽ 125,450

+ +12.5% за месяц +
+
+ +
+
📈
+
+

Доходы

+

₽ 85,000

+ +8.2% за месяц +
+
+ +
+
📉
+
+

Расходы

+

₽ 42,300

+ -5.1% за месяц +
+
+ +
+
🏦
+
+

Накопления

+

₽ 82,750

+ +15.3% за месяц +
+
+
+ + +
+
+

Динамика финансов

+ +
+ +
+

Распределение расходов

+ +
+
+ + +
+
+

Последние транзакции

+ +
+
+ +
+
+ + +
+

Бюджет на месяц

+
+
+
+ Продукты + ₽ 15,000 / ₽ 20,000 +
+
+
+
+
+
+
+ Транспорт + ₽ 3,500 / ₽ 5,000 +
+
+
+
+
+
+
+ Развлечения + ₽ 8,200 / ₽ 10,000 +
+
+
+
+
+
+
+ Здоровье + ₽ 5,600 / ₽ 8,000 +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + From 04343db6b718cb689763596247ee53e63bcfa713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC?= Date: Mon, 5 Jan 2026 15:34:03 +0000 Subject: [PATCH 2/7] fix(demo): prevent XSS by avoiding innerHTML and mask demo credentials; add link-check workflow --- .github/workflows/check-links.yml | 33 ++++++ script.js | 175 ++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 .github/workflows/check-links.yml create mode 100644 script.js diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml new file mode 100644 index 000000000..289ecf09e --- /dev/null +++ b/.github/workflows/check-links.yml @@ -0,0 +1,33 @@ +name: Check external links + +on: + pull_request: + push: + branches: + - main + - 'add/*' + +jobs: + linkcheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Gather external URLs + run: | + # find http(s) links in repo files and dedupe + grep -RhoE "https?://[^")'\s]+" . | sort -u > /tmp/links.txt || true + echo "Found $(wc -l < /tmp/links.txt || echo 0) links" + cat /tmp/links.txt + - name: Check links + run: | + set -euo pipefail + while read -r url; do + echo "Checking: $url" + if curl -sS -I -L --max-time 15 "$url" >/dev/null; then + echo "OK: $url" + else + echo "FAIL: $url" >&2 + exit 2 + fi + done < /tmp/links.txt + shell: bash diff --git a/script.js b/script.js new file mode 100644 index 000000000..d981ea7ec --- /dev/null +++ b/script.js @@ -0,0 +1,175 @@ +// Простая логика для дашборда: графики, транзакции и модал +const formatter = new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB', maximumFractionDigits: 0 }); +let transactions = [ + {id:1,type:'income',category:'salary',amount:85000,description:'Зарплата'}, + {id:2,type:'expense',category:'food',amount:8200,description:'Покупки в супермаркете'}, + {id:3,type:'expense',category:'transport',amount:1200,description:'Такси'}, +]; + +function $(id){return document.getElementById(id)} + +function updateBalances(){ + const totalIncome = transactions.filter(t=>t.type==='income').reduce((s,t)=>s+t.amount,0); + const totalExpense = transactions.filter(t=>t.type==='expense').reduce((s,t)=>s+t.amount,0); + const totalSavings = Math.max(0, totalIncome - totalExpense); + const totalBalance = totalIncome - totalExpense; + $('totalIncome').textContent = formatter.format(totalIncome); + $('totalExpense').textContent = formatter.format(totalExpense); + $('totalSavings').textContent = formatter.format(totalSavings); + $('totalBalance').textContent = formatter.format(totalBalance); +} + +// Charts +let financeChart = null; +let expenseChart = null; +function renderCharts(){ + const ctx = $('financeChart').getContext('2d'); + const months = ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек']; + const incomeData = Array.from({length:12},(_,i)=> Math.round(50000 + Math.random()*50000)); + const expenseData = Array.from({length:12},(_,i)=> Math.round(15000 + Math.random()*30000)); + if(financeChart) financeChart.destroy(); + financeChart = new Chart(ctx,{ + type:'line', + data:{labels:months,datasets:[{label:'Доходы',data:incomeData,borderColor:'#16a34a',backgroundColor:'rgba(16,163,127,0.08)',tension:0.3},{label:'Расходы',data:expenseData,borderColor:'#ef4444',backgroundColor:'rgba(239,68,68,0.06)',tension:0.3}]}, + options:{responsive:true,plugins:{legend:{position:'bottom'}}} + }); + + const ctx2 = $('expenseChart').getContext('2d'); + const categories = ['Продукты','Транспорт','Развлечения','Здоровье','Другое']; + const catValues = [15000,3500,8200,5600,4000]; + if(expenseChart) expenseChart.destroy(); + expenseChart = new Chart(ctx2,{type:'doughnut',data:{labels:categories,datasets:[{data:catValues,backgroundColor:['#60a5fa','#34d399','#f97316','#f472b6','#a78bfa']}]},options:{responsive:true,plugins:{legend:{position:'bottom'}}}}); +} + +// Transactions +function renderTransactions(){ + const list = $('transactionsList'); + list.innerHTML = ''; + transactions.slice().reverse().forEach(t=>{ + const el = document.createElement('div'); + el.className = 'transaction'; + + const meta = document.createElement('div'); + meta.className = 'meta'; + + const category = document.createElement('div'); + category.className = 'category'; + category.textContent = (t.type==='income'? '📈 ' : '📉 ') + t.category; + + const desc = document.createElement('div'); + desc.className = 'desc'; + desc.textContent = t.description || ''; + + meta.appendChild(category); + meta.appendChild(desc); + + const amountEl = document.createElement('div'); + amountEl.className = 'amount'; + amountEl.textContent = (t.type==='income'? '+ ' : '- ') + formatter.format(t.amount); + + el.appendChild(meta); + el.appendChild(amountEl); + list.appendChild(el); + }); +} + +function openModal(){ + $('transactionModal').style.display = 'flex'; +} +function closeModal(){ + $('transactionModal').style.display = 'none'; +} + +function addTransaction(){ + openModal(); +} + +// Логика для модального окна входа +function openLoginModal() { + document.getElementById('loginModal').style.display = 'flex'; +} + +function closeLoginModal() { + document.getElementById('loginModal').style.display = 'none'; +} + +// Логика для модального окна регистрации +function openRegisterModal() { + document.getElementById('registerModal').style.display = 'flex'; +} + +function closeRegisterModal() { + document.getElementById('registerModal').style.display = 'none'; +} + +// Form handling +document.addEventListener('DOMContentLoaded',()=>{ + renderCharts(); + renderTransactions(); + updateBalances(); + const form = $('transactionForm'); + form.addEventListener('submit',(e)=>{ + e.preventDefault(); + const type = $('transactionType').value; + const category = $('transactionCategory').value; + const amount = Math.abs(Number($('transactionAmount').value)||0); + const description = $('transactionDescription').value; + if(amount<=0) return alert('Введите сумму больше 0'); + const id = Date.now(); + transactions.push({id,type,category,amount,description}); + renderTransactions(); + updateBalances(); + renderCharts(); + closeModal(); + form.reset(); + }); + + // close modal when clicking outside content + $('transactionModal').addEventListener('click',(e)=>{ if(e.target.id==='transactionModal') closeModal(); }); + + // Добавляем обработчики для кнопок "Регистрация" и "Войти" + const registerButton = document.querySelector('.btn-primary:nth-of-type(1)'); + const loginButton = document.querySelector('.btn-primary:nth-of-type(2)'); + + registerButton.addEventListener('click', openRegisterModal); + + loginButton.addEventListener('click', openLoginModal); + + const loginForm = document.getElementById('loginForm'); + loginForm.addEventListener('submit', (e) => { + e.preventDefault(); + const username = document.getElementById('loginUsername').value; + const password = document.getElementById('loginPassword').value; + // Authentication is disabled in this demo snapshot. + // Do NOT ship hard-coded credentials in production. Masking demo behavior here. + console.warn('Login attempt for user:', username); + alert('В демо-режиме вход отключён.'); + closeLoginModal(); + }); + + const registerForm = document.getElementById('registerForm'); + registerForm.addEventListener('submit', (e) => { + e.preventDefault(); + const username = document.getElementById('registerUsername').value; + const password = document.getElementById('registerPassword').value; + const confirmPassword = document.getElementById('confirmPassword').value; + + if (password !== confirmPassword) { + alert('Пароли не совпадают.'); + return; + } + + alert('Регистрация успешна! Добро пожаловать, ' + username + '!'); + closeRegisterModal(); + registerForm.reset(); + }); + + // Закрытие модального окна при клике вне контента + document.getElementById('loginModal').addEventListener('click', (e) => { + if (e.target.id === 'loginModal') closeLoginModal(); + }); + + document.getElementById('registerModal').addEventListener('click', (e) => { + if (e.target.id === 'registerModal') closeRegisterModal(); + }); +}); From a5d924d1922f2b559470b8be6d72f6db6f73b1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC?= Date: Mon, 5 Jan 2026 15:37:11 +0000 Subject: [PATCH 3/7] ci: improve link-check workflow (filter localhost, retries, allowlist) and add link-check to PR template --- .github/PULL_REQUEST_TEMPLATE.md | 31 ++++++++++++++++++++++ .github/workflows/check-links.yml | 44 ++++++++++++++++++++++++++----- 2 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..79f8224be --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,31 @@ + + +Title: pkg/github: + +What +- One-sentence summary of the change. + +Why +- Short rationale and intended effect on MCP tools or docs. + +Dev steps performed +- `script/lint` ✅ +- `script/test` ✅ +- `UPDATE_TOOLSNAPS=true go test ./...` (if applicable) ✅ +- `script/generate-docs` (if applicable) ✅ + +Files to review +- `pkg/github/` +- `pkg/github/__toolsnaps__/*.snap` (if changed) +- README.md / docs/ changes (if changed) + +Checklist (required for toolsnaps/docs changes) +- [ ] I ran `script/lint` and fixed formatting/lint issues +- [ ] I ran `script/test` and all tests pass +- [ ] I updated tool snapshots and committed `.snap` files (when schema changed) +- [ ] I ran `script/generate-docs` and included README diffs (if applicable) +- [ ] CI passes: docs-check, lint, license-check + - [ ] CI passes: docs-check, lint, license-check, link-check + +Notes for reviewers +- Brief notes on anything reviewers should watch for (e.g., schema changes, backward-compat concerns). diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index 289ecf09e..337339cc9 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -15,19 +15,51 @@ jobs: - name: Gather external URLs run: | # find http(s) links in repo files and dedupe - grep -RhoE "https?://[^")'\s]+" . | sort -u > /tmp/links.txt || true + grep -RhoE "https?://[^\")'\s]+" . | sort -u > /tmp/links.txt || true echo "Found $(wc -l < /tmp/links.txt || echo 0) links" - cat /tmp/links.txt - - name: Check links + # remove obvious non-http schemes and local addresses + grep -vE "^https?://(localhost|127\.|\[::1\])" /tmp/links.txt | grep -vE "^(file:|mailto:|data:)" | sort -u > /tmp/links_filtered.txt || true + echo "Filtered to $(wc -l < /tmp/links_filtered.txt || echo 0) external links" + cat /tmp/links_filtered.txt + - name: Check links (with retries) + env: + # Optional allowlist regex (set in repo secrets or workflow_dispatch inputs). By default allow common CDNs and github domains. + ALLOWLIST: "github.com|jsdelivr.net|cdn.jsdelivr.net|raw.githubusercontent.com|fonts.googleapis.com|cdnjs.cloudflare.com|app.github.dev" run: | set -euo pipefail + INPUT_FILE=/tmp/links_filtered.txt + # If ALLOWLIST is set, filter links to only matching domains (helps reduce noise) + if [ -n "${ALLOWLIST:-}" ]; then + echo "Applying allowlist: $ALLOWLIST" + grep -E "$ALLOWLIST" "$INPUT_FILE" > /tmp/links_allowed.txt || true + mv /tmp/links_allowed.txt $INPUT_FILE + fi + + failed=0 while read -r url; do + [ -z "$url" ] && continue echo "Checking: $url" - if curl -sS -I -L --max-time 15 "$url" >/dev/null; then + ok=0 + for i in 1 2 3; do + status=$(curl -sS -o /dev/null -w "%{http_code}" -I -L --max-time 15 "$url" || echo "000") + echo " attempt $i -> HTTP $status" + case "$status" in + 2*|3*) ok=1; break;; + 429) sleep $((i*2)); continue;; + 5*) sleep $((i*2)); continue;; + 000) sleep 2; continue;; + *) break;; + esac + done + if [ "$ok" -eq 1 ]; then echo "OK: $url" else echo "FAIL: $url" >&2 - exit 2 + failed=1 fi - done < /tmp/links.txt + done < "$INPUT_FILE" + if [ "$failed" -ne 0 ]; then + echo "One or more external links failed checks" >&2 + exit 2 + fi shell: bash From 33471746164578cfc771b4eaaef31965f6dc90bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC?= Date: Mon, 5 Jan 2026 15:40:04 +0000 Subject: [PATCH 4/7] chore(security): replace inline onclick with addEventListener and add demo CSP meta --- downloaded_homepage.html | 14 +-- index.html | 221 +++++++++++++++++++++++++++++++++++++++ script.js | 21 ++-- 3 files changed, 243 insertions(+), 13 deletions(-) create mode 100644 index.html diff --git a/downloaded_homepage.html b/downloaded_homepage.html index a4acd5757..bee74f7ff 100644 --- a/downloaded_homepage.html +++ b/downloaded_homepage.html @@ -6,6 +6,8 @@ Финансовый Дашборд + + 🌕 @@ -15,8 +17,8 @@

💰 Финансовый Дашборд

@@ -81,7 +83,7 @@

Распределение расходов

Последние транзакции

- +
@@ -136,7 +138,7 @@

Бюджет на месяц