Warn before deleting a file other people share
A feed with other subscribers labels the button Delete for everyone and names them in the confirmation. The server decides: if anyone else has starred the item or not played it, DELETE returns 409 with the reason and only ?force=true proceeds. A feed's header says when it is shared, which answers why a file nobody here asked for exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
This commit is contained in:
22
PROGRESS.md
22
PROGRESS.md
@@ -56,6 +56,26 @@ and until now nothing set them.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-09-11 — Step D: one file, and everyone who wants it
|
||||||
|
|
||||||
|
The last of it, which is all about telling the truth before acting:
|
||||||
|
|
||||||
|
* **Delete says whose file it is.** A feed with other subscribers labels the button *Delete for
|
||||||
|
everyone*, and the confirmation names how many people share it. The server then has the last
|
||||||
|
word: if anyone else has starred the item or not played it yet, `DELETE /api/enclosures/{id}`
|
||||||
|
returns **409** with the reason in plain words, and only `?force=true` goes through. So the
|
||||||
|
rule is enforced where it matters rather than in the page that happens to be asking.
|
||||||
|
* **A feed's header says it is shared** -- "shared with 1 other person" -- which is the answer to
|
||||||
|
"why is there a file here I never asked for": someone else's subscription fetched it, and one
|
||||||
|
copy serves you both.
|
||||||
|
* Retention already respects the same rule from the entry before this one: starred by anyone keeps
|
||||||
|
it, read by everyone releases it.
|
||||||
|
|
||||||
|
`others_wanting` is tested with three subscribers disagreeing, and a browser test walks the whole
|
||||||
|
delete flow: the label, both prompts, declining the second, and the file still being there.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-09-11 — Retention caught up with per-user state
|
## 2026-09-11 — Retention caught up with per-user state
|
||||||
|
|
||||||
Moving read and starred into `entry_state` left `reap_candidates` reading `entries.read` and
|
Moving read and starred into `entry_state` left `reap_candidates` reading `entries.read` and
|
||||||
@@ -175,7 +195,7 @@ on disk. `enclosures.url` is already globally UNIQUE, so the file half is nearly
|
|||||||
- [x] **C. Per-user subscriptions.** `subscriptions(user_id, feed_id)`. config.toml stays the feed
|
- [x] **C. Per-user subscriptions.** `subscriptions(user_id, feed_id)`. config.toml stays the feed
|
||||||
catalogue; the UI lists only what you subscribe to. Adding a feed someone else already has costs
|
catalogue; the UI lists only what you subscribe to. Adding a feed someone else already has costs
|
||||||
nothing. A feed nobody subscribes to stops being scanned but keeps its files.
|
nothing. A feed nobody subscribes to stops being scanned but keeps its files.
|
||||||
- [ ] **D. One file, many users.** Auto-download when *any* subscriber wants it; retention never
|
- [x] **D. One file, many users.** Auto-download when *any* subscriber wants it; retention never
|
||||||
deletes a file another user has starred or not yet played; deleting a download says so when
|
deletes a file another user has starred or not yet played; deleting a download says so when
|
||||||
someone else still has it.
|
someone else still has it.
|
||||||
|
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ test('a second person has their own feeds and their own read state', async ({ br
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('deleting a shared file warns that it is everyone\'s copy', async ({ page }) => {
|
test('deleting a shared file warns that it is everyone\'s copy', async ({ page }) => {
|
||||||
// Admin and Sam both subscribe to Test Show by now; the daemon downloaded one file.
|
// Admin and Sam both subscribe to Test Show by now, and the daemon downloaded a file.
|
||||||
await page.getByText('Test Show').click();
|
await page.getByText('Test Show').click();
|
||||||
await page.locator('.tabs button', { hasText: 'Downloaded' }).click();
|
await page.locator('.tabs button', { hasText: 'Downloaded' }).click();
|
||||||
const row = page.locator('.ep').first();
|
const row = page.locator('.ep').first();
|
||||||
@@ -313,13 +313,23 @@ test('deleting a shared file warns that it is everyone\'s copy', async ({ page }
|
|||||||
await row.click();
|
await row.click();
|
||||||
|
|
||||||
const del = page.locator('#detail button', { hasText: 'Delete' });
|
const del = page.locator('#detail button', { hasText: 'Delete' });
|
||||||
await expect(del).toHaveText(/Delete for everyone/);
|
await expect(del).toHaveText('Delete for everyone');
|
||||||
|
|
||||||
// First prompt: the browser confirm. Say yes, then decline the server's warning, and
|
// Two prompts: the page's own, then the server's, because someone else has not played
|
||||||
// the file must still be there.
|
// it. Accept the first, decline the second, and the file must survive.
|
||||||
page.once('dialog', d => d.accept());
|
const seen = [];
|
||||||
const second = new Promise(resolve => page.once('dialog', d => { resolve(d.message()); d.dismiss(); }));
|
page.on('dialog', d => {
|
||||||
|
seen.push(d.message());
|
||||||
|
if (seen.length === 1) d.accept();
|
||||||
|
else d.dismiss();
|
||||||
|
});
|
||||||
await del.click();
|
await del.click();
|
||||||
expect(await second).toContain('one copy of this file');
|
await expect.poll(() => seen.length, { timeout: 10_000 }).toBe(2);
|
||||||
await expect(page.locator('#detail button', { hasText: 'Delete' })).toBeVisible();
|
expect(seen[0]).toContain('shared with 1 other person');
|
||||||
|
expect(seen[1]).toContain('one copy of this file');
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
await page.getByText('Test Show').click();
|
||||||
|
await page.locator('.tabs button', { hasText: 'Downloaded' }).click();
|
||||||
|
await expect(page.locator('.ep').first()).toBeVisible({ timeout: 20_000 });
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user