Files
Magent/scripts/review_insights_ui.cjs
T
Assclaw 2976145dd8
Magent CI/CD / verify (push) Successful in 10m31s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Successful in 1m16s
Add private Jellystat viewing stats to Magent beta
2026-09-08 11:25:05 +12:00

130 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Fixture-only review. No requests reach Jellystat or a live Magent backend.
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const { chromium } = require(process.env.REVIEW_PLAYWRIGHT || 'playwright')
const base = process.env.REVIEW_BASE || 'http://127.0.0.1:3114'
const output = process.env.REVIEW_DIR
;(async () => {
const browser = await chromium.launch({ headless: true })
try {
const context = await browser.newContext()
await context.addCookies([{ name: 'magent_logged_in', value: '1', url: base }])
let mode = 'ready'
let role = 'admin'
const periods = []
const mutations = []
let settings = [
{ key: 'jellystat_base_url', value: 'http://jellystat:3000', isSet: true, source: 'environment', sensitive: false },
{ key: 'jellystat_api_key', value: null, isSet: true, source: 'environment', sensitive: true },
]
const daily = Array.from({ length: 31 }, (_, i) => ({ date: new Date(Date.UTC(2026, 7, 8 + i)).toISOString().slice(0, 10), minutes: i % 4 ? 30 + i * 2 : 0 }))
const fixture = (days) => ({
state: mode, is_admin: role === 'admin', days, source: 'Jellystat', timezone: 'UTC', updated_at: '2026-09-07T12:00:00Z',
summary: { minutes: daily.reduce((sum, day) => sum + day.minutes, 0), movies: 8, episodes: 23, plays: 35, active_days: 21, current_streak: 3, longest_streak: 6 },
daily,
top_titles: [{ title: 'Severance', type: 'series', minutes: 460, plays: 10 }, { title: 'Arrival', type: 'movie', minutes: 116, plays: 1 }, { title: 'The Bear', type: 'series', minutes: 91, plays: 3 }],
clients: [{ name: 'Jellyfin Web', minutes: 740 }, { name: 'Jellyfin for Android TV', minutes: 301 }],
methods: [{ name: 'Direct play', minutes: 880 }, { name: 'Transcode', minutes: 161 }],
recent: [{ id: '1', title: 'Good News About Hell', series: 'Severance', type: 'episode', episode: 'S1 · E1', minutes: 57, played_at: '2026-09-07T10:00:00Z', client: 'Jellyfin Web', method: 'Direct play' },
{ id: '2', title: 'Arrival', series: '', type: 'movie', minutes: 116, played_at: '2026-09-06T10:00:00Z', client: 'Jellyfin for Android TV', method: 'Direct play' }],
requests: { total: 3, movies: 2, tv: 1, pending: 1, approved: 2, declined: 0, recent: [{ request_id: 12, title: 'Dune: Part Two', media_type: 'movie', status: 2 }] },
})
await context.route('**/api/**', async (route) => {
const request = route.request()
const url = new URL(request.url())
if (url.pathname === '/api/auth/me') return route.fulfill({ json: { username: 'Fixture viewer', role } })
if (url.pathname === '/api/insights') {
const days = Number(url.searchParams.get('days'))
periods.push(days)
if (mode === 'unauthorized') return route.fulfill({ status: 401, json: { detail: 'Sign in' } })
if (mode === 'unavailable') return route.fulfill({ status: 502, json: { detail: 'Your viewing stats are temporarily unavailable. Please try again shortly.' } })
const result = fixture(days)
if (mode === 'empty') {
result.state = 'ready'
for (const key of Object.keys(result.summary)) result.summary[key] = 0
result.daily = daily.map((day) => ({ ...day, minutes: 0 }))
result.top_titles = result.recent = result.clients = result.methods = []
} else if (mode !== 'ready') result.summary = null
return route.fulfill({ json: result })
}
if (url.pathname === '/api/admin/settings') {
if (request.method() === 'PUT') {
const body = request.postDataJSON()
mutations.push({ path: url.pathname, body })
settings = settings.map((setting) => Object.hasOwn(body, setting.key) ? { ...setting, value: setting.sensitive ? null : body[setting.key], isSet: true, source: 'database' } : setting)
return route.fulfill({ json: { updated: Object.keys(body).length } })
}
return route.fulfill({ json: { settings } })
}
if (url.pathname === '/api/status/services/jellystat/test') {
mutations.push({ path: url.pathname })
return route.fulfill({ json: { name: 'Jellystat', status: 'up', detail: { connected: true } } })
}
if (url.pathname === '/api/status/services') return route.fulfill({ json: { services: [{ name: 'Jellystat', status: 'up' }] } })
if (url.pathname.includes('/events/stream')) return route.fulfill({ contentType: 'text/event-stream', body: ': fixture\n\n' })
return route.fulfill({ json: {} })
})
const page = await context.newPage()
const errors = []
page.on('pageerror', (error) => errors.push(error.message))
for (const width of [1440, 980, 390, 320]) {
await page.setViewportSize({ width, height: 1000 })
await page.goto(base + '/insights')
await page.getByRole('heading', { name: 'Your viewing rhythm' }).waitFor()
assert.notEqual(await page.locator('.stats-period [aria-pressed=true]').evaluate((element) => getComputedStyle(element).backgroundColor), await page.locator('.stats-period [aria-pressed=false]').first().evaluate((element) => getComputedStyle(element).backgroundColor), 'Selected period must be visibly different')
await page.locator('.stats-chart-bars button').first().click()
assert.match(await page.locator('.stats-chart-detail').innerText(), /minutes/)
assert.equal(await page.getByRole('link', { name: 'Dune: Part Two' }).getAttribute('href'), '/requests/12')
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), `Overflow at ${width}px`)
if (width <= 980) {
await page.getByRole('navigation', { name: 'Mobile navigation' }).getByRole('link', { name: 'Config' }).waitFor({ state: 'visible' })
await page.getByRole('navigation', { name: 'Mobile navigation' }).getByRole('link', { name: 'Stats' }).waitFor({ state: 'visible' })
}
if (output) {
fs.mkdirSync(output, { recursive: true })
await page.screenshot({ path: path.join(output, `insights-${width}.png`), fullPage: true })
}
}
await page.getByRole('button', { name: '90 days', exact: true }).click()
await page.getByRole('heading', { name: 'Your viewing rhythm' }).waitFor()
assert.equal(periods.at(-1), 90)
for (const [state, text] of [['empty', 'No viewing history in this period yet.'], ['not_configured', 'Your viewing story starts here'], ['unlinked', 'Link your viewing account'], ['unavailable', 'Stats couldnt load']]) {
mode = state
await page.reload()
await page.getByText(text, { exact: false }).waitFor()
if (state === 'not_configured') assert.equal(await page.getByRole('link', { name: 'Connect Jellystat' }).getAttribute('href'), '/admin/jellystat')
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth))
}
mode = 'not_configured'
role = 'user'
await page.reload()
await page.getByText('Viewing stats will appear here once your administrator connects Jellystat.').waitFor()
assert.equal(await page.getByRole('link', { name: 'Connect Jellystat' }).count(), 0)
mode = 'unauthorized'
await page.reload()
await page.waitForURL('**/login?next=%2Finsights')
role = 'admin'
for (const width of [1440, 390]) {
await page.setViewportSize({ width, height: 1000 })
await page.goto(base + '/admin/jellystat')
await page.getByRole('heading', { name: 'Jellystat', exact: true }).waitFor()
await page.getByRole('button', { name: 'Test connection', exact: true }).waitFor()
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth))
if (output) await page.screenshot({ path: path.join(output, `jellystat-settings-${width}.png`), fullPage: true })
}
await page.getByRole('button', { name: 'Test connection', exact: true }).click()
await page.getByText('Jellystat connection test passed.').waitFor()
assert(mutations.some((request) => request.path === '/api/status/services/jellystat/test'))
const urlInput = page.locator('#setting-jellystat_base_url')
await urlInput.fill('http://jellystat:3001')
await page.getByRole('button', { name: 'Save changes', exact: true }).click()
await page.getByText('Saved', { exact: true }).first().waitFor()
assert(mutations.some((request) => request.body?.jellystat_base_url === 'http://jellystat:3001'))
assert(mutations.filter((request) => request.body).every((request) => !Object.hasOwn(request.body, 'jellystat_api_key')), 'An unchanged secret must be preserved')
assert.deepEqual(errors, [])
console.log('Insights browser checks passed: desktop/mobile, chart, period, requests, empty/setup/unlinked/error/auth states, settings save/test, preserved secret.')
} finally { await browser.close() }
})().catch((error) => { console.error(error); process.exit(1) })