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:
@@ -152,18 +152,17 @@ export default function ChannelPage({
|
||||
[channelId, qc, t],
|
||||
);
|
||||
|
||||
// First visit of an un-subscribed channel → ingest its recent uploads. Skipped for the demo
|
||||
// account (explore spends shared quota) and when videos are already present (a revisit, or a
|
||||
// subscribed channel whose uploads the scheduler backfills).
|
||||
// First visit of an un-subscribed channel → ingest its recent uploads. Skipped when videos are
|
||||
// already present (a revisit, or a subscribed channel whose uploads the scheduler backfills).
|
||||
useEffect(() => {
|
||||
if (!ch || me.is_demo) return;
|
||||
if (!ch) return;
|
||||
if (autoTried.current === channelId) return;
|
||||
if (ch.subscribed || ch.blocked) return;
|
||||
if (ch.explored && ch.stored_videos > 0) return;
|
||||
autoTried.current = channelId;
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- a one-off INGEST of an unsubscribed channel's uploads on first visit — a network side effect, not state. The autoTried ref keyed on the channel id is what makes it once per channel.
|
||||
void runExplore();
|
||||
}, [ch, channelId, me.is_demo, runExplore]);
|
||||
}, [ch, channelId, runExplore]);
|
||||
|
||||
const block = useMutation({
|
||||
mutationFn: () => (ch?.blocked ? api.unblockChannel(channelId) : api.blockChannel(channelId)),
|
||||
@@ -238,9 +237,9 @@ export default function ChannelPage({
|
||||
// Personal tags, editable right here (the same control + flow the channel manager uses). Optimistic
|
||||
// on this channel's detail cache; a failed attach/detach reverts by refetching, and a success
|
||||
// invalidates the manager list so a tag added here shows there too.
|
||||
// Only fetch the tag catalog when the editor will actually render (subscribed, non-demo) — an
|
||||
// unsubscribed/demo channel page never shows the control, so the request would be thrown away.
|
||||
const tagsQuery = useTags({ enabled: !!ch?.subscribed && !me.is_demo });
|
||||
// Only fetch the tag catalog when the editor will actually render (subscribed) — an unsubscribed
|
||||
// channel page never shows the control, so the request would be thrown away.
|
||||
const tagsQuery = useTags({ enabled: !!ch?.subscribed });
|
||||
const userTags = (tagsQuery.data ?? []).filter((tg) => !tg.system);
|
||||
const { toggleTag, createAndAttachTag } = useChannelTagActions({
|
||||
optimisticToggle: (id, tagId, attach) =>
|
||||
@@ -503,7 +502,7 @@ export default function ChannelPage({
|
||||
<SlidersHorizontal className="w-4 h-4" />
|
||||
</IconButton>
|
||||
)}
|
||||
{ch?.subscribed && !me.is_demo && (
|
||||
{ch?.subscribed && (
|
||||
<div className="inline-flex items-center gap-1.5 h-9 px-2.5 rounded-lg border border-border">
|
||||
<span className="text-xs text-muted">{t("channels.cols.priority")}</span>
|
||||
<PriorityStepper
|
||||
@@ -520,7 +519,7 @@ export default function ChannelPage({
|
||||
While loading, the label carries the live stored/total count (the detail poll refreshes
|
||||
it) so a large channel's paced tail reads as visible progress rather than a stuck
|
||||
spinner. */}
|
||||
{ch?.subscribed && !ch.backfill_done && !me.is_demo && (
|
||||
{ch?.subscribed && !ch.backfill_done && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => !ch.deep_requested && deepBackfill.mutate()}
|
||||
@@ -545,23 +544,21 @@ export default function ChannelPage({
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
{!me.is_demo && (
|
||||
<IconButton
|
||||
// Busy blocks the click, not the focus (FocusSafeButton). Nor does a busy toggle
|
||||
// light up on hover, as if a click would do something.
|
||||
size="md"
|
||||
variant={ch?.blocked ? "danger-soft" : "outline-danger"}
|
||||
onClick={() => block.mutate()}
|
||||
loading={block.isPending}
|
||||
label={ch?.blocked ? t("channel.unblock") : t("channel.block")}
|
||||
>
|
||||
<Ban />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton
|
||||
// Busy blocks the click, not the focus (FocusSafeButton). Nor does a busy toggle
|
||||
// light up on hover, as if a click would do something.
|
||||
size="md"
|
||||
variant={ch?.blocked ? "danger-soft" : "outline-danger"}
|
||||
onClick={() => block.mutate()}
|
||||
loading={block.isPending}
|
||||
label={ch?.blocked ? t("channel.unblock") : t("channel.block")}
|
||||
>
|
||||
<Ban />
|
||||
</IconButton>
|
||||
{/* Hide-from-feed toggle — subscription-scoped, so offered only on a followed channel
|
||||
(mirrors the manager's Eye/EyeOff). Keeps the subscription; only drops its videos
|
||||
from the feed. EyeOff = currently hidden (click to unhide). */}
|
||||
{ch?.subscribed && !me.is_demo && (
|
||||
{ch?.subscribed && (
|
||||
<IconButton
|
||||
size="md"
|
||||
variant={ch.hidden ? "outline-accent" : "outline"}
|
||||
@@ -573,8 +570,7 @@ export default function ChannelPage({
|
||||
{ch.hidden ? <EyeOff /> : <Eye />}
|
||||
</IconButton>
|
||||
)}
|
||||
{!me.is_demo &&
|
||||
!ch?.blocked &&
|
||||
{!ch?.blocked &&
|
||||
(ch?.subscribed ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -598,7 +594,7 @@ export default function ChannelPage({
|
||||
{/* Tagging is a subscription-scoped action (attach_tag → _user_subscription 404s
|
||||
otherwise), and personal tags are for your subscriptions — so only offer it on a
|
||||
channel you actually follow. */}
|
||||
{ch?.subscribed && !me.is_demo && (
|
||||
{ch?.subscribed && (
|
||||
<div className="min-w-0 flex-1">
|
||||
<ChannelTagsControl
|
||||
userTags={userTags}
|
||||
@@ -631,7 +627,6 @@ export default function ChannelPage({
|
||||
view={view}
|
||||
setView={setView}
|
||||
canRead={me.can_read}
|
||||
isDemo={me.is_demo}
|
||||
ytSearch={null}
|
||||
onYtSearch={() => {}}
|
||||
onExitYtSearch={() => {}}
|
||||
|
||||
Reference in New Issue
Block a user