A separate admin page: server settings, accounts and the log
/admin, with Server, Accounts and Log sections chosen by the URL's hash. The server sends the page and /admin.js to admins only (anyone else asking for the page goes back to the app, and the script is 403), and removes the header's link to it from everyone else's page rather than hiding it. The API keeps refusing all of it to non-admins as before. Settings becomes personal: theme, OPML import and export, and the schedule and download folder to read. The server fields, the Users dialog and the Log dialog move out of dialogs.ts into admin.ts. The CSS moves out of index.html into web/app.css, which both pages load as /app.css?v=<hash>, served immutable like the scripts. The smoke test checks both pages. Closes #19. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -125,19 +125,25 @@ test('a theme this browser kept before themes were on the account goes up to it
|
||||
await fresh.close();
|
||||
});
|
||||
|
||||
test('settings opens and saves the global schedule', async ({ page }) => {
|
||||
await page.locator('#prefs').click();
|
||||
await expect(page.locator('#modal.on')).toBeVisible();
|
||||
await expect(page.locator('#gnum')).toBeVisible();
|
||||
test('the admin page saves the global schedule', async ({ page }) => {
|
||||
// Reached from the header's Admin link, and on its own page, not in Settings (issue #19).
|
||||
await page.locator('#admin').click();
|
||||
await expect(page).toHaveURL(/\/admin$/);
|
||||
await expect(page.locator('#atabs a.on')).toHaveText('Server');
|
||||
await page.locator('#gnum').fill('4');
|
||||
await page.locator('#gunit').selectOption('h');
|
||||
await page.locator('#gsave').click();
|
||||
await expect(page.locator('#modal.on')).toBeHidden();
|
||||
await expect(page.locator('#toasts')).toContainText('Settings saved');
|
||||
|
||||
// It must survive a reload, i.e. actually reach the config.
|
||||
await page.locator('#prefs').click();
|
||||
await page.reload();
|
||||
await expect(page.locator('#gnum')).toHaveValue('4');
|
||||
await expect(page.locator('#gunit')).toHaveValue('h');
|
||||
// And Settings in the app no longer has it.
|
||||
await page.goto('/');
|
||||
await page.locator('#prefs').click();
|
||||
await expect(page.locator('#modalCard')).toContainText('Theme');
|
||||
await expect(page.locator('#gnum')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('episodes show with their metadata, and the text opens below', async ({ page }) => {
|
||||
@@ -365,7 +371,7 @@ test('a feed URL is editable and has a copy button', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('the log view has tabs and shows daemon traffic', async ({ page }) => {
|
||||
await page.locator('#logs').click();
|
||||
await page.goto('/admin#log');
|
||||
await expect(page.locator('#logbox')).toBeVisible();
|
||||
await expect(page.locator('#logtabs button')).toHaveCount(4);
|
||||
|
||||
@@ -563,18 +569,22 @@ test('a second person has their own feeds and their own read state', async ({ br
|
||||
|
||||
// Sam subscribes to nothing yet, so sees nothing -- the admin's feeds are not theirs.
|
||||
await expect(page.locator('#feedlist')).toContainText('No feeds.');
|
||||
// Settings stays: Sam has their own subscriptions to export and import, and the
|
||||
// schedule and quota are worth seeing even without a say in them. Only the log and the
|
||||
// users screen -- and the server -- are an admin's alone.
|
||||
// Settings stays: Sam has their own theme and subscriptions. The admin page -- the server's
|
||||
// settings, the accounts and the log -- is an admin's alone, and Sam is not even sent the
|
||||
// link to it, never mind the page.
|
||||
await expect(page.locator('#prefs')).toBeVisible();
|
||||
await page.locator('#prefs').click();
|
||||
await expect(page.locator('#modalCard')).toContainText('Subscriptions');
|
||||
await expect(page.locator('#gsave')).toBeHidden();
|
||||
await expect(page.locator('#gusers')).toBeHidden();
|
||||
await expect(page.locator('#modalCard')).toContainText('Only an admin changes this');
|
||||
await page.locator('#modalCard .cardacts .btn').first().click();
|
||||
// Hiding the button is not the guard; the server is.
|
||||
await expect(page.locator('#admin')).toHaveCount(0);
|
||||
expect(await (await page.request.get('/')).text()).not.toContain('href=/admin');
|
||||
// Asking for it anyway goes back to the app, and its script is refused.
|
||||
await page.goto('/admin');
|
||||
await expect(page).toHaveURL(/:8791\/$/);
|
||||
expect((await page.request.get('/admin.js')).status()).toBe(403);
|
||||
// Hiding the way in is not the guard; the server is.
|
||||
expect((await page.request.get('/api/users')).status()).toBe(403);
|
||||
await expect(page.locator('#logs')).toBeHidden();
|
||||
expect((await page.request.get('/api/logs')).status()).toBe(403);
|
||||
|
||||
// Subscribing to a feed the admin already has costs no second fetch: same feed, same
|
||||
@@ -630,11 +640,12 @@ test('deleting a shared file warns that it is everyone\'s copy', async ({ page }
|
||||
|
||||
// Every row says "Admin" on its checkbox, so match the name exactly.
|
||||
const userRow = (page, name) =>
|
||||
page.locator('#modalCard [data-id]').filter({ has: page.locator('b', { hasText: new RegExp(`^${name}$`) }) });
|
||||
page.locator('#accounts [data-id]').filter({ has: page.locator('b', { hasText: new RegExp(`^${name}$`) }) });
|
||||
|
||||
async function openUsers(page) {
|
||||
await page.locator('#prefs').click();
|
||||
await page.locator('#gusers').click();
|
||||
await page.goto('/admin');
|
||||
await page.locator('#atabs a', { hasText: 'Accounts' }).click();
|
||||
await expect(page).toHaveURL(/\/admin#accounts$/);
|
||||
await expect(userRow(page, 'admin')).toBeVisible();
|
||||
}
|
||||
|
||||
@@ -960,9 +971,7 @@ test('one action, one icon: the toolbar, the page and every dialog agree', async
|
||||
const dialogs = [
|
||||
() => page.locator('#addFeed').click(),
|
||||
() => page.locator('#prefs').click(),
|
||||
async () => { await page.locator('#prefs').click(); await page.locator('#gusers').click(); },
|
||||
async () => { await page.locator('#prefs').click(); await page.locator('#gopml').click(); },
|
||||
() => page.locator('#logs').click(),
|
||||
() => page.locator('#content .acts [data-a="settings"]').click(),
|
||||
() => page.locator('#content .acts [data-a="dl"]').click(),
|
||||
() => page.locator('#content .acts [data-a="rm"]').click(),
|
||||
|
||||
Reference in New Issue
Block a user