web: say what the guard actually applies
The Blocker page showed all_domains even when nothing was in force, and its copy still called a bare '*' an invalid expansion. Hostnames now comes from the live list (empty while blocking is off), the routed DNS zones get their own stat, and the banners distinguish junk labels from masks no hosts file can express — which is what the server now reports.
This commit is contained in:
2 files changed
+59
-12
No files matched your search
@@ -79,7 +79,12 @@ export interface RefreshResult {
|
||||
|
||||
export interface BlockerPolicy {
|
||||
active: boolean;
|
||||
/** Masks a hosts file cannot express: `*.x.com` (answered by the desktop app's routed DNS zone) and `*gram*` (no zone exists). */
|
||||
unexpandable?: string[];
|
||||
/** Expansion entries that are not subdomain labels (`not a label`) — dropped. A bare `*` is kept: it means every subdomain. */
|
||||
invalid_expansions?: string[];
|
||||
/** The `allowed` masks, normalized — what the desktop app's resolver serves as an exception. */
|
||||
allowed?: string[];
|
||||
reason: string;
|
||||
mode: string;
|
||||
enabled: boolean;
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
* The focus guard: what is blocked, when, and what it caught.
|
||||
*
|
||||
* Lists accept masks. A hosts file has no wildcards of its own, so `*.x.com` is
|
||||
* expanded server-side into real hostnames — this page shows that expansion, and
|
||||
* says plainly when a pattern (`*gram*`) cannot be expressed that way.
|
||||
* expanded server-side into real hostnames, and the patterns that leaves over
|
||||
* are answered by the desktop app's local DNS stub for the zones it can route.
|
||||
* The stats show what is really in force (`domains` is empty while blocking is
|
||||
* off) and every banner says plainly what could not be enforced.
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
@@ -28,13 +30,24 @@ import type { BlockEvents, BlockerPolicy } from "../lib/types";
|
||||
|
||||
const MASK_HELP = [
|
||||
["youtube.com", "the site itself, plus www. and m."],
|
||||
["*.google.com", "every subdomain in the expansion list below"],
|
||||
["*gram*", "matches apps; a hosts file cannot express it"],
|
||||
["*.google.com", "the bare domain plus every subdomain in the expansion list"],
|
||||
["*gram*", "matches apps, and URLs on the phone; no DNS zone can be routed for it"],
|
||||
["* in the list below", "makes *.site cover every subdomain — answered by the desktop app's DNS stub"],
|
||||
];
|
||||
|
||||
/**
|
||||
* The DNS zone a mask can be routed through: what follows its last `*`, and at
|
||||
* least two labels deep — `*gram*` has nothing after its last `*`, and `*.site`
|
||||
* has only one label, so neither can be handed to a resolver.
|
||||
*/
|
||||
const zoneOf = (mask: string): string | null => {
|
||||
const zone = mask.slice(mask.lastIndexOf("*") + 1).replace(/^\.+/, "");
|
||||
return zone.split(".").filter(Boolean).length >= 2 ? zone : null;
|
||||
};
|
||||
|
||||
export default function BlockerPage({ onToast }: { onToast: (message: string) => void }) {
|
||||
const { settings, defaults, save } = useSettings();
|
||||
const policy = useResource<BlockerPolicy & { unexpandable?: string[] }>("/api/blocker/policy", 5000);
|
||||
const policy = useResource<BlockerPolicy>("/api/blocker/policy", 5000);
|
||||
const events = useResource<BlockEvents>("/api/blocker/events", 20000);
|
||||
|
||||
const [domains, setDomains] = useState("");
|
||||
@@ -78,6 +91,12 @@ export default function BlockerPage({ onToast }: { onToast: (message: string) =>
|
||||
|
||||
const blocker = settings?.blocker;
|
||||
const unexpandable = policy.data?.unexpandable ?? [];
|
||||
const unroutable = useMemo(() => unexpandable.filter((mask) => zoneOf(mask) === null), [unexpandable]);
|
||||
const zones = useMemo(
|
||||
() => [...new Set(unexpandable.map(zoneOf).filter((zone): zone is string => zone !== null))],
|
||||
[unexpandable],
|
||||
);
|
||||
const badExpansions = policy.data?.invalid_expansions ?? [];
|
||||
const top = useMemo(() => events.data?.by_target.slice(0, 8) ?? [], [events.data]);
|
||||
|
||||
return (
|
||||
@@ -100,8 +119,22 @@ export default function BlockerPage({ onToast }: { onToast: (message: string) =>
|
||||
/>
|
||||
|
||||
<StatRow>
|
||||
<Stat label="Hostnames" value={policy.data?.all_domains.length ?? 0} caption="after expansion" />
|
||||
<Stat label="Apps" value={policy.data?.all_apps.length ?? 0} caption="closed on sight" />
|
||||
<Stat
|
||||
label="Hostnames"
|
||||
value={policy.data?.domains.length ?? 0}
|
||||
caption={policy.data?.active ? "in force" : "not written (blocking off)"}
|
||||
/>
|
||||
<Stat
|
||||
label="Zones"
|
||||
value={zones.length}
|
||||
caption={policy.data?.active ? "the desktop app routes these" : "unrouted while blocking is off"}
|
||||
size="sm"
|
||||
/>
|
||||
<Stat
|
||||
label="Apps"
|
||||
value={policy.data?.all_apps.length ?? 0}
|
||||
caption={blocker?.kill_blocked_apps ?? true ? "closed on sight" : "warned only"}
|
||||
/>
|
||||
<Stat label="Attempts today" value={events.data?.total ?? 0} caption="turned away" />
|
||||
<Stat
|
||||
label="Mode"
|
||||
@@ -163,11 +196,19 @@ export default function BlockerPage({ onToast }: { onToast: (message: string) =>
|
||||
</Stack>
|
||||
</Panel>
|
||||
|
||||
{unexpandable.length > 0 && (
|
||||
{badExpansions.length > 0 && (
|
||||
<Alert severity="warning" variant="outlined" sx={{ mb: 3 }}>
|
||||
{badExpansions.join(", ")} {badExpansions.length === 1 ? "is not" : "are not"} a subdomain label
|
||||
{badExpansions.length === 1 ? "" : "s"}, so {badExpansions.length === 1 ? "it was" : "they were"} ignored.
|
||||
Each entry is one label — web, m, mobile. A bare <code>*</code> is kept and means every subdomain.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{policy.data?.active && unroutable.length > 0 && (
|
||||
<Alert severity="info" variant="outlined" sx={{ mb: 3 }}>
|
||||
{unexpandable.join(", ")} {unexpandable.length === 1 ? "is a pattern" : "are patterns"} a hosts file
|
||||
cannot express — {unexpandable.length === 1 ? "it is" : "they are"} still matched against application
|
||||
names.
|
||||
{unroutable.join(", ")} cannot be routed to a DNS zone — nothing usable follows the last{" "}
|
||||
<code>*</code>. A hosts file cannot express {unroutable.length === 1 ? "it" : "them"} either;
|
||||
your phone still matches {unroutable.length === 1 ? "it" : "them"} against the URLs you open.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -249,7 +290,8 @@ export default function BlockerPage({ onToast }: { onToast: (message: string) =>
|
||||
setSubdomains(event.target.value);
|
||||
setDirty(true);
|
||||
}}
|
||||
helperText="Comma separated. Add the subdomains you actually visit."
|
||||
error={badExpansions.length > 0}
|
||||
helperText="Comma separated, one label each: web, m, mobile. A bare * means every subdomain."
|
||||
/>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Button variant="contained" disabled={!dirty} onClick={() => void saveLists()}>
|
||||
|
||||
Reference in new issue
Block a user