chore(frontend): add ESLint + Prettier; fix real findings; format the tree
The frontend had no JS/TS linter — only svelte-check. Add flat-config ESLint (typescript-eslint
+ eslint-plugin-svelte) and Prettier (tabs/single-quote, matching the existing style).
Rules encode "catch bugs, not enforce taste":
- svelte/require-each-key KEPT — it is the exact bug class as the feed mis-tap fix. Fixed every
flagged block: keyed activeFilters, filteredUsers, stagedFiles (by previewUrl), captionTags,
admin tabs/jobs/users, the export-viewer suggestions/filters/comments, and the static skeleton
loops.
- svelte/prefer-svelte-reactivity KEPT — inline-disabled only the verified-safe sites (a local
freq Map in a $derived.by, throwaway URLSearchParams query builders), with a reason each.
- svelte/no-navigation-without-resolve OFF — wants resolve() around every goto()/href; taste, not
a bug, and pure churn.
- svelte/no-unused-svelte-ignore OFF — those comments are consumed by svelte-check, which ESLint
can't see, so it wrongly calls them unused; removing them would reintroduce a11y warnings.
- no-explicit-any OFF for *.test.ts only (partial fixtures legitimately use any).
Real code fixes beyond keys: removed a dead jobLabel(), an unused ViewerComment import and unused
catch binding, an unused scroll-lock arg, and replaced an empty interface with a type alias.
Then `prettier --write` (54 files). Formatting only. Verified: eslint clean, svelte-check 0 errors,
vitest 46 passed, vite build succeeds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,36 +9,36 @@ import { classifyUploadStatus, isReversibleLock, entryToQueueItem } from './uplo
|
||||
* fired against a dead session. 401 must be `auth` (blob kept, re-auth), NEVER `terminal`.
|
||||
*/
|
||||
describe('classifyUploadStatus', () => {
|
||||
it('2xx → success', () => {
|
||||
expect(classifyUploadStatus(200)).toBe('success');
|
||||
expect(classifyUploadStatus(201)).toBe('success');
|
||||
expect(classifyUploadStatus(299)).toBe('success');
|
||||
});
|
||||
it('2xx → success', () => {
|
||||
expect(classifyUploadStatus(200)).toBe('success');
|
||||
expect(classifyUploadStatus(201)).toBe('success');
|
||||
expect(classifyUploadStatus(299)).toBe('success');
|
||||
});
|
||||
|
||||
it('401 → auth, NOT terminal (never purge the blob on a dead session)', () => {
|
||||
expect(classifyUploadStatus(401)).toBe('auth');
|
||||
});
|
||||
it('401 → auth, NOT terminal (never purge the blob on a dead session)', () => {
|
||||
expect(classifyUploadStatus(401)).toBe('auth');
|
||||
});
|
||||
|
||||
it('429 → rate_limit (back off, auto-resume)', () => {
|
||||
expect(classifyUploadStatus(429)).toBe('rate_limit');
|
||||
});
|
||||
it('429 → rate_limit (back off, auto-resume)', () => {
|
||||
expect(classifyUploadStatus(429)).toBe('rate_limit');
|
||||
});
|
||||
|
||||
it('408 → transient (request timeout is retryable, not terminal)', () => {
|
||||
expect(classifyUploadStatus(408)).toBe('transient');
|
||||
});
|
||||
it('408 → transient (request timeout is retryable, not terminal)', () => {
|
||||
expect(classifyUploadStatus(408)).toBe('transient');
|
||||
});
|
||||
|
||||
it('genuinely permanent 4xx → terminal (locked / banned / released / quota)', () => {
|
||||
expect(classifyUploadStatus(403)).toBe('terminal'); // banned / locked / released
|
||||
expect(classifyUploadStatus(413)).toBe('terminal'); // quota exhausted
|
||||
expect(classifyUploadStatus(400)).toBe('terminal');
|
||||
expect(classifyUploadStatus(404)).toBe('terminal');
|
||||
});
|
||||
it('genuinely permanent 4xx → terminal (locked / banned / released / quota)', () => {
|
||||
expect(classifyUploadStatus(403)).toBe('terminal'); // banned / locked / released
|
||||
expect(classifyUploadStatus(413)).toBe('terminal'); // quota exhausted
|
||||
expect(classifyUploadStatus(400)).toBe('terminal');
|
||||
expect(classifyUploadStatus(404)).toBe('terminal');
|
||||
});
|
||||
|
||||
it('5xx / unexpected → transient (retryable, blob kept)', () => {
|
||||
expect(classifyUploadStatus(500)).toBe('transient');
|
||||
expect(classifyUploadStatus(502)).toBe('transient');
|
||||
expect(classifyUploadStatus(503)).toBe('transient');
|
||||
});
|
||||
it('5xx / unexpected → transient (retryable, blob kept)', () => {
|
||||
expect(classifyUploadStatus(500)).toBe('transient');
|
||||
expect(classifyUploadStatus(502)).toBe('transient');
|
||||
expect(classifyUploadStatus(503)).toBe('transient');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -48,27 +48,27 @@ describe('classifyUploadStatus', () => {
|
||||
* loses a photo the guest expected to survive a reopen, or lets a banned device retry forever.
|
||||
*/
|
||||
describe('isReversibleLock', () => {
|
||||
it('an `uploads_locked` code is reversible at any status (event closed / released)', () => {
|
||||
expect(isReversibleLock(403, 'uploads_locked')).toBe(true);
|
||||
expect(isReversibleLock(409, 'uploads_locked')).toBe(true);
|
||||
});
|
||||
it('an `uploads_locked` code is reversible at any status (event closed / released)', () => {
|
||||
expect(isReversibleLock(403, 'uploads_locked')).toBe(true);
|
||||
expect(isReversibleLock(409, 'uploads_locked')).toBe(true);
|
||||
});
|
||||
|
||||
it('a `forbidden` 403 (banned) is PERMANENT — purge, never resume', () => {
|
||||
expect(isReversibleLock(403, 'forbidden')).toBe(false);
|
||||
});
|
||||
it('a `forbidden` 403 (banned) is PERMANENT — purge, never resume', () => {
|
||||
expect(isReversibleLock(403, 'forbidden')).toBe(false);
|
||||
});
|
||||
|
||||
it('an unidentifiable 403 (unparseable proxy/WAF/captive-portal body) is treated reversible', () => {
|
||||
// Losing a photo is the worst outcome; 403 is the reversible-lock status here.
|
||||
expect(isReversibleLock(403, undefined)).toBe(true);
|
||||
expect(isReversibleLock(403, null)).toBe(true);
|
||||
expect(isReversibleLock(403, '')).toBe(true);
|
||||
});
|
||||
it('an unidentifiable 403 (unparseable proxy/WAF/captive-portal body) is treated reversible', () => {
|
||||
// Losing a photo is the worst outcome; 403 is the reversible-lock status here.
|
||||
expect(isReversibleLock(403, undefined)).toBe(true);
|
||||
expect(isReversibleLock(403, null)).toBe(true);
|
||||
expect(isReversibleLock(403, '')).toBe(true);
|
||||
});
|
||||
|
||||
it('a non-403 permanent 4xx (e.g. 413 quota) is NOT reversible unless explicitly locked', () => {
|
||||
expect(isReversibleLock(413, undefined)).toBe(false);
|
||||
expect(isReversibleLock(400, 'bad_request')).toBe(false);
|
||||
expect(isReversibleLock(413, 'uploads_locked')).toBe(true); // explicit tag still wins
|
||||
});
|
||||
it('a non-403 permanent 4xx (e.g. 413 quota) is NOT reversible unless explicitly locked', () => {
|
||||
expect(isReversibleLock(413, undefined)).toBe(false);
|
||||
expect(isReversibleLock(400, 'bad_request')).toBe(false);
|
||||
expect(isReversibleLock(413, 'uploads_locked')).toBe(true); // explicit tag still wins
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -78,32 +78,32 @@ describe('isReversibleLock', () => {
|
||||
* re-selecting the same file after a reload would MISS the duplicate and queue it twice.
|
||||
*/
|
||||
describe('entryToQueueItem', () => {
|
||||
const base = {
|
||||
id: 'e1',
|
||||
userId: 'u1',
|
||||
fileName: 'photo.jpg',
|
||||
fileSize: 1234,
|
||||
lastModified: 1_700_000_000_000,
|
||||
mimeType: 'image/jpeg',
|
||||
status: 'pending' as const
|
||||
};
|
||||
const base = {
|
||||
id: 'e1',
|
||||
userId: 'u1',
|
||||
fileName: 'photo.jpg',
|
||||
fileSize: 1234,
|
||||
lastModified: 1_700_000_000_000,
|
||||
mimeType: 'image/jpeg',
|
||||
status: 'pending' as const
|
||||
};
|
||||
|
||||
it('carries lastModified across rehydration (dedup depends on it)', () => {
|
||||
expect(entryToQueueItem(base).lastModified).toBe(1_700_000_000_000);
|
||||
});
|
||||
it('carries lastModified across rehydration (dedup depends on it)', () => {
|
||||
expect(entryToQueueItem(base).lastModified).toBe(1_700_000_000_000);
|
||||
});
|
||||
|
||||
it('downgrades an interrupted `uploading` entry to `pending` so it resumes', () => {
|
||||
expect(entryToQueueItem({ ...base, status: 'uploading' }).status).toBe('pending');
|
||||
});
|
||||
it('downgrades an interrupted `uploading` entry to `pending` so it resumes', () => {
|
||||
expect(entryToQueueItem({ ...base, status: 'uploading' }).status).toBe('pending');
|
||||
});
|
||||
|
||||
it('a `done` entry reports 100% progress; others start at 0', () => {
|
||||
expect(entryToQueueItem({ ...base, status: 'done' }).progress).toBe(100);
|
||||
expect(entryToQueueItem(base).progress).toBe(0);
|
||||
});
|
||||
it('a `done` entry reports 100% progress; others start at 0', () => {
|
||||
expect(entryToQueueItem({ ...base, status: 'done' }).progress).toBe(100);
|
||||
expect(entryToQueueItem(base).progress).toBe(0);
|
||||
});
|
||||
|
||||
it('defaults caption/hashtags to empty strings', () => {
|
||||
const item = entryToQueueItem(base);
|
||||
expect(item.caption).toBe('');
|
||||
expect(item.hashtags).toBe('');
|
||||
});
|
||||
it('defaults caption/hashtags to empty strings', () => {
|
||||
const item = entryToQueueItem(base);
|
||||
expect(item.caption).toBe('');
|
||||
expect(item.hashtags).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user