▍ humdrum codex / soft

Settings: fixed source set — toggle to hide, no add/remove (issue 4)

aa2ed843b70aa003470cb5fb98a2d608023793f6
humdrum-tiv <45084903+humdrum-tiv@users.noreply.github.com> · 2026-06-09 11:52

parent 1d3c8f7f

Settings: fixed source set — toggle to hide, no add/remove (issue 4)

Source cards can no longer be added or deleted; every seeded source
always exists and is just unchecked to hide it from the dashboard.
Per-card Configure buttons already cover setup, so the confusing
"Add [kind] [label]" row and the per-row ✕ delete button are gone.
Added a one-line hint under the Sources list.

Also mark issue 3 (calendar icalBuddy/EventKit path, shipped 13972f7)
and issue 4 done in ISSUES.md.

POST /api/sources and DELETE /api/sources/[id] routes remain but are
now unused by the UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

2 files changed

ISSUES.md +15 −13
@@ -29,22 +29,24 @@   covers both kinds; seeded in `scripts/seed.ts`.
 - Friendlier "Add RSS feed" affordance still rides on the issue 4 Add-source UX rework; the
   empty card currently shows the generic "Needs configuration → settings".
 
-## 3. Calendar still has issues
+## 3. Calendar still has issues ✅ done 2026-06-07
 
-Local Calendar.app reading is unreliable.
-- Large/subscribed calendars (e.g. sports fixtures) make the AppleScript `whose` query slow →
-  hits the 60s timeout. `lib/sources/modules/calendar.ts`.
-- Investigate faster paths: `icalBuddy` if installed, or EventKit via a small helper, instead
-  of Calendar.app AppleScript. Fall back to AppleScript only if needed.
-- Verify events actually render across calendars/time zones; confirm all-day handling.
-- Possibly cap per-calendar scan window or events count.
+Was: local Calendar.app `whose`-query slow → 60s timeout on big/subscribed calendars.
+- ✅ `calendar.ts` now prefers `icalBuddy` (reads EventKit directly — fast), found at
+  `/opt/homebrew/bin/icalBuddy` or `/usr/local/bin/icalBuddy` via `lib/mac.ts`.
+- ✅ Falls back to the AppleScript `whose` query only when icalBuddy isn't installed.
+- Shipped in commit `13972f7` "Calendar updates".
 
-## 4. Add-source field (bottom of Settings) doesn't make sense
+## 4. Add-source field (bottom of Settings) doesn't make sense ✅ done 2026-06-09
 
-The "Add [kind] [label]" row at the bottom of Settings → Sources is confusing.
-- Rethink the UX: clearer labels, grouping, or a different entry point (e.g. "+ Add a card"
-  with a friendly picker that explains each source).
-- `components/SettingsView.tsx` → `AddSource`.
+Resolved by making the source set fixed instead of add/remove.
+- ✅ Removed the confusing "Add [kind] [label]" row (`AddSource`) entirely.
+- ✅ Removed the per-row ✕ delete button. Cards are no longer added or removed — every
+  seeded source always exists; you just **uncheck** to hide it from the dashboard. Per-card
+  Configure buttons handle setup.
+- Added a one-line hint under the Sources list explaining this.
+- `components/SettingsView.tsx`. (`addSource`/`removeSource` handlers dropped; the
+  `POST /api/sources` + `DELETE /api/sources/[id]` routes remain but are now unused by the UI.)
 
 ## 5. Refresh interval — minutes/hours, not just seconds
 
components/SettingsView.tsx +5 −70
@@ -7,7 +7,7 @@ import { downloadExport } from "@/lib/export";
 import { uploadImport } from "@/lib/import";
 import type { SourceState } from "@/lib/types";
 import type { Settings } from "@/lib/schemas/setting";
-import { SOURCE_SIZES, SOURCE_KINDS } from "@/lib/schemas/source";
+import { SOURCE_SIZES } from "@/lib/schemas/source";
 import { SIZE_LABELS } from "@/lib/layout";
 
 const cardStyle = {
@@ -76,20 +76,6 @@     await Promise.all([
       patchSource(a.source.id, { position: b.source.position }),
       patchSource(b.source.id, { position: a.source.position }),
     ]);
-  }
-
-  async function addSource(kind: string, label: string) {
-    await fetch("/api/sources", {
-      method: "POST",
-      headers: { "Content-Type": "application/json" },
-      body: JSON.stringify({ kind, label }),
-    });
-    await load();
-  }
-
-  async function removeSource(id: string) {
-    await fetch(`/api/sources/${id}`, { method: "DELETE" });
-    await load();
   }
 
   return (
@@ -213,11 +199,13 @@               state={it}
               onPatch={(patch) => patchSource(it.source.id, patch)}
               onUp={i > 0 ? () => move(i, -1) : undefined}
               onDown={i < sources.length - 1 ? () => move(i, 1) : undefined}
-              onDelete={() => removeSource(it.source.id)}
             />
           ))}
         </div>
-        <AddSource onAdd={addSource} />
+        <p className="mt-3 text-xs" style={{ color: "var(--text-faint)" }}>
+          Uncheck a card to hide it from the dashboard. Every source stays available — nothing
+          is ever removed.
+        </p>
       </Section>
 
       <Section title="Data">
@@ -255,13 +243,11 @@   state,
   onPatch,
   onUp,
   onDown,
-  onDelete,
 }: {
   state: SourceState;
   onPatch: (patch: Record<string, unknown>) => void;
   onUp?: () => void;
   onDown?: () => void;
-  onDelete?: () => void;
 }) {
   const { source } = state;
   const [open, setOpen] = useState(false);
@@ -327,60 +313,9 @@           >
             {open ? "Close" : "Configure"}
           </button>
         )}
-        {onDelete && (
-          <button
-            className="text-xs px-1"
-            style={{ color: "var(--text-faint)" }}
-            aria-label={`Delete ${source.label}`}
-            onClick={() => {
-              if (confirm(`Remove "${source.label}" from the dashboard?`)) onDelete();
-            }}
-          >
-            ✕
-          </button>
-        )}
       </div>
       {open && configurable && <SourceConfig state={state} onPatch={onPatch} />}
     </div>
-  );
-}
-
-function AddSource({ onAdd }: { onAdd: (kind: string, label: string) => void }) {
-  const [kind, setKind] = useState<string>("news");
-  const [label, setLabel] = useState("");
-
-  return (
-    <form
-      className="flex gap-2 items-center mt-4 pt-4"
-      style={{ borderTop: "1px solid var(--border)" }}
-      onSubmit={(e) => {
-        e.preventDefault();
-        const l = label.trim() || kind;
-        onAdd(kind, l);
-        setLabel("");
-      }}
-    >
-      <span className="text-xs uppercase tracking-widest" style={{ color: "var(--text-muted)" }}>
-        Add
-      </span>
-      <select value={kind} onChange={(e) => setKind(e.target.value)} className="text-sm px-2 py-2" style={inputStyle}>
-        {SOURCE_KINDS.filter((k) => k !== "todos").map((k) => (
-          <option key={k} value={k}>
-            {k}
-          </option>
-        ))}
-      </select>
-      <input
-        value={label}
-        onChange={(e) => setLabel(e.target.value)}
-        placeholder="Label (e.g. My Feeds)"
-        className="flex-1 px-3 py-2 text-sm"
-        style={inputStyle}
-      />
-      <button type="submit" className="px-3 py-2 text-sm" style={inputStyle}>
-        Add source
-      </button>
-    </form>
   );
 }