feat: clone Opal's rules feature with hard block enforcement

Rebuild the app around recurring screen-time blocking rules modeled on
Opal's My Apps tab:

- Onboarding with FamilyControls Screen Time authorization
- Apps home with Blocked Apps tiles and live rule cards
- New Rule sheet: Schedule/Time Limit/Open Limit types + preset gallery
- Rule editors: time windows (incl. overnight), day picker, app selection
  via FamilyActivityPicker (Block/Allow Only), rename, Hold to Commit
- Hard Mode: active hard rules cannot be edited, disabled, deleted, or
  unblocked until their window ends; soft rules pause until next window
- Shield enforcement through per-rule ManagedSettingsStore + RuleEnforcer
- 73 unit tests (Swift Testing) + 16 UI tests (XCUITest) with launch-arg
  harness for in-memory storage, mocked authorization, seeded scenarios
- docs/RULES_FEATURE_SPEC.md: spec derived from the reference recording

Known gap: background window transitions and time/open-limit thresholds
need a DeviceActivityMonitor extension; shields currently sync while the
app is running.
This commit is contained in:
2026-06-12 12:35:52 -04:00
parent c760aa696f
commit 3aac2004d2
45 changed files with 3749 additions and 206 deletions

316
docs/RULES_FEATURE_SPEC.md Normal file
View File

