feat!: remove the shared demo account

The demo account — one shared user that whitelisted emails entered from
the login page, reset on a schedule — goes end to end (owner decision,
E34): demo.py and the reset job, users.is_demo and demo_whitelist
(migration 0098 deletes the demo user, its audit rows, the reset job's
override and notices, then drops the column and table; the downgrade
restores the empty schema), the /auth/demo entry, the admin whitelist and
reset, the Demo tab, the banner, the login page's "Try the demo", and
every is_demo branch in routes, userscope, scheduler and the UI.

require_human only ever refused the demo account, so its 87 uses become
current_user (owner-approved); require_write_scope keeps its own check.
The login page's "or" divider now shows only beside the Google button:
without the demo entry under it, an instance without Google would have
said "or" to nothing.

A contract test (removedFeatures) fails on the word anywhere in the
backend, frontend, e2e or scripts — seen red on a planted hit. Size
ceilings tightened for the modules that shrank; admin.py is back under
the limit.
This commit is contained in:
2026-10-05 00:19:38 +02:00
parent cca3315836
commit 6d8b7be6d8
109 changed files with 487 additions and 1400 deletions
+18 -26
View File
@@ -342,7 +342,7 @@ function AccessRow({
// Sign-in methods: link a Google account to a password account (or vice-versa set a password),
// so either method can reach the same account. Google connect is a full-page OAuth round-trip
// (auth.py attaches the identity to the current session via /auth/link); the password form posts
// directly with inline errors. Demo accounts never see this (handled by the caller).
// directly with inline errors.
function SignInMethods({ me }: { me: Me }) {
const { t } = useTranslation();
const qc = useQueryClient();
@@ -785,13 +785,11 @@ export function AccountHeader({ me }: { me: Me }) {
<div className="text-xs text-muted truncate">{me.email}</div>
{/* Translated, like the sidebar's tooltip: the raw `user` read in English under HU. */}
<div className="text-xs text-muted">{t(`nav.role.${roleKey(me)}`)}</div>
{!me.is_demo && (
<div className="text-xs text-muted" data-testid="account-last-login">
{me.last_login_at
? t("settings.account.lastLogin", { time: formatDateTime(me.last_login_at, lang) })
: t("settings.account.lastLoginNone")}
</div>
)}
<div className="text-xs text-muted" data-testid="account-last-login">
{me.last_login_at
? t("settings.account.lastLogin", { time: formatDateTime(me.last_login_at, lang) })
: t("settings.account.lastLoginNone")}
</div>
</div>
</div>
</Section>
@@ -823,17 +821,13 @@ function Account({ me, onOpenWizard }: { me: Me; onOpenWizard: () => void }) {
<>
<AccountHeader me={me} />
{!me.is_demo && <SignInMethods me={me} />}
<SignInMethods me={me} />
{me.role === "admin" && me.plex_enabled && <PlexWatchSync />}
{!me.is_demo && <DownloadAuth />}
<DownloadAuth />
{me.is_demo ? (
<Section title={t("settings.account.youtubeAccess")}>
<p className="text-xs text-muted leading-relaxed">{t("settings.account.demoNotice")}</p>
</Section>
) : me.google_enabled ? (
{me.google_enabled ? (
// YouTube access requires Google OAuth; hidden when this instance has no Google configured.
<Section title={t("settings.account.youtubeAccess")}>
<p className="text-xs text-muted leading-relaxed mb-1">
@@ -865,17 +859,15 @@ function Account({ me, onOpenWizard }: { me: Me; onOpenWizard: () => void }) {
</Section>
) : null}
{!me.is_demo && (
<Section title={t("settings.account.dangerZone")}>
<p className="text-xs text-muted leading-relaxed mb-2">
{t("settings.account.deleteHint")}
</p>
<Button variant="danger-soft" size="md" onClick={onDeleteAccount}>
<Trash2 className="w-4 h-4" />
{t("settings.account.deleteAccount")}
</Button>
</Section>
)}
<Section title={t("settings.account.dangerZone")}>
<p className="text-xs text-muted leading-relaxed mb-2">
{t("settings.account.deleteHint")}
</p>
<Button variant="danger-soft" size="md" onClick={onDeleteAccount}>
<Trash2 className="w-4 h-4" />
{t("settings.account.deleteAccount")}
</Button>
</Section>
</>
);
}