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
63 changes: 0 additions & 63 deletions .eslintrc.json

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ on:
- '**.md'
- 'CLAUDE.md'
- '.github/dependabot.yml'
- '.eslintrc.json'
- 'eslint.config.js'
- 'docs/openapi.yaml'
workflow_dispatch:

Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@ jobs:
lint:
name: ESLint
runs-on: ubuntu-latest
continue-on-error: true # optional dependency; don't block merges
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install --no-save eslint@9
- run: npx eslint server.js test/unit.js test/integration.js test/e2e.js telegram-bot.js
node-version: '22' # pnpm 11.25+ requires Node >=22.13; '20' crashes on node:sqlite
- uses: pnpm/action-setup@v4
with:
version: 11
- run: pnpm install --frozen-lockfile
- run: pnpm exec eslint server.js test/unit.js test/integration.js test/e2e.js telegram-bot.js
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ CI ב-[.github/workflows/test.yml](.github/workflows/test.yml) — מריץ unit

`test/unit.js` משתמש ב-`node:test` המובנה (Node 18+) ובודק פונקציות פניניות (escapeHtml, formatShelter, shelterClass, distanceKm, isDND, fuzzyMatch, isRTL) וכן ששלל 14 השפות ב-`LN` חולקות בדיוק את אותו סט מפתחות. `test/integration.js` מקים שרת mock של OREF, מצביע אליו דרך `OREF_URL_OVERRIDE`, ובודק שאזעקה זורמת ל-`/api/alerts`, ל-SSE, ול-`/api/health` (12 assertions) — הכל ברמת API, בלי דפדפן. `test/e2e.js` מריץ Chrome אמיתי דרך Playwright (`channel:'chrome'`, לא מוריד דפדפן bundled) ובודק רגרסיות UI קונקרטיות מהיסטוריית הפרויקט (פוקוס בחיפוש, שימור טאב, רוחב ניווט מובייל, תוויות מקלטים, צבעי option).

קונפיג ESLint ב-[.eslintrc.json](.eslintrc.json) — מינימלי, מתמקד בחיפוש באגים אמיתיים (`no-unused-vars`, `no-undef`, `no-redeclare`, `eqeqeq`); לא אכפתי לסגנון בכוונה כי הקוד דחוס במכוון.
קונפיג ESLint ב-[eslint.config.js](eslint.config.js) (flat config) — מינימלי, מתמקד בחיפוש באגים אמיתיים (`no-unused-vars`, `no-undef`, `no-redeclare`, `eqeqeq`); לא אכפתי לסגנון בכוונה כי הקוד דחוס במכוון.

---

Expand Down
74 changes: 74 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use strict';
// פלאט-קונפיג (ESLint 9+) — מיגרציה מ-.eslintrc.json. שומר בכוונה רק על כללי "באג אמיתי":
// לא נוספו כללי סגנון (max-len/quotes/semi/indent/no-mixed-operators) — הקוד one-liners דחוסים בכוונה.
const globals = require('globals');

const domGlobals = {
L: 'readonly',
indexedDB: 'readonly',
speechSynthesis: 'readonly',
SpeechSynthesisUtterance: 'readonly',
Notification: 'readonly',
AudioContext: 'readonly',
webkitAudioContext: 'readonly',
PushManager: 'readonly',
EventSource: 'readonly',
ServiceWorkerRegistration: 'readonly',
};

