diff --git a/css/serene-shell.css b/css/serene-shell.css
index 03d1f632..0dbe5876 100644
--- a/css/serene-shell.css
+++ b/css/serene-shell.css
@@ -663,6 +663,241 @@ body[data-surface="popup"] .input-group {
border-color: rgba(46, 213, 115, 0.68);
}
+.import-export-rule-list .import-export-row {
+ align-items: stretch;
+}
+
+.import-export-rule-list .import-export-text {
+ display: grid;
+ flex: 1 1 auto;
+ gap: 0.58rem;
+ max-width: none;
+}
+
+.import-export-rule-list .import-export-actions {
+ flex: 0 0 12.5rem;
+ min-width: 12.5rem;
+ align-items: stretch;
+}
+
+.import-backup-format-strip {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.42rem;
+ margin-top: 0.45rem;
+}
+
+.import-backup-format-strip span {
+ display: inline-flex;
+ align-items: center;
+ min-height: 1.85rem;
+ padding: 0.32rem 0.62rem;
+ border: 1px solid rgba(74, 157, 127, 0.22);
+ border-radius: 999px;
+ background: rgba(74, 157, 127, 0.08);
+ color: #12643e;
+ font-size: 0.76rem;
+ font-weight: 820;
+ line-height: 1.1;
+}
+
+.rule-list-sheet-example {
+ display: grid;
+ gap: 0;
+ max-width: 44rem;
+ margin-top: 0.25rem;
+ overflow: hidden;
+ border: 1px solid rgba(15, 23, 42, 0.09);
+ border-radius: 1rem;
+ background:
+ linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(255, 252, 247, 0.9)),
+ var(--ft-color-bg-panel);
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.82),
+ 0 18px 30px -28px rgba(15, 23, 42, 0.22);
+}
+
+.rule-list-sheet-example__bar,
+.rule-list-sheet-example__formats,
+.rule-list-sheet-example__examples {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.5rem;
+ padding: 0.58rem 0.72rem;
+ color: var(--ft-color-text-secondary);
+ font-size: 0.78rem;
+ line-height: 1.25;
+}
+
+.rule-list-sheet-example__bar {
+ border-bottom: 1px solid rgba(15, 23, 42, 0.08);
+ background: rgba(255, 255, 255, 0.62);
+ font-weight: 850;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+}
+
+.rule-list-sheet-example__formats {
+ border-top: 1px solid rgba(15, 23, 42, 0.08);
+ background: rgba(255, 249, 241, 0.72);
+}
+
+.rule-list-sheet-example__examples {
+ display: grid;
+ grid-template-columns: 1fr;
+ align-items: stretch;
+ justify-content: stretch;
+ border-top: 1px solid rgba(15, 23, 42, 0.08);
+ background: rgba(255, 255, 255, 0.52);
+}
+
+.rule-list-sheet-example__examples div {
+ display: grid;
+ gap: 0.28rem;
+ min-width: 0;
+ padding: 0.58rem 0.68rem;
+ border: 1px solid rgba(15, 23, 42, 0.07);
+ border-radius: 0.75rem;
+ background: rgba(255, 255, 255, 0.72);
+}
+
+.rule-list-sheet-example__examples b {
+ color: var(--ft-color-brand-primary);
+ font-size: 0.68rem;
+ font-weight: 900;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.rule-list-sheet-example__examples pre {
+ max-width: 100%;
+ max-height: 8.8rem;
+ margin: 0;
+ overflow: auto;
+ color: var(--ft-color-text-secondary);
+ white-space: pre;
+ font: 0.74rem/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+}
+
+.rule-list-sheet-example code {
+ padding: 0.04rem 0.22rem;
+ border: 1px solid rgba(15, 23, 42, 0.08);
+ border-radius: 0.35rem;
+ background: rgba(255, 255, 255, 0.72);
+ color: var(--ft-color-text-primary);
+ font-size: 0.9em;
+}
+
+.rule-list-sheet-example__grid {
+ display: grid;
+ min-width: 0;
+}
+
+.rule-list-sheet-example__row {
+ display: grid;
+ grid-template-columns: minmax(5.5rem, 0.55fr) minmax(9rem, 1fr) minmax(8rem, 1fr);
+ min-width: 0;
+}
+
+.rule-list-sheet-example__row + .rule-list-sheet-example__row {
+ border-top: 1px solid rgba(15, 23, 42, 0.06);
+}
+
+.rule-list-sheet-example__row span {
+ min-width: 0;
+ padding: 0.48rem 0.65rem;
+ overflow: hidden;
+ color: var(--ft-color-text-secondary);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ border-right: 1px solid rgba(15, 23, 42, 0.06);
+ font-size: 0.8rem;
+}
+
+.rule-list-sheet-example__row span:last-child {
+ border-right: 0;
+}
+
+.rule-list-sheet-example__row--head span {
+ background: rgba(171, 68, 56, 0.08);
+ color: var(--ft-color-brand-primary);
+ font-size: 0.68rem;
+ font-weight: 900;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.rule-list-target-picker {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.45rem;
+ max-width: 44rem;
+ margin: 0.1rem 0 0;
+ padding: 0;
+ border: 0;
+}
+
+.rule-list-target-picker legend {
+ width: 100%;
+ margin: 0 0 0.06rem;
+ color: var(--ft-color-text-secondary);
+ font-size: 0.76rem;
+ font-weight: 850;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.rule-list-target-picker label {
+ position: relative;
+ min-width: 0;
+ cursor: pointer;
+}
+
+.rule-list-target-picker input {
+ position: absolute;
+ inset: 0;
+ opacity: 0;
+ pointer-events: none;
+}
+
+.rule-list-target-picker span {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 2.35rem;
+ padding: 0.48rem 0.86rem;
+ border: 1px solid rgba(15, 23, 42, 0.1);
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.76);
+ color: var(--ft-color-text-secondary);
+ font-size: 0.84rem;
+ font-weight: 780;
+ line-height: 1;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72);
+ transition:
+ transform 0.22s cubic-bezier(0.32, 0.72, 0, 1),
+ background-color 0.22s cubic-bezier(0.32, 0.72, 0, 1),
+ border-color 0.22s cubic-bezier(0.32, 0.72, 0, 1),
+ color 0.22s cubic-bezier(0.32, 0.72, 0, 1);
+}
+
+.rule-list-target-picker label:hover span,
+.rule-list-target-picker input:focus-visible + span {
+ border-color: rgba(171, 68, 56, 0.28);
+ color: var(--ft-color-brand-primary);
+ transform: translateY(-1px);
+}
+
+.rule-list-target-picker input:checked + span {
+ border-color: rgba(171, 68, 56, 0.36);
+ background: linear-gradient(180deg, #c66354 0%, #aa4739 100%);
+ color: #fffaf7;
+ box-shadow: 0 16px 28px -22px rgba(129, 47, 35, 0.54);
+}
+
#ftExportV3EncryptedBtn.btn-secondary {
background: linear-gradient(180deg, rgba(196, 150, 72, 0.2), rgba(176, 126, 52, 0.14));
color: #724c16;
@@ -722,6 +957,81 @@ html[data-theme="dark"] .import-export-actions .btn-secondary.btn-import:hover {
border-color: rgba(110, 234, 156, 0.46);
}
+html[data-theme="dark"] .rule-list-sheet-example {
+ border-color: rgba(255, 255, 255, 0.1);
+ background:
+ linear-gradient(180deg, rgba(18, 25, 34, 0.94), rgba(16, 22, 30, 0.92)),
+ var(--ft-color-bg-card);
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.04),
+ 0 18px 30px -26px rgba(0, 0, 0, 0.42);
+}
+
+html[data-theme="dark"] .import-backup-format-strip span {
+ border-color: rgba(74, 222, 128, 0.2);
+ background: rgba(22, 101, 52, 0.14);
+ color: #9af0be;
+}
+
+html[data-theme="dark"] .rule-list-sheet-example__bar {
+ border-bottom-color: rgba(255, 255, 255, 0.08);
+ background: rgba(255, 255, 255, 0.04);
+}
+
+html[data-theme="dark"] .rule-list-sheet-example__formats {
+ border-top-color: rgba(255, 255, 255, 0.08);
+ background: rgba(255, 255, 255, 0.035);
+}
+
+html[data-theme="dark"] .rule-list-sheet-example__examples {
+ border-top-color: rgba(255, 255, 255, 0.08);
+ background: rgba(255, 255, 255, 0.025);
+}
+
+html[data-theme="dark"] .rule-list-sheet-example__examples div {
+ border-color: rgba(255, 255, 255, 0.08);
+ background: rgba(255, 255, 255, 0.04);
+}
+
+html[data-theme="dark"] .rule-list-sheet-example__examples pre {
+ color: var(--ft-color-text-secondary-dark);
+}
+
+html[data-theme="dark"] .rule-list-sheet-example code {
+ border-color: rgba(255, 255, 255, 0.1);
+ background: rgba(255, 255, 255, 0.05);
+ color: var(--ft-color-text-primary);
+}
+
+html[data-theme="dark"] .rule-list-sheet-example__row + .rule-list-sheet-example__row,
+html[data-theme="dark"] .rule-list-sheet-example__row span {
+ border-color: rgba(255, 255, 255, 0.07);
+}
+
+html[data-theme="dark"] .rule-list-sheet-example__row--head span {
+ background: rgba(195, 90, 75, 0.15);
+ color: #f7c7bd;
+}
+
+html[data-theme="dark"] .rule-list-target-picker span {
+ border-color: rgba(255, 255, 255, 0.12);
+ background: rgba(255, 255, 255, 0.05);
+ color: var(--ft-color-text-secondary-dark);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
+}
+
+html[data-theme="dark"] .rule-list-target-picker label:hover span,
+html[data-theme="dark"] .rule-list-target-picker input:focus-visible + span {
+ border-color: rgba(195, 90, 75, 0.34);
+ color: #f7c7bd;
+}
+
+html[data-theme="dark"] .rule-list-target-picker input:checked + span {
+ border-color: rgba(195, 90, 75, 0.36);
+ background: linear-gradient(180deg, #c36557 0%, #a9483c 100%);
+ color: #fffaf7;
+}
+
html[data-theme="dark"] #ftExportV3EncryptedBtn.btn-secondary {
background: rgba(112, 82, 36, 0.28);
color: #e7ca92;
diff --git a/css/tab-view.css b/css/tab-view.css
index c3ffbfb8..3f7759bf 100644
--- a/css/tab-view.css
+++ b/css/tab-view.css
@@ -842,9 +842,12 @@ html[data-theme="dark"] body::after {
.ft-managed-command-center__row {
min-height: 76px;
display: grid;
- grid-template-columns: minmax(220px, 0.95fr) minmax(230px, 1fr) minmax(260px, 1.2fr) minmax(220px, auto);
+ grid-template-columns: minmax(210px, 1.1fr) minmax(260px, 1.4fr) minmax(188px, auto);
+ grid-template-areas:
+ "profile status actions"
+ "details details actions";
gap: 12px;
- align-items: center;
+ align-items: start;
padding: 12px;
border: 1px solid var(--ft-color-sem-neutral-border);
border-radius: 8px;
@@ -869,10 +872,11 @@ html[data-theme="dark"] body::after {
}
.ft-managed-command-center__profile {
+ grid-area: profile;
display: grid;
grid-template-columns: 28px minmax(0, 1fr);
gap: 8px;
- align-items: center;
+ align-items: start;
min-width: 0;
}
@@ -924,7 +928,9 @@ html[data-theme="dark"] body::after {
}
.ft-managed-command-center__details {
+ grid-area: details;
display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 6px;
min-width: 0;
}
@@ -951,6 +957,7 @@ html[data-theme="dark"] body::after {
}
.ft-managed-command-center__status {
+ grid-area: status;
display: flex;
flex-wrap: wrap;
gap: 5px;
@@ -998,6 +1005,7 @@ html[data-theme="dark"] body::after {
}
.ft-managed-command-center__actions {
+ grid-area: actions;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
@@ -1510,6 +1518,14 @@ body:not(.ft-managed-child-editor-view) .ft-managed-child-global {
grid-template-columns: 1fr;
}
+ .ft-managed-command-center__row {
+ grid-template-areas:
+ "profile"
+ "status"
+ "details"
+ "actions";
+ }
+
.ft-managed-command-center__meta {
justify-content: flex-start;
text-align: left;
@@ -2183,6 +2199,36 @@ html[data-theme="dark"] .card::after {
line-height: 1.45;
}
+.managed-channel-list-modal__template {
+ display: grid;
+ gap: 0.55rem;
+ padding: 0.78rem;
+ border: 1px solid rgba(74, 157, 127, 0.22);
+ border-radius: 0.9rem;
+ background: rgba(74, 157, 127, 0.07);
+}
+
+.managed-channel-list-modal__template-title {
+ color: var(--ft-color-text-primary);
+ font-size: 0.86rem;
+ font-weight: 800;
+}
+
+.managed-channel-list-modal__template pre {
+ max-height: 132px;
+ margin: 0;
+ overflow: auto;
+ white-space: pre;
+ color: var(--ft-color-text-secondary);
+ font: 0.78rem/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+}
+
+.managed-channel-list-modal__template-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
.managed-channel-list-modal__help,
.managed-channel-list-modal__error {
padding: 0.68rem 0.78rem;
@@ -2197,6 +2243,128 @@ html[data-theme="dark"] .card::after {
background: rgba(74, 157, 127, 0.08);
}
+.managed-channel-list-modal__formats {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.45rem;
+}
+
+.managed-channel-list-modal__formats span {
+ display: grid;
+ gap: 0.16rem;
+ min-width: 0;
+ padding: 0.58rem 0.68rem;
+ border: 1px solid rgba(15, 23, 42, 0.08);
+ border-radius: 0.8rem;
+ background: rgba(255, 255, 255, 0.72);
+ color: var(--ft-color-text-secondary);
+ font-size: 0.78rem;
+ line-height: 1.35;
+}
+
+.managed-channel-list-modal__formats b {
+ color: var(--ft-color-brand-primary);
+ font-size: 0.68rem;
+ font-weight: 900;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.managed-channel-list-modal__formats code {
+ overflow: visible;
+ color: var(--ft-color-text-primary);
+ overflow-wrap: anywhere;
+ white-space: normal;
+ font-size: 0.84em;
+}
+
+.managed-channel-list-modal__preview {
+ display: grid;
+ gap: 0.58rem;
+ padding: 0.72rem 0.78rem;
+ border: 1px solid rgba(82, 128, 214, 0.22);
+ border-radius: 0.9rem;
+ background: rgba(82, 128, 214, 0.08);
+}
+
+.managed-channel-list-modal__preview-title {
+ color: var(--ft-color-text-primary);
+ font-size: 0.86rem;
+ font-weight: 850;
+}
+
+.managed-channel-list-modal__preview-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 0.5rem;
+}
+
+.managed-channel-list-modal__preview-stat {
+ display: grid;
+ gap: 0.14rem;
+ min-width: 0;
+ padding: 0.58rem 0.64rem;
+ border: 1px solid rgba(15, 23, 42, 0.08);
+ border-radius: 0.75rem;
+ background: rgba(255, 255, 255, 0.78);
+}
+
+.managed-channel-list-modal__preview-stat strong {
+ color: var(--ft-color-text-primary);
+ font-size: 1.1rem;
+ line-height: 1;
+}
+
+.managed-channel-list-modal__preview-stat span,
+.managed-channel-list-modal__preview-note,
+.managed-channel-list-modal__preview-empty {
+ color: var(--ft-color-text-secondary);
+ font-size: 0.86rem;
+ line-height: 1.38;
+}
+
+.managed-channel-list-modal__sheet {
+ display: grid;
+ overflow: hidden;
+ border: 1px solid rgba(15, 23, 42, 0.08);
+ border-radius: 0.82rem;
+ background: rgba(255, 255, 255, 0.7);
+}
+
+.managed-channel-list-modal__sheet-row {
+ display: grid;
+ grid-template-columns: minmax(5.5rem, 0.55fr) minmax(11rem, 1.2fr) minmax(7rem, 0.8fr);
+ min-width: 0;
+}
+
+.managed-channel-list-modal__sheet-row + .managed-channel-list-modal__sheet-row {
+ border-top: 1px solid rgba(15, 23, 42, 0.06);
+}
+
+.managed-channel-list-modal__sheet-row span {
+ min-width: 0;
+ padding: 0.5rem 0.62rem;
+ overflow: hidden;
+ color: var(--ft-color-text-secondary);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ border-right: 1px solid rgba(15, 23, 42, 0.06);
+ font-size: 0.8rem;
+}
+
+.managed-channel-list-modal__sheet-row span:last-child {
+ border-right: 0;
+}
+
+.managed-channel-list-modal__sheet-head span {
+ background: rgba(82, 128, 214, 0.1);
+ color: var(--ft-color-text-primary);
+ font-size: 0.68rem;
+ font-weight: 900;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
.managed-channel-list-modal__library {
display: grid;
gap: 0.65rem;
@@ -2273,6 +2441,36 @@ html[data-theme="dark"] .card::after {
background: rgba(22, 101, 52, 0.16);
}
+:root[data-theme="dark"] .managed-channel-list-modal__template {
+ border-color: rgba(74, 222, 128, 0.22);
+ background: rgba(22, 101, 52, 0.14);
+}
+
+:root[data-theme="dark"] .managed-channel-list-modal__formats span,
+:root[data-theme="dark"] .managed-channel-list-modal__sheet,
+:root[data-theme="dark"] .managed-channel-list-modal__preview-stat {
+ border-color: rgba(255, 255, 255, 0.08);
+ background: rgba(255, 255, 255, 0.05);
+}
+
+:root[data-theme="dark"] .managed-channel-list-modal__formats code {
+ color: var(--ft-color-text-primary);
+}
+
+:root[data-theme="dark"] .managed-channel-list-modal__preview {
+ border-color: rgba(96, 165, 250, 0.22);
+ background: rgba(30, 64, 175, 0.15);
+}
+
+:root[data-theme="dark"] .managed-channel-list-modal__sheet-row + .managed-channel-list-modal__sheet-row,
+:root[data-theme="dark"] .managed-channel-list-modal__sheet-row span {
+ border-color: rgba(255, 255, 255, 0.07);
+}
+
+:root[data-theme="dark"] .managed-channel-list-modal__sheet-head span {
+ background: rgba(96, 165, 250, 0.14);
+}
+
:root[data-theme="dark"] .managed-channel-list-modal__library-item {
border-color: var(--ft-color-sem-neutral-border);
background: var(--ft-color-bg-card);
@@ -2285,6 +2483,19 @@ html[data-theme="dark"] .card::after {
}
@media (max-width: 640px) {
+ .managed-channel-list-modal__formats,
+ .managed-channel-list-modal__preview-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .managed-channel-list-modal__sheet {
+ overflow-x: auto;
+ }
+
+ .managed-channel-list-modal__sheet-row {
+ min-width: 34rem;
+ }
+
.managed-channel-list-modal__url-row {
grid-template-columns: 1fr;
}
diff --git a/docs/audit/FILTERTUBE_LOCAL_NETWORK_MANAGED_PARENT_CONTROLS_PLAN_2026-06-03.md b/docs/audit/FILTERTUBE_LOCAL_NETWORK_MANAGED_PARENT_CONTROLS_PLAN_2026-06-03.md
index 9beaf970..a1e9f30b 100644
--- a/docs/audit/FILTERTUBE_LOCAL_NETWORK_MANAGED_PARENT_CONTROLS_PLAN_2026-06-03.md
+++ b/docs/audit/FILTERTUBE_LOCAL_NETWORK_MANAGED_PARENT_CONTROLS_PLAN_2026-06-03.md
@@ -94,13 +94,18 @@ extension authority code.
per-profile details column so profile names and next actions do not collapse
under provider/status copy.
- [x] Parent workflow strip: Family Controls now leads with `Choose profile`,
- `Set rules and time`, and `Pair or send` so parents understand that local
- control works first and verified-device delivery is only needed for another
- device.
+ `Set guardrails`, and `Sync when needed` so parents understand that local
+ control works first and verified-device delivery is only needed when the same
+ protected profile must update another device.
- [x] Family Controls row copy and feedback were simplified for parent use:
- `Pair to sync` means remote device setup only, profile ownership reads as
- `Parent: ...`, status chips have explanatory titles, and neutral detail cards
- no longer look like warning/error states when nothing is wrong.
+ rows now say `Device sync: Not paired` for local-only profiles, saved
+ verified devices say `open both devices` when live P2P is needed, profile
+ ownership reads as `Parent: ...`, status chips have explanatory titles, and
+ neutral detail cards no longer look like warning/error states when nothing is
+ wrong.
+- [x] Family Controls row layout now keeps profile/status/actions on the first
+ line and moves details underneath, so parent names, device state, list source,
+ and action buttons do not squeeze each other on desktop or mobile.
- [x] Accounts & Sync now shows a protected-edit boundary when a parent is
editing a protected profile: Family Controls remains the target surface for
rules/time/history/send, generic device pairing remains parent-owned, and
@@ -116,10 +121,11 @@ parent tool instead of a sync/debug console.
and time, then pair/send only when another device needs the update.
- [x] Optional mailbox/LAN provider rows are not shown as first-run required
setup when no protected profile exists or when providers are not configured.
-- [x] Imported channel lists are treated as parent-approved rule sources, not as
+- [x] Imported rule lists are treated as parent-approved rule sources, not as
transport authority or executable filter code.
-- [x] List-derived channel rules preserve source metadata, source format,
- source hash, last checked time, pause state, and Manual-vs-list separation.
+- [x] List-derived channel and keyword rules preserve source metadata, source
+ format, source hash, last checked time, pause state, and Manual-vs-list
+ separation.
- [x] Channels page exposes a source filter/dropdown so parents can view
`Manual`, `Imported lists`, and individual managed lists without guessing
where a channel entry came from.
@@ -134,7 +140,7 @@ parent tool instead of a sync/debug console.
- [ ] Managed action history clearly answers who changed a rule, whether it was
manual or list-derived, and whether it was sent to a verified device, without
exposing raw policy JSON or sensitive rule payloads to protected users.
-- [ ] Downstream app UI contract documents the same channel-list source filter,
+- [ ] Downstream app UI contract documents the same rule-list source filter,
Kids list selection, and source badges so mobile/tablet surfaces do not fork
the parent mental model.
- [x] Command center can send signed active managed-policy updates to currently
@@ -152,15 +158,21 @@ parent tool instead of a sync/debug console.
while offline.
- [x] Runtime Main/Kids route gate, background-owned time-budget accounting, and
protected timeout overlay exist for active protected profiles.
-- [x] Managed channel filter-list imports and parent-triggered URL subscriptions
+- [x] Managed rule-list imports and parent-triggered URL subscriptions
are now part of the managed parent/caregiver goal. Issue 62 asks for
- content-blocker-style channel lists that can be imported, enabled, disabled,
- and synced instead of forcing parents to add channels one at a time. The
+ content-blocker-style lists that can be imported, enabled, disabled,
+ and synced instead of forcing parents to add channels or keywords one at a time. The
extension-owned manual/check/refresh/pause/remove path is present. This is a
parent/caregiver rule-source feature, not an untrusted URL authority path. The
parent-facing flow stays simple: import or check a list, preview channels,
- choose protected profiles, apply, then send to verified devices when delivery
- is ready. Silent scheduled refresh remains deferred.
+ keywords, and skipped rows, choose protected profiles, apply, then send to
+ verified devices when delivery is ready. Silent scheduled refresh remains
+ deferred.
+ - [x] First CSV rule-list slice: parents can use a visible CSV template with
+ `channel_id,keyword,notes`, paste or load CSV, preview explicit channels and
+ explicit keywords separately, apply them to Main/Kids/both under the target
+ profile current Blocklist/Whitelist mode, and send the changed profile
+ policy through the existing managed-policy JSON path.
- [x] First local import slice: parent/account profiles can paste or choose a
text file, preview valid channel identifiers, apply the list to selected
protected profiles on Main/Kids/both, write protected redacted history, and
@@ -214,13 +226,13 @@ parent tool instead of a sync/debug console.
list revision they imported or refreshed, while the URL/list still has no
policy authority by itself.
- [x] First structured-list compatibility slice: parents can paste or choose
- a simple JSON channel list (`channels`, `items`, `entries`,
- `blockedChannels`, `channelIds`, or `handles`) and the entries still
- normalize through the same preview, parent re-auth, materialized channel
- rows, list metadata, and verified-device delivery path as text lists.
- Materialized rows now also preserve compact source-format metadata so apps
- can distinguish text rows from JSON sources without treating either format
- as policy authority.
+ a simple JSON rule list (`channels`, `items`, `entries`, `blockedChannels`,
+ `channelIds`, `handles`, and explicit `keywords`) and the entries still
+ normalize through the same preview, parent re-auth, materialized rule rows,
+ list metadata, and verified-device delivery path as text lists. Materialized
+ rows now also preserve compact source-format metadata so apps can
+ distinguish text rows from JSON/CSV sources without treating any format as
+ policy authority.
- [x] First subscription-check slice: parent/account profiles can check
URL-backed lists from the `Lists` action. Changed source hashes refresh
materialized channel rows after parent re-auth; unchanged source hashes only
@@ -964,7 +976,7 @@ the current extension dashboard.
Parent/account-authorized profile manager views now also show a command-center
overview for protected profiles, viewing spaces, time limits, sync status,
and protected history. Command-center row buttons are delegated action intents
- for existing gated Edit Rules, History, Time Limit, and Send Update paths,
+ for existing gated Rules, History, Time Limit, and Send paths,
plus a selected-profile rule editor handoff, local selected-profile bulk
time-limit and viewing-space actions, and selected-profile signed-policy
sends. Child/protected views do not receive detailed managed status text or
@@ -982,7 +994,7 @@ the current extension dashboard.
when both sides are available. Unconfigured mailbox and local-network provider
setup is hidden from the normal command center so parents are not asked to
bring infrastructure. If a provider is already configured, the UI labels it as
- Later Updates or Same-Network and uses plain parent/user language while audit
+ Offline Pickup or Same-Network and uses plain parent/user language while audit
docs retain the trusted-link, target-profile, scope, revision, hash, and
signature authority proof.
- **Acceptance Criteria**:
diff --git a/docs/audit/FILTERTUBE_MANAGED_IMPORT_LIST_FORMAT_2026-06-18.md b/docs/audit/FILTERTUBE_MANAGED_IMPORT_LIST_FORMAT_2026-06-18.md
new file mode 100644
index 00000000..ff088ad8
--- /dev/null
+++ b/docs/audit/FILTERTUBE_MANAGED_IMPORT_LIST_FORMAT_2026-06-18.md
@@ -0,0 +1,324 @@
+# FilterTube Managed Import List Format
+
+Date: 2026-06-18
+Scope: parent/caregiver-managed protected profiles, local profile rules, Nanah live sync handoff, and future app parity.
+
+## Goal
+
+Keep imports simple enough for parents and caregivers:
+
+1. The file provides rule values.
+2. FilterTube previews what it understood.
+3. The parent chooses protected profiles.
+4. The parent chooses Main YouTube, YouTube Kids, or both.
+5. FilterTube applies the rules locally after parent/account unlock.
+6. FilterTube can then send the changed profile policy to verified devices.
+
+The file must not decide parent authority, target child profile, PIN behavior, sync delivery, list mode, or Main/Kids access. Those decisions stay in the UI.
+
+## User-Facing Name
+
+Use `Import List` in the UI.
+
+Avoid making parents choose between many importer types. The importer can accept:
+
+- channel lists
+- keyword lists
+- mixed CSV rule lists
+- URL-backed lists
+
+The preview should explain the result in plain counts:
+
+```text
+48 channels
+12 keywords
+3 rows skipped
+Applies to: Main + Kids
+Profiles: Asha, Kabir
+```
+
+## Recommended CSV Format
+
+CSV is for explicit channel identifiers and explicit keywords. It is not semantic inference.
+
+Recommended header:
+
+```csv
+channel_id,keyword,notes
+UCxxxxxxxxxxxxxxxxxxxxxx,,block this channel
+@SomeChannel,,handle is accepted
+https://www.youtube.com/@AnotherChannel,,URL is accepted
+,spider,hide this topic keyword
+,brainrot,hide this keyword
+```
+
+Rules:
+
+- `channel_id` can contain:
+ - `UC...` channel ID
+ - `@handle`
+ - `/channel/UC...`
+ - `/c/name`
+ - `/user/name`
+ - YouTube channel URL
+- `keyword` can contain a plain keyword or phrase.
+- `notes` is ignored by runtime and exists only so list authors can explain rows.
+- A row may contain a channel, a keyword, or both.
+- Empty rows and comment rows are skipped.
+- Name-only channel rows are skipped for safety.
+
+Accepted channel column aliases:
+
+```text
+channel
+channel_id
+channelId
+channel_url
+youtube_url
+url
+handle
+uc_id
+```
+
+Accepted keyword column aliases:
+
+```text
+keyword
+keywords
+term
+terms
+phrase
+phrases
+```
+
+## Alternate CSV Format
+
+For list maintainers who prefer one value column:
+
+```csv
+type,value,notes
+channel,UCxxxxxxxxxxxxxxxxxxxxxx,channel id
+channel,@SomeChannel,handle
+keyword,spider,topic keyword
+keyword,brainrot,topic keyword
+```
+
+This is readable, but the UI should still recommend `channel_id,keyword,notes` because it is easier for spreadsheet users.
+
+## Plain Text Format
+
+Plain text remains channel-first for safety.
+
+```text
+# title: Family channel block list
+# version: 2026.06.18
+UCxxxxxxxxxxxxxxxxxxxxxx
+@SomeChannel
+https://www.youtube.com/@AnotherChannel
+```
+
+Do not treat arbitrary plain text rows as keywords. A note like `bad thumbnails` should not silently become a keyword rule.
+
+## JSON Format
+
+JSON can support both channels and keywords.
+
+```json
+{
+ "title": "Family safety starter list",
+ "version": "2026.06.18",
+ "homepage": "https://example.com/filtertube-list",
+ "channels": [
+ "UCxxxxxxxxxxxxxxxxxxxxxx",
+ "@SomeChannel",
+ "https://www.youtube.com/@AnotherChannel"
+ ],
+ "keywords": [
+ "spider",
+ "brainrot"
+ ]
+}
+```
+
+Simple arrays remain channel lists for backward compatibility:
+
+```json
+[
+ "UCxxxxxxxxxxxxxxxxxxxxxx",
+ "@SomeChannel"
+]
+```
+
+## Not In The File
+
+These must not be accepted from the import file:
+
+- protected profile id
+- parent profile id
+- PIN or password
+- Nanah trusted-device id
+- sync destination
+- list mode change
+- Main/Kids access mode
+- daily time limit
+- allow/deny authority
+- remote command
+
+Reason: a downloaded list should never become authority. It is only a source of suggested rules.
+
+## How It Applies
+
+Parent UI owns the final choices:
+
+```text
+Import List
+ -> preview channels/keywords/skipped rows
+ -> choose protected profiles
+ -> choose Main / Kids / Both
+ -> parent/account unlock
+ -> apply rows to each target profile's current mode
+ -> optionally send update to verified device
+```
+
+### Parent/Caregiver User Flow
+
+```mermaid
+flowchart TD
+ A["Parent opens Accounts and Sync"] --> B["Choose protected profile or selected profiles"]
+ B --> C["Open Lists"]
+ C --> D["Import List"]
+ D --> E["Paste, choose file, load HTTPS URL, or download CSV template"]
+ E --> F["FilterTube previews channels, keywords, skipped rows"]
+ F --> G{"Parent accepts preview?"}
+ G -- "No" --> H["Cancel with no profile changes"]
+ G -- "Yes" --> I["Choose Main, Kids, or Both"]
+ I --> J["Parent/account unlock"]
+ J --> K["Apply explicit rules to selected protected profiles"]
+ K --> L{"Verified device path available?"}
+ L -- "No" --> M["Keep local profile policy ready"]
+ L -- "Yes" --> N["Offer Send Update"]
+ N --> O["Send managed-policy JSON through existing verified-device path"]
+```
+
+### Rule Import Behavior Flow
+
+```mermaid
+flowchart TD
+ A["Input text/file/URL"] --> B{"Looks like JSON?"}
+ B -- "Yes" --> C["Parse channels and keywords arrays"]
+ B -- "No" --> D{"CSV headers found?"}
+ D -- "Yes" --> E["Parse channel_id and keyword columns or type/value rows"]
+ D -- "No" --> F["Plain text channel-only parser"]
+ C --> G["Normalize explicit channel identifiers"]
+ E --> G
+ F --> G
+ C --> H["Normalize explicit keywords"]
+ E --> H
+ G --> I["Deduplicate against target profile surface"]
+ H --> I
+ I --> J["Decorate rows with managedListId, source hash, source label"]
+ J --> K["Save rows under current Blocklist or Whitelist mode"]
+```
+
+### Verified-Device Sync Flow
+
+Imported list rows do not create a second sync protocol. After rules are applied
+to a protected profile, FilterTube sends the changed policy through the existing
+managed-policy JSON path when the parent chooses to send now.
+
+```mermaid
+sequenceDiagram
+ participant Parent as Parent profile
+ participant Runtime as FilterTube extension runtime
+ participant Nanah as Nanah verified path
+ participant Child as Protected device/profile
+
+ Parent->>Runtime: Apply imported rule list after unlock
+ Runtime->>Runtime: Update protected profile channels/keywords
+ Runtime->>Runtime: Record protected action history
+ Runtime->>Parent: Offer Send Update when verified path exists
+ Parent->>Runtime: Confirm send
+ Runtime->>Nanah: Send signed managed-policy JSON envelope
+ Nanah->>Child: Deliver to verified target profile path
+ Child->>Child: Verify trusted link, target profile, scope, revision, hash
+ Child->>Child: Apply only if newer and trusted
+```
+
+Mode behavior:
+
+- If the target surface is in blocklist mode, imported channels go to block channels and imported keywords go to block keywords.
+- If the target surface is in whitelist mode, imported channels go to whitelist channels and imported keywords go to whitelist keywords.
+- The import does not switch modes.
+
+Pause/resume/remove behavior:
+
+- Rows from the same imported list share `managedListId`.
+- Pause disables list-derived channel and keyword rows without deleting manual rules.
+- Resume re-enables them.
+- Remove deletes only rows with that `managedListId`.
+- URL refresh replaces only rows from that list.
+
+## Semantic ML Boundary
+
+This CSV format is deterministic. It does not infer related channels or related keywords.
+
+Later semantic ML can build a separate `suggested rules` layer:
+
+```text
+seed term -> suggested keywords/channels -> parent review -> explicit rules
+```
+
+That future layer should still end at the same explicit rule model before it affects a child/protected profile.
+
+## UI Simplification
+
+Use one modal with four clear areas:
+
+```text
+1. List source
+ Paste, choose file, or load public HTTPS URL.
+
+2. Preview
+ Channels found, keywords found, rows skipped.
+
+3. Apply to
+ Main, Kids, or Both.
+
+4. Finish
+ Apply locally, then optionally Send to verified devices.
+```
+
+Avoid showing mailbox/LAN/provider wording inside the import flow. Import is local first; delivery is the next optional step.
+
+## 2026-06-18 UX Completion Note
+
+The first parser slice was not complete from a user-flow standpoint because CSV import was only reachable through protected-profile list actions and Help text. A second pass briefly placed separate import affordances inside Main Filters and YouTube Kids channel management, but that split the mental model: Filters/Kids pages are rule-editing surfaces, while external files and backup/list imports belong in Settings.
+
+Current completion rule:
+
+- Primary entry point: Settings -> Import / Export -> Rule list imports.
+- Target choice: Main YouTube, YouTube Kids, or Both. This works for the active profile or the protected profile currently being edited by a parent/account profile.
+- Main/Kids rule pages remain the place to review, edit, pause, resume, and remove the imported rows after import.
+- The Settings card shows a sheet-like structure preview instead of dense prose: `type`, `value`, `notes`, plus supported CSV/JSON shapes.
+- The modal shows supported formats, CSV template, file/URL/paste inputs, live preview counts, skipped row counts, a spreadsheet-like parsed-row preview, and the final Apply confirmation.
+- Rule-list JSON is intentionally narrower than a full FilterTube backup JSON. It may add channels and keywords only; it does not change profile structure, PINs, trusted devices, viewing spaces, or sync targets.
+- The Settings card and import modal expose both CSV and JSON rule-list templates. The CSV template is the spreadsheet path; the JSON template is the lightweight rule-list shape, not the full FilterTube backup/export structure.
+- Import backup remains the full restore/migration lane for FilterTube backup JSON and legacy BlockTube export JSON.
+- Help text should stay short and point to the UI path; this audit file owns the detailed format contract.
+
+Supported source shapes in this slice:
+
+- CSV: `channel_id,keyword,notes`, `channel,keyword,notes`, or typed rows such as `type,value,notes`.
+- Text: bare rows are treated as channel IDs/handles/custom URLs/URLs. Explicit typed rows are also accepted: `channel: @SomeChannel`, `channel: UC...`, `channel: c/Name`, and `keyword: brainrot`.
+- Simple JSON: `channels` and/or `keywords` arrays.
+- BlockTube-style JSON: `filterData.channelId`, `filterData.channelName`, and `filterData.title` arrays are read as rule-list channels/keywords.
+- Raw HTTPS source URL: public CSV, text, or JSON fetched into the same preview before apply.
+
+Not shipped in this CSV PR:
+
+- Built-in global/public list catalog.
+- Automatic subscriptions to third-party lists.
+- Parent/community moderation workflow for shared "good" or "bad" channel lists.
+- Silent application across profiles or devices.
+
+Those are compatible with this foundation, but they need a separate catalog and governance design: source URL, maintainer, scope, last checked, content hash, update policy, enable/disable state, user review, and per-profile Main/Kids target selection.
diff --git a/html/tab-view.html b/html/tab-view.html
index 6c192798..5a4ab14a 100644
--- a/html/tab-view.html
+++ b/html/tab-view.html
@@ -460,11 +460,16 @@
Import / Export
- Merge a saved FilterTube export or compatible JSON.
+ Merge a full FilterTube backup or compatible backup JSON.
- Existing entries stay put; matching channels/keywords are merged
+ Use this for complete settings restore or legacy BlockTube export migration. For previewed channel/keyword-only lists, use Rule list imports below.
+
+ FilterTube backup JSON
+ BlockTube export JSON
+ Full restore or migration
+
Details
@@ -480,6 +485,94 @@
Import / Export
+
+
Rule list imports
+
+
+
+ Import channel and keyword lists into the current profile.
+
+
+ Use CSV, text, raw HTTPS lists, or rule-list JSON. FilterTube previews parsed rows before anything changes.
+
+
Add a channel by UC... or @handle or /c/Name or /user/Name. Channels blocked from the YouTube 3-dot menu will appear here automatically.
-
Channel list imports
-
Parent/account profiles can use Lists in Family Controls to view, import, pause, resume, check, refresh, or remove channel lists for protected profiles. URL lists load into the same preview box before anything changes. FilterTube previews valid channel identifiers, skips name-only rows for safety, applies the list to selected protected profiles, and can send changed rules to verified devices. Paused lists stay saved but stop contributing channel rules. URL-backed lists show source title/version when a list provides it, last-checked and hash metadata, show when they need refresh, can be checked one at a time, only when stale, or together from the Lists menu. If the source hash is unchanged, FilterTube updates checked metadata without replacing channel rows.
+
Rule list imports
+
Open Settings > Import / Export > Rule list imports. Choose Main YouTube, YouTube Kids, or Both, then preview a CSV, text, raw HTTPS list, or rule-list JSON before applying it. Imported rows are then reviewed and edited from the normal Main/Kids rule pages.
Filter All Content (per-channel)
@@ -1304,7 +1397,7 @@
Nanah Sync
A live Nanah session is not meant to survive a page refresh, browser restart, or tab close. Refresh ends the live pairing session.
Trusted links are saved locally so you do not lose the remembered relationship, but today you still start a fresh live session when reconnecting.
The trusted-device card now has a Start New Session shortcut that starts the next fresh session with the saved role/policy defaults.
-
Saved trust means a faster next session. If a later-update service is available, a protected profile can check for signed parent updates when it opens without adding YouTube page background work.
+
Saved trust means a faster next live P2P session. If an advanced offline-pickup service is separately configured, a protected profile can check for signed parent updates when it opens without adding YouTube page background work.
If you try to reuse the same pairing code on the same active device session, FilterTube now blocks that locally instead of silently burning the code.
If no fixed target is saved on the managed replica link, non-full sync still writes into the receiver's current active profile at receive time.
If a managed replica link is saved with Always this local profile, future managed updates land in that pinned local profile even when some other profile is active on the receiving device.
@@ -1351,7 +1444,7 @@
Nanah Sync
What the relay does
-
Nanah’s relay is only there to help both devices meet and exchange connection setup data. It is not meant to be a place where FilterTube reads your synced rules. After the secure handshake, your settings are meant to move directly device-to-device. The underlying project is open here: github.com/varshneydevansh/nanah.
+
Nanah’s relay is only there to help both devices meet and exchange connection setup data. It is not a mailbox and it is not meant to read or store synced rules. In the normal parent flow, open both devices, pair, verify the phrase, and send directly device-to-device. The underlying project is open here: github.com/varshneydevansh/nanah.
@@ -1474,9 +1567,9 @@
Profiles, PINs, and Child Profiles
-
Import a channel list for protected profiles
+
Import a rule list for protected profiles
- In Accounts & Sync, use Lists on a protected profile or selected protected profiles. View existing imported lists first, or choose Import List, paste a list, load a public HTTPS list URL, or choose a text file, preview the valid channels, choose Main YouTube, YouTube Kids, or both, then apply after parent/account unlock. Family Controls shows which protected profiles have imported lists, when they were last checked, and a compact source hash. URL-backed lists also show when they need refresh. Use Lists again to pause or resume a list, check one URL-backed list, check only stale URL-backed lists, check all URL-backed lists in one parent-approved pass, or remove an imported list; manual rules stay untouched. Unchanged source hashes only update checked metadata.
+ From the parent/account profile, open a child row and choose Edit Rules. Then use Settings > Import / Export > Rule list imports and choose Main, Kids, or Both. Imported files only add channel/keyword rules; they cannot change PINs, trusted devices, viewing spaces, or sync targets. After import, use the protected profile row to send the updated policy to a verified device.
@@ -1488,7 +1581,7 @@
Profiles, PINs, and Child Profiles
Remote updates in plain words
- Use live P2P when both devices are open. Use Later Updates only when a protected device should receive parent changes after it opens later. Use Same-Network only when you run a trusted local gateway. None of these options lets a protected profile edit its own rules.
+ Normal parent control is live P2P: open parent and protected devices, pair, verify the phrase, then send the selected profile update. Saved trust remembers the verified parent-child relationship and target profile for the next live session. Offline pickup and same-network gateways are advanced optional delivery tools only if you run a compatible service; they are not required for ordinary parent control and they never let a protected profile edit its own rules.