@@ -0,0 +1,316 @@
# Severed — "Rules" Feature Spec (derived from Opal screen recording)
Source: `ScreenRecording_06-12-2026 11-26-19_1.MP4` (iPhone, 4:29). The rules feature
appears roughly between 1:003:00 of the recording, on Opal's "My Apps" tab.
This spec describes what was observed, then maps it onto an implementation plan
for Severed.
---
## 1. Concept
A **Rule** is a recurring, automated app-blocking policy. Unlike a one-off
block/timer session, a rule re-arms itself on a schedule. Three rule types are
offered (Opal's "New Rule" sheet):
| Type | Icon | Example shown | Semantics |
|------|------|---------------|-----------|
| **Schedule** | calendar grid | "e.g. 9-5, Daily" | Block selected apps during a daily time window on chosen days |
| **Time Limit** | hourglass | "e.g. 45m/day" | After N minutes of cumulative use of selected apps per day, block them until a reset point |
| **Open Limit** | padlock | "e.g. 5 opens/day" | After N opens of selected apps per day, block them (not demoed in video; inferred from card) |
Common attributes across all types:
- **Name** — user-editable, free text (presets: "Work Time", "Weekend Zen", "Locked In", "Sleep", "Wind Down", "Deep Sleep", "Laser Focus", "Reading Time", "Gym Time"; defaults for new rules: "In the Zone" (schedule), "Time Keeper" (time limit))
- **Days of week** — 7-day toggle set, summarized as "Weekdays" / "Weekends" / "Every day" / custom
- **App selection** — apps, categories, and websites; selection mode is either **Block** (block selected) or **Allow Only** (block everything except selected)
- **Hard Mode** — boolean, PRO-gated in Opal; subtitle "No unblocks allowed". When off, the rule detail shows "Unblocks allowed: Yes"
- **Enabled/disabled** — a rule can be disabled without deleting ("Disable Rule")
Derived status (drives card/detail UI):
- **Active** → countdown to window end: green pill "6h left"
- **Inactive** → countdown to next activation: "Starts in 22h" / "Starts in 11h"
---
## 2. Screen inventory & navigation map
```
Tab bar: [Home] [My Apps] [Timer]
Apps screen (large title "Apps")
├── "Blocked Apps" section
├── "Rules >" section ──ta p "+ New"──▶ New Rule sheet
│ │ ├── tap rule-type card ─▶ Rule Editor (blank/default)
│ │ └── tap preset card ───▶ Rule Editor (pre-filled)
│ └── tap rule card ─▶ Rule Detail sheet
│ └── "Edit Rule" ─▶ Rule Editor (edit mode)
│ └── "Selected Apps >" ─▶ App Picker
└── "Apps" section (folders: Distracting / Always Allowed / Never Allowed)
```
All rules UI is presented as **sheets stacked over the Apps screen** (dimmed,
blurred background; grabber at top; circular ✕ or button top-left). Nothing
navigates by push except within the sheet stack.
---
## 3. Screens in detail
### 3.1 Apps screen ("My Apps" tab)
Dark theme throughout (near-black background, very dark green tint).
1. **Large title** "Apps".
2. **Blocked Apps** — section header; horizontal row of currently-blocked app
icons. Each icon has a lock badge overlay and a teal/green rounded-rect
outline; caption "Unblock" under the icon. Tapping unblocks (with friction
if hard mode — not demoed).
3. **Rules** — header row: "Rules " (leading, tappable to a full list,
presumably) and "**+ New**" (trailing, green tint) which opens the New Rule
sheet.
- Content: horizontally scrolling row of **rule cards** (~2 visible).
- **Rule card** anatomy (rounded ~24pt corners):
- Top: icon pair — rule-type icon (calendar) → small arrow → shield icon.
Active rule: icons in color, card tinted dark green. Inactive: greyscale.
- Middle: status — active: green capsule pill "6h left"; inactive: plain
text "Starts in 22h".
- Bottom: rule **name** (semibold), then a sub-row "Block" + tiny cluster
of the blocked app icons.
4. **Apps** — section of folder-style groups: "Distracting (4 items)" showing
a 2×2 mini icon grid, "Always Allowed", "Never Allowed (Hidden)" with an
eye-slash glyph. (Out of scope for the rules clone but shares the app
selection model.)
### 3.2 Rule Detail sheet
Presented on tapping a rule card. Partial-height card sheet.
- Top-left: circular ✕ close button.
- Centered: icon pair (rule type → shield), then caption
"`Schedule, Starts in 22h`" (type + live status), then large title
("Weekend Zen").
- **Detail rows** (single inset rounded card, label left / value right):
| Label | Example value |
|---|---|
| During this time | `09:00 12:00` |
| On these days | `Weekends` |
| Block | `[app icons] 1 App` / `3 Apps` |
| Unblocks allowed | `Yes` (hidden/`No` when Hard Mode) |
- Bottom: full-width white pill button "**✎ Edit Rule**" → morphs the sheet
into the Rule Editor in edit mode.
### 3.3 New Rule sheet
Presented from "+ New". Full-height sheet, scrollable.
- Header: ✕ left, centered title "**New Rule**".
- **Rule type row** — 3 horizontally arranged cards (Schedule / Time Limit /
Open Limit), each: glyph, bold name, example caption ("e.g. 9-5, Daily",
"e.g. 45m/day", "e.g. 5 opens/day"). Tapping opens the matching editor with
defaults.
- **Preset gallery** — vertically scrolling sections, each with a bold header
+ grey subtitle, containing a 2-up grid of photo-backed preset cards:
- **Get More Done** — "Maximize your productivity while staying sane."
- Work Time (Schedule 09:0017:00, Block, weekdays)
- Laser Focus (Schedule 14:0018:00, Block)
- **Sleep, Relax and Reset** — "Sleep better, rise refreshed."
- Wind Down (Schedule 20:0022:00, Block)
- Deep Sleep (Schedule 22:0006:00, Block)
- **Build Healthy Habits** — "Spend time on important things."
- Reading Time (Schedule ~19:4520:45, Block)
- Gym Time (Schedule 17:3018:30, Block)
- **Preset card** anatomy: full-bleed background photo, top row icon pair
(type → shield), time range caption, name, "Block" + suggested app icons,
and a circular "+" button bottom-right. Tapping anywhere opens the
Schedule editor pre-filled with the preset's name/times/days.
### 3.4 Rule Editor — Schedule type
Sheet with: back (top-left), centered **rule name** as title, ✎ pencil
button (top-right) to rename.
Sections (each an inset rounded group with a small icon + caption header):
1. **📅 During this time**
- Rows `From` / `To` with right-aligned time + stepper chevrons (`09:00 ⌃⌄`).
- A dotted vertical line with ●/○ endpoints visually links From → To.
- Tapping a row expands an inline wheel time picker (24h, demoed changing
From 22:00 → 23:00).
2. **On these days:** — trailing summary label ("Weekdays"/"Weekends"/custom);
row of 7 circular toggles `S M T W T F S`; selected = filled white circle
with black letter, unselected = dark circle.
3. **🛡 Apps are blocked**
- Row: `Selected Apps``N Apps ` — pushes the App Picker.
4. **Hard Mode** `⚡PRO` badge — subtitle "No unblocks allowed"; trailing
toggle.
5. **CTA**
- Creating: full-width gradient pill "**Hold to Commit**" — a press-and-hold
interaction (deliberate friction) that fills, then saves and dismisses to
the Apps screen where the new card appears.
- Editing existing: "**✓ Done**" pill, plus a red text button
"**⏸ Disable Rule**" beneath it.
### 3.5 Rule Editor — Time Limit type ("Time Keeper")
Same chrome (back / title / rename). Sections:
1. **⏳ When I use** — row `This App``Select ` (app selection).
2. **For this long** — subtitle "Daily"; right-aligned value with stepper
`45m ⌃⌄`.
3. **On these days:** — identical day picker as Schedule.
4. **🛡 Then block app** — row `Until` with stepper value `Tomorrow ⌃⌄`
(reset point — e.g. tomorrow/next morning).
5. **Hard Mode** toggle — same as Schedule.
6. **Hold to Commit**.
### 3.6 Rule Editor — Open Limit type
Not demoed beyond its card. Spec by analogy: "When I open [apps]" /
"More than `N opens ⌃⌄` (Daily)" / day picker / "Then block until …" /
Hard Mode / Hold to Commit.
### 3.7 App Picker (shared component — also used in onboarding & timer)
Full-height sheet:
- Header: back, centered title "**Selected**", and a circular green **✓**
confirm button top-right.
- **Segmented control**: `Block` | `Allow Only`.
- Top rows: "**+ Add App or Website**", a "Suggested" horizontal row of app
icons (one-tap add), and a "Never Allowed — 0 Apps" row with footnote
"Never allowed Apps will also be blocked".
- Hint text: *Select apps/websites, tap ">" to expand*.
- **Category list** — each row: circular checkbox (tri-state: empty /
partially-selected count / checked), emoji glyph, category name, trailing
selected-count + chevron to expand into individual apps:
`All Apps & Categories, Social, Games, Entertainment, Creativity, Education,
Health & Fitness, Information & Reading, Productivity & Finance,
Shopping & Food`.
- **Search bar** pinned near bottom (with mic). Typing surfaces app matches
and website suggestions (e.g. typing "insta" offers `instagram.com`),
letting users add arbitrary domains.
- Footer: "**N Apps Selected**" caption + white pill "**Save**" (+ "Cancel").
> Implementation note: Opal ships its own app categorization. On iOS, third
> parties cannot enumerate installed apps; the system-sanctioned route is
> `FamilyActivityPicker` (FamilyControls), which provides its own
> category/app/website UI and returns opaque tokens. **v1 of Severed should
> embed `FamilyActivityPicker`** instead of cloning Opal's custom picker, and
> keep the `Block`/`Allow Only` segmented control as our own wrapper state.
---
## 4. Behavioral spec
1. **Activation** — a Schedule rule becomes active at `From` on an enabled
day and deactivates at `To` (windows crossing midnight, e.g. 22:0006:00,
must be supported — Deep Sleep preset does this).
2. **While active** — the rule's app selection is shielded; blocked apps also
surface in the "Blocked Apps" row on the Apps screen; the card turns green
with a "Xh left" pill.
3. **Unblocking** — with Hard Mode off, the user may unblock mid-window
("Unblocks allowed: Yes"). With Hard Mode on, no unblocks until the window
ends.
4. **Time-limit rules** — accumulate usage daily across the selected apps;
on crossing the threshold, shield until the `Until` reset point
(e.g. tomorrow), then reset the budget.
5. **Disable vs delete** — "Disable Rule" pauses scheduling but keeps the
rule (card presumably shows disabled state). No delete flow was shown;
add delete via swipe/long-press or a button in the editor.
6. **Commit friction** — creating/committing a rule uses press-and-hold
("Hold to Commit"), echoing Opal's philosophy that *starting* a commitment
should be deliberate. Editing uses a plain "Done".
7. **Live countdowns** — "Starts in 22h" / "6h left" update over time
(minute granularity is fine).
---
## 5. Implementation plan for Severed
### 5.1 Frameworks & capabilities
- **FamilyControls** — `AuthorizationCenter.shared.requestAuthorization(for: .individual)`;
`FamilyActivityPicker` + `FamilyActivitySelection` (app/category/web tokens).
- **ManagedSettings** — `ManagedSettingsStore` per rule
(`ManagedSettingsStore.Name("rule-<uuid>")`); set
`store.shield.applications` / `applicationCategories` / `webDomains`.
- **DeviceActivity** — `DeviceActivityCenter.startMonitoring` with a
`DeviceActivitySchedule(intervalStart:intervalEnd:repeats:)` per rule;
a **DeviceActivityMonitor app extension** applies/removes shields in
`intervalDidStart`/`intervalDidEnd`. Time-limit rules use
`DeviceActivityEvent(applications:threshold:)` +
`eventDidReachThreshold`.
- Requires the **Family Controls entitlement** (works in dev; distribution
needs Apple approval) and an App Group to share rule data with the monitor
extension.
### 5.2 Data model (SwiftData)
Replace the template `AppListProfile` with:
```swift
enum RuleKind: String, Codable { case schedule, timeLimit, openLimit }
enum SelectionMode: String, Codable { case block, allowOnly }
@Model final class BlockingRule {
var id: UUID
var name: String
var kind: RuleKind
var isEnabled: Bool
var hardMode: Bool
var selectionMode: SelectionMode
var selectionData: Data // encoded FamilyActivitySelection
var days: [Int] // 1...7, Calendar weekday numbers
// schedule
var startMinutes: Int // minutes from midnight, e.g. 540 = 09:00
var endMinutes: Int // may be < start (crosses midnight)
// time limit
var dailyLimitMinutes: Int?
var resetPoint: String? // "tomorrow" | "nextMorning"
// open limit
var maxOpens: Int?
var createdAt: Date
}
```
`FamilyActivitySelection` is `Codable` → store as `Data`. Status
("active", "starts in Xh", "Xh left") is **derived**, not stored.
### 5.3 View inventory
| View | Notes |
|---|---|
| `AppsView` (tab) | Sections: Blocked Apps, Rules carousel, (later) app folders |
| `RuleCardView` | Card per §3.1, active/inactive styling |
| `RuleDetailSheet` | §3.2, rows + Edit Rule |
| `NewRuleSheet` | §3.3, type cards + preset gallery (`RulePreset` static data) |
| `ScheduleRuleEditor` | §3.4 |
| `TimeLimitRuleEditor` | §3.5 |
| `DayOfWeekPicker` | 7 circle toggles + summary ("Weekdays"/"Weekends"/…) |
| `AppSelectionView` | wraps `FamilyActivityPicker`, Block/Allow Only segmented control |
| `HoldToCommitButton` | long-press progress fill, haptics, fires on completion |
| `RuleScheduler` (service) | translates `BlockingRule` ⇄ DeviceActivity monitoring |
| `ShieldController` (service) | applies/clears `ManagedSettingsStore` shields |
### 5.4 Suggested build order
1. Data model + Apps tab with Rules section (cards from seeded sample rules,
status derivation, detail sheet) — pure UI, no entitlements needed.
2. New Rule sheet + Schedule editor + day picker + Hold to Commit (CRUD into
SwiftData; Disable/Done editing path).
3. FamilyControls authorization + `FamilyActivityPicker` integration
("Selected Apps" row, "N Apps" counts, icon clusters via `Label(token:)`).
4. DeviceActivity monitor extension + ManagedSettings shields (real blocking,
incl. midnight-crossing windows).
5. Time Limit editor + threshold events; Open Limit last (needs shield
action extension + open counting).
6. Preset gallery content + polish (gradients, photos, haptics, live
countdown timers).
### 5.5 Out of scope (seen in video, not part of "rules")
- Onboarding flow, paywall ("You know Opal works. Make it permanent."),
Home tab gem/score UI, Timer tab (one-off focus sessions, "Leave Early?"
friction screen), notification nudges ("Complete Your Setup").