module.exports = [
{
languageOptions: {
ecmaVersion: 2022,
sourceType: 'script',
globals: {
...globals.browser,
...globals.node,
...globals.es2022,
...globals.serviceworker,
...globals.worker,
...domGlobals,
// server.js עושה const crypto = require('crypto') — לא ה-Web Crypto API הגלובלי של Node
crypto: 'off',
},
},
rules: {
'no-unused-vars': ['warn', { args: 'none', varsIgnorePattern: '^_' }],
'no-undef': 'error',
'no-redeclare': 'error',
'no-unreachable': 'error',
'no-dupe-keys': 'error',
'no-dupe-args': 'error',
'no-dupe-else-if': 'error',
'no-duplicate-case': 'error',
'no-constant-condition': ['error', { checkLoops: false }],
'no-debugger': 'warn',
'no-empty': ['warn', { allowEmptyCatch: true }],
'no-self-assign': 'error',
'no-self-compare': 'error',
'no-template-curly-in-string': 'warn',
'no-unused-private-class-members': 'warn',
'no-use-before-define': ['error', { functions: false, classes: false, variables: true }],
'valid-typeof': 'error',
'use-isnan': 'error',
eqeqeq: ['warn', 'always', { null: 'ignore' }],
'no-var': 'warn',
'prefer-const': ['warn', { destructuring: 'all' }],
},
},
{
files: ['test/unit.js', 'test/integration.js'],
rules: {
'no-unused-vars': 'off',
},
},
{
// page.evaluate(() => tglShl()) מריץ בקונטקסט הדפדפן — tglShl מוגדר ב-index.html, לא בקובץ הזה
files: ['test/e2e.js'],
languageOptions: {
globals: {
tglShl: 'readonly',
},
},
},
];
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
},
"devDependencies": {
"eslint": "^9.0.0",
"globals": "^17.12.0",
"playwright": "^1.62.1"
},
"engines": {
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ function adminPage() { return `<!DOCTYPE html><html><head><meta charset="UTF-8">
// Optional TOTP 2FA (RFC 6238) — active only if ADMIN_TOTP_SECRET is set, so existing deployments
// see no behavior change. When enabled, the Basic Auth password becomes ADMIN_PASS + the current
// 6-digit code (e.g. "hunter2483726"), compatible with any standard authenticator app.
function base32Decode(str) { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; let bits = '', bytes = []; for (const c of String(str).replace(/=+$/, '').toUpperCase()) { const val = alphabet.indexOf(c); if (val === -1) continue; bits += val.toString(2).padStart(5, '0'); } for (let i = 0; i + 8 <= bits.length; i += 8) bytes.push(parseInt(bits.slice(i, i + 8), 2)); return Buffer.from(bytes); }
function base32Decode(str) { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; let bits = ''; const bytes = []; for (const c of String(str).replace(/=+$/, '').toUpperCase()) { const val = alphabet.indexOf(c); if (val === -1) continue; bits += val.toString(2).padStart(5, '0'); } for (let i = 0; i + 8 <= bits.length; i += 8) bytes.push(parseInt(bits.slice(i, i + 8), 2)); return Buffer.from(bytes); }
function totpAt(secretB32, timeStep) { const key = base32Decode(secretB32); const buf = Buffer.alloc(8); buf.writeBigUInt64BE(BigInt(timeStep)); const hmac = crypto.createHmac('sha1', key).update(buf).digest(); const offset = hmac[hmac.length - 1] & 0xf; const code = ((hmac[offset] & 0x7f) << 24 | (hmac[offset + 1] & 0xff) << 16 | (hmac[offset + 2] & 0xff) << 8 | (hmac[offset + 3] & 0xff)) % 1000000; return String(code).padStart(6, '0'); }
function verifyTOTP(secretB32, token) { if (!/^\d{6}$/.test(token || '')) return false; const step = Math.floor(Date.now() / 30000); for (const drift of [-1, 0, 1]) if (safeEqual(totpAt(secretB32, step + drift), token)) return true; return false; }
// Plain === short-circuits on the first mismatched byte, which in principle leaks a timing
Expand Down Expand Up @@ -532,7 +532,7 @@ const server = http.createServer(async (req, res) => {
if (p === '/api/stream') {
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no' });
const a = [...activeAlerts.values()]; res.write(`data: ${JSON.stringify({ type: 'init', alerts: a })}\n\n`);
let ls = JSON.stringify(a.map(a => a.id)); const cl = { res, ls }; sseClients.add(cl); track(p, 200);
const ls = JSON.stringify(a.map(a => a.id)); const cl = { res, ls }; sseClients.add(cl); track(p, 200);
const iv = setInterval(() => { try { const c = [...activeAlerts.values()]; const ids = JSON.stringify(c.map(a => a.id)); if (ids !== cl.ls) { res.write(`data: ${JSON.stringify({ type: 'update', alerts: c })}\n\n`); cl.ls = ids; } else res.write(`: hb\n\n`); } catch { clearInterval(iv); sseClients.delete(cl); } }, 2000);
req.on('close', () => { clearInterval(iv); sseClients.delete(cl); }); return;
}
Expand Down
Loading