max / goingson
- Co-Authored-By
- Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 files changed,
+25 insertions,
-479 deletions
| @@ -8,32 +8,33 @@ | |||
| 8 | 8 | ### Deferred to post-launch (audited but not blocking) | |
| 9 | 9 | ||
| 10 | 10 | **rust-fuzz** | |
| 11 | - | - `src-tauri/src/commands/email.rs:598` — `std::fs::read` for attachments inside async command; wrap in `spawn_blocking` (pattern already at email.rs:422 and email_sync.rs:224). | |
| 12 | - | - `src-tauri/src/email/smtp_client.rs:291` — `ContentType::parse("application/octet-stream").unwrap()` on a static; replace with infallible mime constant. | |
| 13 | - | - `src-tauri/src/state.rs:173`, `commands/sync.rs:{75,177,203}`, `commands/oauth.rs:{131,166}`, `commands/email_sync.rs:49` — `.expect("...poisoned")` on `std::sync::Mutex`; switch to `unwrap_or_else(|e| e.into_inner())` since protected data is recoverable. | |
| 14 | - | - `src-tauri/src/db_watcher.rs:61` — shutdown latency = `MIN_EVENT_INTERVAL_MS`, thread unjoinable. Cosmetic. | |
| 11 | + | - ~~`email.rs:598` blocking `fs::read` in async.~~ Done 2026-06-02. Switched to `tokio::fs::read(...).await` — simpler than the `spawn_blocking` recipe and matches the existing async-IO style for tokio::fs::write a few lines above. | |
| 12 | + | - ~~`smtp_client.rs:291` `ContentType::parse(...).unwrap()`.~~ Done 2026-06-02. Replaced unwrap with `or_else` parse + `map_err(...)` to propagate as a real error. `lettre::ContentType` has no public const for octet-stream; this is the closest no-panic path. | |
| 13 | + | - ~~Mutex `.expect("poisoned")` sweep.~~ Done 2026-06-02. All 7 callsites (`state.rs:173`, `sync.rs:{75,177,203}`, `oauth.rs:{131,166}`, `email_sync.rs:49`) switched to `unwrap_or_else(|e| e.into_inner())`. `sync.rs:75` is an `RwLock::read`; the same pattern works. | |
| 14 | + | - `db_watcher.rs:61` — shutdown latency = `MIN_EVENT_INTERVAL_MS`, thread unjoinable. Cosmetic. Leaving. | |
| 15 | 15 | ||
| 16 | 16 | **use-fuzz** | |
| 17 | - | - `src-tauri/frontend/js/seed-data.js:393` — production `console.log` for demo seed instructions; strip from prod bundles or gate on debug flag. | |
| 18 | - | - `src-tauri/frontend/js/app.js:{12,28,37,...}` — chatty `console.log` on every launch; trim or gate. | |
| 19 | - | - `src-tauri/frontend/js/{emails,tasks,task-board}.js:*` — bare "Failed to load" without recovery actions; add "Try again" buttons per `ERROR_CODE_HINTS` pattern. | |
| 20 | - | - `src-tauri/frontend/js/settings.js:196` — empty-plugins hint hardcodes `~/.config/goingson/plugins/` (Linux path); platform-detect or replace with doc link. | |
| 21 | - | - `src-tauri/frontend/index.html:603` — task drawer close button uses literal `x` not `×`. | |
| 22 | - | - `src-tauri/frontend/index.html:18-57` — `?ui=mobile` URL param + localStorage override not gated for production per `ui_mode_separation_plan.md` Phase 5; risk of users landing in untested mobile UI on desktop. | |
| 17 | + | - ~~`seed-data.js:393` prod `console.log`.~~ Done 2026-06-02. Removed. | |
| 18 | + | - ~~`app.js` chatty launch logs.~~ Done 2026-06-02. Stripped the four informational `console.log` calls (Desktop loaded, Loaded N projects, Loaded N accounts, external-change/sync-changes/modal-skip/refresh-success). All `console.error` retained. | |
| 19 | + | - ~~`{emails,tasks,task-board}.js` bare "Failed to load".~~ Done 2026-06-02. All four inline error blocks (tasks.js:126, emails.js:105, events.js:419, task-board.js:92) now embed a "Try again" `.btn-link` that calls the corresponding load fn. The toast-side Retry action already existed; this is the parallel in-place recovery. | |
| 20 | + | - ~~`settings.js:196` Linux plugin path.~~ Done 2026-06-02. Replaced `~/.config/goingson/plugins/` with neutral OS-agnostic phrasing ("inside your OS app-data location"). | |
| 21 | + | - ~~`index.html:603` literal `x` close button.~~ Done 2026-06-02. Now `×`. | |
| 22 | + | - `index.html:18-57` — `?ui=mobile` prod lockdown. Deferred — same item as §K Phase 6, blocks on a desktop-mobile distribution channel existing. | |
| 23 | 23 | ||
| 24 | 24 | **creator-fuzz** | |
| 25 | - | - `CHANGELOG.md:24-26` — "Fixed: all issues identified in audit runs 1-12" is internal-process language; replace with concrete user-visible fixes. | |
| 26 | - | - `docs/data-export.md` (missing) — document JSON backup schema so "portable" is verifiable. | |
| 27 | - | - `tauri.conf.json:48-54` — add Settings toggle "Check for updates on launch" for parity with privacy-policy framing (currently no opt-out, though install is user-initiated). | |
| 28 | - | - README has no pricing / commercial-license-contact line for PolyForm-NC. | |
| 25 | + | - ~~`CHANGELOG.md:24-26` audit-runs language.~~ Done 2026-06-02. Replaced "all issues identified in audit runs 1–12" with six concrete user-visible fixes (empty-state copy, About/Settings populated, hard-fail copy, bulk-action danger color, mutex poisoning recovery, SMTP unwrap fix). | |
| 26 | + | - ~~`docs/data-export.md`.~~ Done 2026-06-02. Written. Covers all five export formats (JSON, gzipped backup, CSV, ICS, raw SQLite), the top-level JSON schema with version + `exportedAt`, per-entity field summaries pointing at `crates/core/src/` for full shapes, denormalized-field caveats, restore semantics (replace vs merge), a verification recipe a user can run with `gunzip` + a text editor, and the not-exported list (OAuth tokens, TOTP secrets, sync state, window state). Source-of-truth pointer at the bottom. | |
| 27 | + | - ~~Settings "Check for updates on launch" toggle.~~ Done 2026-06-02. New `commands::preferences` module persists a tiny JSON pref file in the Tauri app config dir (`preferences.json`, `update_check_on_launch: bool`, default true). Two commands wired: `get_preferences`, `set_update_check_on_launch`. `main.rs` setup hook gates the 10s-delay updater check on the preference. Settings → About now shows a toggle row (new `.settings-toggle-row` primitive) replacing the static "Updates are checked automatically on launch" line; toast confirms the save. API wrapper: `GoingsOn.api.preferences.{get, setUpdateCheckOnLaunch}`. Tauri config (`createUpdaterArtifacts`, endpoints, pubkey) untouched — the toggle gates the check, not the plugin. | |
| 28 | + | - ~~README PolyForm-NC commercial contact.~~ Done 2026-06-02. License section now invites commercial-use inquiries at `info@makenot.work`. | |
| 29 | 29 | ||
| 30 | 30 | **ux-audit** | |
| 31 | - | - `styles.css:590-660` — `.btn`/`.btn-primary`/`.btn-danger` lack the offset-shadow brand cue that `.card` and `.bulk-actions-bar` use; controls look flatter than chrome. One-pass fix on interactive primitives also resolves disabled-state contrast and `.pill.active`/`.tab.active` lift issues. | |
| 32 | - | - `styles.css:615` — disabled-button signal is opacity-only; swap to `transparent bg + muted color/border`. | |
| 33 | - | - `styles.css:4712` — `.sync-label` hidden in `ui-mode-mobile` leaves dot color as the only state cue; add shape variation or keep label for `error`/`warn`. | |
| 34 | - | - Single-item destructive actions (`tasks.js:657` etc.) lack `.btn-loading` during round-trip. | |
| 35 | - | - `index.html:466` straggler `row-flex-2` vs neighbors using `.row-flex row-flex-N` — will be caught by ongoing CSS dedup. | |
| 36 | - | - Back-arrow buttons inconsistent: `← Back` (index.html:220, 538) vs literal `←` (index.html:270). | |
| 31 | + | - ~~`styles.css:590-660` — `.btn` offset-shadow brand cue.~~ Done 2026-06-02. `.btn` base now carries `--shadow-brutal-xs` (1px). Hover lifts to 3px shadow + `translate(-1px,-1px)`; active presses to `translate(1px,1px)` + no shadow. Transition extended to include `transform` and `box-shadow`. `.btn-primary` / `.btn-danger` inherit. `.btn-link` and `.btn-icon` are sibling primitives (not `.btn` extenders) so unaffected. `.bulk-actions-bar .btn` inherits cleanly. | |
| 32 | + | - ~~`styles.css:615` — disabled-button contrast.~~ Done 2026-06-02. Opacity dropped; now `transparent bg + muted color + muted border + no shadow`. | |
| 33 | + | - `.pill.active` / `.tab.active` lift — Done 2026-06-02. Both gained `--shadow-brutal-xs` to distinguish active from inactive chrome. | |
| 34 | + | - ~~`styles.css:4712` — `.sync-label` mobile shape variation.~~ Done 2026-06-02. Shape now encodes state on the dot itself (works in mobile + monochrome + high-contrast): `connected`/`syncing` stay circular; `warn` is a rotated square (diamond); `error` is a square. Label-hiding behavior unchanged. | |
| 35 | + | - ~~Single-item destructive `.btn-loading`.~~ Reviewed 2026-06-02 — not applicable. Audit cited `tasks.js:657` but every single-item destructive flow (`tasks.js`, `projects.js`, `events.js`, `emails.js`) is optimistic with `showUndoToast`: the UI removes the item the instant the confirm modal closes, so there is no button still mounted to carry a loading state. Optimism + undo is a stronger UX than a spinner. Closing without code change. | |
| 36 | + | - ~~`index.html:466` straggler `row-flex-2`.~~ Done 2026-06-02. Now matches neighbors as `row-flex row-flex-2`. | |
| 37 | + | - ~~Back-arrow consistency.~~ Done 2026-06-02. `index.html:270` literal `← Back` → `← Back` matching the other three back-arrow buttons. | |
| 37 | 38 | ||
| 38 | 39 | ### §3.1 — DONE for emoji/About/error-copy. Remaining: | |
| 39 | 40 | - Mobile UI mode override hardening (see use-fuzz deferral above). | |
| @@ -41,8 +42,10 @@ | |||
| 41 | 42 | ### §3.2 distribution — NOT STARTED | |
| 42 | 43 | - Signed builds + notarization stapled + DMG smoke test on a clean macOS account. | |
| 43 | 44 | ||
| 44 | - | ### §3.3 repo hygiene — NOT STARTED | |
| 45 | - | - Resolve stray root planning files (`css_refactor_plan_2026-05-24.md`, `css_refactor_plan.md`, `css_state_audit.md`, `ui_mode_separation_plan.md`); decide on `generate_pitch_pdf.py` + `GoingsOn-Pitch.pdf`; review `_archive/`. | |
| 45 | + | ### §3.3 repo hygiene — DONE 2026-06-02 | |
| 46 | + | - Stray root planning files: already moved to `docs/_archive/` pre-session. | |
| 47 | + | - `generate_pitch_pdf.py` + `GoingsOn-Pitch.pdf` deleted (regenerable; git preserves). | |
| 48 | + | - Root `_archive/` (pre-Tauri Rust HTTP server) deleted; migration shipped 2026-06-01, git history is the right home for pre-migration reference. | |
| 46 | 49 | ||
| 47 | 50 | CSS dedup work below is the active in-progress stream; launch readiness is its own track. | |
| 48 | 51 |
Binary file
| @@ -1,457 +1,0 @@ | |||
| 1 | - | #!/usr/bin/env python3 | |
| 2 | - | """Generate GoingsOn pitch PDF.""" | |
| 3 | - | ||
| 4 | - | from fpdf import FPDF | |
| 5 | - | ||
| 6 | - | # Colors | |
| 7 | - | BG = (232, 244, 248) # Light cyan background | |
| 8 | - | TEXT = (27, 54, 93) # Dark blue text | |
| 9 | - | ACCENT = (247, 209, 84) # Yellow accent | |
| 10 | - | MUTED = (100, 120, 145) # Muted text | |
| 11 | - | WHITE = (255, 255, 255) | |
| 12 | - | DARK = (30, 40, 55) | |
| 13 | - | GREEN = (92, 184, 92) | |
| 14 | - | SECTION_BG = (245, 250, 252) | |
| 15 | - | ||
| 16 | - | ||
| 17 | - | class PitchPDF(FPDF): | |
| 18 | - | def header(self): | |
| 19 | - | pass | |
| 20 | - | ||
| 21 | - | def footer(self): | |
| 22 | - | if self.page_no() > 1: | |
| 23 | - | self.set_y(-15) | |
| 24 | - | self.set_font("Helvetica", "I", 8) | |
| 25 | - | self.set_text_color(*MUTED) | |
| 26 | - | self.cell(0, 10, f"GoingsOn | goingson.app | Page {self.page_no()}", align="C") | |
| 27 | - | ||
| 28 | - | def section_title(self, title): | |
| 29 | - | self.set_font("Helvetica", "B", 16) | |
| 30 | - | self.set_text_color(*TEXT) | |
| 31 | - | self.cell(0, 10, title, new_x="LMARGIN", new_y="NEXT") | |
| 32 | - | # Yellow underline | |
| 33 | - | self.set_draw_color(*ACCENT) | |
| 34 | - | self.set_line_width(1.5) | |
| 35 | - | self.line(self.l_margin, self.get_y(), self.l_margin + 50, self.get_y()) | |
| 36 | - | self.ln(6) | |
| 37 | - | ||
| 38 | - | def feature_block(self, icon, title, bullets): | |
| 39 | - | y_start = self.get_y() | |
| 40 | - | # Title | |
| 41 | - | self.set_font("Helvetica", "B", 12) | |
| 42 | - | self.set_text_color(*TEXT) | |
| 43 | - | self.cell(0, 7, f"{icon} {title}", new_x="LMARGIN", new_y="NEXT") | |
| 44 | - | self.ln(1) | |
| 45 | - | # Bullets | |
| 46 | - | self.set_font("Helvetica", "", 9.5) | |
| 47 | - | self.set_text_color(*MUTED) | |
| 48 | - | for bullet in bullets: | |
| 49 | - | self.set_x(self.l_margin + 6) | |
| 50 | - | self.multi_cell(self.epw - 6, 4.5, f"- {bullet}", new_x="LMARGIN", new_y="NEXT") | |
| 51 | - | self.ln(3) | |
| 52 | - | ||
| 53 | - | def two_col_features(self, left, right): | |
| 54 | - | col_w = (self.epw - 8) / 2 | |
| 55 | - | y_start = self.get_y() | |
| 56 | - | ||
| 57 | - | # Left column | |
| 58 | - | self.set_x(self.l_margin) | |
| 59 | - | self.feature_col(left, col_w) | |
| 60 | - | y_left = self.get_y() | |
| 61 | - | ||
| 62 | - | # Right column | |
| 63 | - | self.set_y(y_start) | |
| 64 | - | self.set_x(self.l_margin + col_w + 8) | |
| 65 | - | self.feature_col(right, col_w) | |
| 66 | - | y_right = self.get_y() | |
| 67 | - | ||
| 68 | - | self.set_y(max(y_left, y_right)) | |
| 69 | - | ||
| 70 | - | def feature_col(self, items, width): | |
| 71 | - | x_start = self.get_x() | |
| 72 | - | for icon, title, bullets in items: | |
| 73 | - | self.set_x(x_start) | |
| 74 | - | self.set_font("Helvetica", "B", 11) | |
| 75 | - | self.set_text_color(*TEXT) | |
| 76 | - | self.cell(width, 7, f"{icon} {title}", new_x="LMARGIN", new_y="NEXT") | |
| 77 | - | self.ln(1) | |
| 78 | - | self.set_font("Helvetica", "", 9) | |
| 79 | - | self.set_text_color(*MUTED) | |
| 80 | - | for bullet in bullets: | |
| 81 | - | self.set_x(x_start + 5) | |
| 82 | - | self.multi_cell(width - 5, 4.2, f"- {bullet}", new_x="LMARGIN", new_y="NEXT") | |
| 83 | - | self.ln(3) | |
| 84 | - | ||
| 85 | - | ||
| 86 | - | def build_pdf(): | |
| 87 | - | pdf = PitchPDF() | |
| 88 | - | pdf.set_auto_page_break(auto=True, margin=20) | |
| 89 | - | pdf.set_margins(20, 15, 20) | |
| 90 | - | ||
| 91 | - | # ========================================================= | |
| 92 | - | # PAGE 1 - Title / Hero | |
| 93 | - | # ========================================================= | |
| 94 | - | pdf.add_page() | |
| 95 | - | ||
| 96 | - | # Background fill | |
| 97 | - | pdf.set_fill_color(*DARK) | |
| 98 | - | pdf.rect(0, 0, 210, 297, "F") | |
| 99 | - | ||
| 100 | - | # "GO" logo text | |
| 101 | - | pdf.set_y(60) | |
| 102 | - | pdf.set_font("Helvetica", "B", 72) | |
| 103 | - | pdf.set_text_color(*ACCENT) | |
| 104 | - | pdf.cell(0, 30, "GO", align="C", new_x="LMARGIN", new_y="NEXT") | |
| 105 | - | ||
| 106 | - | # App name | |
| 107 | - | pdf.set_font("Helvetica", "B", 36) | |
| 108 | - | pdf.set_text_color(*WHITE) | |
| 109 | - | pdf.cell(0, 16, "GoingsOn", align="C", new_x="LMARGIN", new_y="NEXT") | |
| 110 | - | ||
| 111 | - | # Tagline | |
| 112 | - | pdf.ln(6) | |
| 113 | - | pdf.set_font("Helvetica", "", 14) | |
| 114 | - | pdf.set_text_color(180, 195, 210) | |
| 115 | - | pdf.cell(0, 8, "Tasks. Email. Calendar. Contacts.", align="C", new_x="LMARGIN", new_y="NEXT") | |
| 116 | - | pdf.cell(0, 8, "One app. Offline-first. Native.", align="C", new_x="LMARGIN", new_y="NEXT") | |
| 117 | - | ||
| 118 | - | # Divider | |
| 119 | - | pdf.ln(12) | |
| 120 | - | pdf.set_draw_color(*ACCENT) | |
| 121 | - | pdf.set_line_width(1) | |
| 122 | - | pdf.line(70, pdf.get_y(), 140, pdf.get_y()) | |
| 123 | - | pdf.ln(12) | |
| 124 | - | ||
| 125 | - | # Value props | |
| 126 | - | pdf.set_font("Helvetica", "", 11) | |
| 127 | - | pdf.set_text_color(200, 210, 220) | |
| 128 | - | props = [ | |
| 129 | - | "Everything in one place -- no more juggling 5 apps", | |
| 130 | - | "Your data stays on your machine -- offline-first with local storage", | |
| 131 | - | "Native performance -- built with Rust, not Electron", | |
| 132 | - | "Free during alpha -- no subscriptions, no lock-in", | |
| 133 | - | "macOS + iOS -- with Windows and Linux support", | |
| 134 | - | ] | |
| 135 | - | for prop in props: | |
| 136 | - | pdf.cell(0, 7, prop, align="C", new_x="LMARGIN", new_y="NEXT") | |
| 137 | - | ||
| 138 | - | # Bottom | |
| 139 | - | pdf.set_y(255) | |
| 140 | - | pdf.set_font("Helvetica", "I", 10) | |
| 141 | - | pdf.set_text_color(120, 135, 155) | |
| 142 | - | pdf.cell(0, 6, "Alpha Release -- March 2026", align="C", new_x="LMARGIN", new_y="NEXT") | |
| 143 | - | pdf.cell(0, 6, "goingson.app", align="C", new_x="LMARGIN", new_y="NEXT") | |
| 144 | - | ||
| 145 | - | # ========================================================= | |
| 146 | - | # PAGE 2 - Core Features | |
| 147 | - | # ========================================================= | |
| 148 | - | pdf.add_page() | |
| 149 | - | ||
| 150 | - | pdf.section_title("Core Features") | |
| 151 | - | ||
| 152 | - | pdf.feature_block("TASKS", "Task Management", [ | |
| 153 | - | "Automatic urgency scoring based on priority, due date, and age", | |
| 154 | - | "Quick-add with natural language: \"Fix bug due:tomorrow +urgent @Project\"", | |
| 155 | - | "Subtasks, annotations, and milestone tracking", | |
| 156 | - | "Recurrence (daily, weekly, monthly) with auto-creation on completion", | |
| 157 | - | "Snooze tasks until later today, tomorrow, weekend, or next week", | |
| 158 | - | "Bulk operations: complete, snooze, or delete multiple tasks at once", | |
| 159 | - | "Saved views and named filters for any combination of criteria", | |
| 160 | - | ]) | |
| 161 | - | ||
| 162 | - | pdf.feature_block("EMAIL", "Email Integration", [ | |
| 163 | - | "Full IMAP/SMTP client built in -- connect any email account", | |
| 164 | - | "OAuth2 for Fastmail, Google, Microsoft, and Yahoo", | |
| 165 | - | "Native JMAP protocol support for Fastmail", | |
| 166 | - | "Threaded conversations with reader mode (strips HTML clutter)", | |
| 167 | - | "Create tasks directly from emails with source linking", | |
| 168 | - | "Auto-sync on configurable intervals (5/15/30/60 min)", | |
| 169 | - | "Compose, reply, archive, snooze -- all without leaving the app", | |
| 170 | - | ]) | |
| 171 | - | ||
| 172 | - | pdf.feature_block("CALENDAR", "Calendar & Events", [ | |
| 173 | - | "Create events with title, description, location, and time range", | |
| 174 | - | "Link events to projects and contacts", | |
| 175 | - | "Recurrence support (daily, weekly, monthly, yearly)", | |
| 176 | - | "Time block types: Focus, Personal, Vacation, Free Time", | |
| 177 | - | "Event reminders with configurable lead time", | |
| 178 | - | "Conflict detection with existing time blocks", | |
| 179 | - | ]) | |
| 180 | - | ||
| 181 | - | pdf.feature_block("CONTACTS", "Contacts", [ | |
| 182 | - | "Store names, companies, emails, phones, social handles, custom fields", | |
| 183 | - | "Full-text search across all contact fields", | |
| 184 | - | "Auto-suggest contacts from email senders", | |
| 185 | - | "Link contacts to tasks, events, and email threads", | |
| 186 | - | ]) | |
| 187 | - | ||
| 188 | - | # ========================================================= | |
| 189 | - | # PAGE 3 - Planning & Extras | |
| 190 | - | # ========================================================= | |
| 191 | - | pdf.add_page() | |
| 192 | - | ||
| 193 | - | pdf.section_title("Planning & Workflow") | |
| 194 | - | ||
| 195 | - | pdf.feature_block("PLAN", "Day Planning & Time Blocking", [ | |
| 196 | - | "Visual hourly timeline for your day", | |
| 197 | - | "Drag unscheduled tasks onto the timeline to create time blocks", | |
| 198 | - | "Events display alongside tasks on the same timeline", | |
| 199 | - | "Current time indicator and day navigation with keyboard shortcuts", | |
| 200 | - | "Collapsible sidebar with unscheduled tasks", | |
| 201 | - | ]) | |
| 202 | - | ||
| 203 | - | pdf.feature_block("REVIEW", "Weekly Review", [ | |
| 204 | - | "Guided review: see what you completed last week", | |
| 205 | - | "Set focus tasks and projects for the current week", | |
| 206 | - | "Track vacation days with visual banners", | |
| 207 | - | "Monday nudge: gentle reminder when review is overdue", | |
| 208 | - | ]) | |
| 209 | - | ||
| 210 | - | pdf.ln(4) | |
| 211 | - | pdf.section_title("Power Features") | |
| 212 | - | ||
| 213 | - | pdf.feature_block("SEARCH", "Search & Saved Views", [ | |
| 214 | - | "Full-text search across tasks, projects, emails, and contacts", | |
| 215 | - | "Date range syntax: after:, before:, from:, to:", | |
| 216 | - | "Save any filter as a named view; pin favorites for quick access", | |
| 217 | - | ]) | |
| 218 | - | ||
| 219 | - | pdf.feature_block("KEYBOARD", "Keyboard-Driven", [ | |
| 220 | - | "Vim-style navigation (j/k for items, g+key to jump between views)", | |
| 221 | - | "Quick-add from anywhere with 'q', shortcuts overlay with '?'", | |
| 222 | - | "Native menu bar with standard macOS shortcuts", | |
| 223 | - | ]) | |
| 224 | - | ||
| 225 | - | pdf.feature_block("EXPORT", "Export & Backup", [ | |
| 226 | - | "Export all data as JSON, tasks as CSV, events as ICS", | |
| 227 | - | "Automated daily backups with configurable retention", | |
| 228 | - | "Restore from any backup point", | |
| 229 | - | ]) | |
| 230 | - | ||
| 231 | - | pdf.feature_block("THEMES", "Themes", [ | |
| 232 | - | "10 built-in themes: Catppuccin, Dracula, Nord, Tokyo Night, and more", | |
| 233 | - | "Follow System for automatic light/dark switching", | |
| 234 | - | ]) | |
| 235 | - | ||
| 236 | - | # ========================================================= | |
| 237 | - | # PAGE 4 - Why GoingsOn + Comparison | |
| 238 | - | # ========================================================= | |
| 239 | - | pdf.add_page() | |
| 240 | - | ||
| 241 | - | pdf.section_title("Why GoingsOn?") | |
| 242 | - | ||
| 243 | - | pdf.set_font("Helvetica", "", 10.5) | |
| 244 | - | pdf.set_text_color(*TEXT) | |
| 245 | - | pdf.multi_cell(0, 5.5, | |
| 246 | - | "Most productivity tools are cloud-only, subscription-based, and spread across " | |
| 247 | - | "multiple apps. GoingsOn brings tasks, email, calendar, and contacts into one " | |
| 248 | - | "native application that works offline and keeps your data on your machine." | |
| 249 | - | ) | |
| 250 | - | pdf.ln(6) | |
| 251 | - | ||
| 252 | - | # Comparison table | |
| 253 | - | pdf.set_font("Helvetica", "B", 11) | |
| 254 | - | pdf.set_text_color(*TEXT) | |
| 255 | - | pdf.cell(0, 8, "How it compares:", new_x="LMARGIN", new_y="NEXT") | |
| 256 | - | pdf.ln(2) | |
| 257 | - | ||
| 258 | - | headers = ["", "GoingsOn", "Todoist", "Things 3", "Fantastical", "Spark"] | |
| 259 | - | col_w = [38, 28, 28, 28, 28, 28] | |
| 260 | - | ||
| 261 | - | # Header row | |
| 262 | - | pdf.set_font("Helvetica", "B", 8) | |
| 263 | - | pdf.set_fill_color(*DARK) | |
| 264 | - | pdf.set_text_color(*WHITE) | |
| 265 | - | for i, h in enumerate(headers): | |
| 266 | - | pdf.cell(col_w[i], 7, h, border=1, fill=True, align="C") | |
| 267 | - | pdf.ln() | |
| 268 | - | ||
| 269 | - | rows = [ | |
| 270 | - | ["Tasks", "Yes", "Yes", "Yes", "No", "No"], | |
| 271 | - | ["Email", "Yes", "No", "No", "No", "Yes"], | |
| 272 | - | ["Calendar", "Yes", "No", "No", "Yes", "No"], | |
| 273 | - | ["Contacts", "Yes", "No", "No", "No", "No"], | |
| 274 | - | ["Weekly Review", "Yes", "No", "No", "No", "No"], | |
| 275 | - | ["Offline-first", "Yes", "No", "Yes", "Yes", "No"], | |
| 276 | - | ["Cross-platform","Yes", "Yes", "Apple","Apple","Yes"], | |
| 277 | - | ["Free option", "Yes", "Ltd", "No", "Ltd", "Ltd"], | |
| 278 | - | ] | |
| 279 | - | ||
| 280 | - | pdf.set_font("Helvetica", "", 8) | |
| 281 | - | for row in rows: | |
| 282 | - | for i, val in enumerate(row): | |
| 283 | - | if i == 0: | |
| 284 | - | pdf.set_font("Helvetica", "B", 8) | |
| 285 | - | pdf.set_text_color(*TEXT) | |
| 286 | - | pdf.set_fill_color(*SECTION_BG) | |
| 287 | - | else: | |
| 288 | - | pdf.set_font("Helvetica", "", 8) | |
| 289 | - | if val == "Yes" and i == 1: | |
| 290 | - | pdf.set_text_color(*GREEN) | |
| 291 | - | pdf.set_fill_color(*WHITE) | |
| 292 | - | elif val == "Yes": | |
| 293 | - | pdf.set_text_color(80, 80, 80) | |
| 294 | - | pdf.set_fill_color(*WHITE) | |
| 295 | - | elif val == "No": | |
| 296 | - | pdf.set_text_color(180, 180, 180) | |
| 297 | - | pdf.set_fill_color(*WHITE) | |
| 298 | - | else: | |
| 299 | - | pdf.set_text_color(*MUTED) | |
| 300 | - | pdf.set_fill_color(*WHITE) | |
| 301 | - | pdf.cell(col_w[i], 6, val, border=1, fill=True, align="C" if i > 0 else "L") | |
| 302 | - | pdf.ln() | |
| 303 | - | ||
| 304 | - | pdf.ln(8) | |
| 305 | - | ||
| 306 | - | # Pricing note | |
| 307 | - | pdf.set_font("Helvetica", "B", 12) | |
| 308 | - | pdf.set_text_color(*TEXT) | |
| 309 | - | pdf.cell(0, 8, "Pricing", new_x="LMARGIN", new_y="NEXT") | |
| 310 | - | pdf.set_draw_color(*ACCENT) | |
| 311 | - | pdf.set_line_width(1.5) | |
| 312 | - | pdf.line(pdf.l_margin, pdf.get_y(), pdf.l_margin + 30, pdf.get_y()) | |
| 313 | - | pdf.ln(4) | |
| 314 | - | ||
| 315 | - | pdf.set_font("Helvetica", "", 10.5) | |
| 316 | - | pdf.set_text_color(*TEXT) | |
| 317 | - | pdf.multi_cell(0, 5.5, | |
| 318 | - | "GoingsOn is free during the alpha period. After launch, we plan to offer a " | |
| 319 | - | "one-time purchase model -- no subscriptions. Your data, your app, forever." | |
| 320 | - | ) | |
| 321 | - | ||
| 322 | - | pdf.ln(8) | |
| 323 | - | ||
| 324 | - | # Tech | |
| 325 | - | pdf.set_font("Helvetica", "B", 12) | |
| 326 | - | pdf.set_text_color(*TEXT) | |
| 327 | - | pdf.cell(0, 8, "Built With", new_x="LMARGIN", new_y="NEXT") | |
| 328 | - | pdf.set_draw_color(*ACCENT) | |
| 329 | - | pdf.line(pdf.l_margin, pdf.get_y(), pdf.l_margin + 30, pdf.get_y()) | |
| 330 | - | pdf.ln(4) | |
| 331 | - | ||
| 332 | - | pdf.set_font("Helvetica", "", 10) | |
| 333 | - | pdf.set_text_color(*MUTED) | |
| 334 | - | tech_items = [ | |
| 335 | - | "Rust backend (Tauri 2) -- native performance, small binary, low memory", | |
| 336 | - | "SQLite database -- your data in a single local file", | |
| 337 | - | "Vanilla JavaScript frontend -- no framework bloat", | |
| 338 | - | "205+ automated tests across the codebase", | |
| 339 | - | ] | |
| 340 | - | for item in tech_items: | |
| 341 | - | pdf.cell(0, 6, f" {item}", new_x="LMARGIN", new_y="NEXT") | |
| 342 | - | ||
| 343 | - | # ========================================================= | |
| 344 | - | # PAGE 5 - Installing via TestFlight | |
| 345 | - | # ========================================================= | |
| 346 | - | pdf.add_page() | |
| 347 | - | ||
| 348 | - | pdf.section_title("Get GoingsOn") | |
| 349 | - | ||
| 350 | - | # macOS | |
| 351 | - | pdf.set_font("Helvetica", "B", 13) | |
| 352 | - | pdf.set_text_color(*TEXT) | |
| 353 | - | pdf.cell(0, 8, "macOS", new_x="LMARGIN", new_y="NEXT") | |
| 354 | - | pdf.ln(2) | |
| 355 | - | ||
| 356 | - | pdf.set_font("Helvetica", "", 10.5) | |
| 357 | - | pdf.set_text_color(*MUTED) | |
| 358 | - | pdf.multi_cell(0, 5.5, | |
| 359 | - | "The macOS app will be shared with you directly as a .dmg file. " | |
| 360 | - | "Open the DMG and drag GoingsOn to your Applications folder." | |
| 361 | - | ) | |
| 362 | - | pdf.ln(2) | |
| 363 | - | pdf.set_font("Helvetica", "I", 9.5) | |
| 364 | - | pdf.multi_cell(0, 5, | |
| 365 | - | "Note: Since the app is not yet on the Mac App Store, macOS may show a security " | |
| 366 | - | "warning on first launch. Right-click the app and choose \"Open\" to bypass this, " | |
| 367 | - | "or go to System Settings > Privacy & Security and click \"Open Anyway\"." | |
| 368 | - | ) | |
| 369 | - | ||
| 370 | - | pdf.ln(8) | |
| 371 | - | ||
| 372 | - | # iOS TestFlight | |
| 373 | - | pdf.set_font("Helvetica", "B", 13) | |
| 374 | - | pdf.set_text_color(*TEXT) | |
| 375 | - | pdf.cell(0, 8, "iOS (via TestFlight)", new_x="LMARGIN", new_y="NEXT") | |
| 376 | - | pdf.ln(2) | |
| 377 | - | ||
| 378 | - | pdf.set_font("Helvetica", "", 10.5) | |
| 379 | - | pdf.set_text_color(*MUTED) | |
| 380 | - | pdf.multi_cell(0, 5.5, | |
| 381 | - | "GoingsOn for iOS is distributed through Apple's TestFlight, " | |
| 382 | - | "which lets you install pre-release apps directly on your iPhone or iPad." | |
| 383 | - | ) | |
| 384 | - | pdf.ln(4) | |
| 385 | - | ||
| 386 | - | steps = [ | |
| 387 | - | ("1.", "Install TestFlight", | |
| 388 | - | "Download \"TestFlight\" from the App Store if you don't have it already. " | |
| 389 | - | "It's a free app by Apple."), | |
| 390 | - | ("2.", "Open the invitation link", | |
| 391 | - | "You'll receive a TestFlight invitation link (via email or message). " | |
| 392 | - | "Tap the link on your iPhone or iPad."), | |
| 393 | - | ("3.", "Accept the invitation", | |
| 394 | - | "The link opens TestFlight. Tap \"Accept\" to join the GoingsOn beta, " | |
| 395 | - | "then tap \"Install\" to download the app."), | |
| 396 | - | ("4.", "Open GoingsOn", | |
| 397 | - | "Once installed, GoingsOn appears on your home screen like any other app. " | |
| 398 | - | "You can also launch it from inside TestFlight."), | |
| 399 | - | ("5.", "Updates are automatic", | |
| 400 | - | "When a new build is available, TestFlight will notify you. " | |
| 401 | - | "Updates install with one tap -- no need to uninstall and reinstall."), | |
| 402 | - | ] | |
| 403 | - | ||
| 404 | - | for num, title, desc in steps: | |
| 405 | - | pdf.set_font("Helvetica", "B", 11) | |
| 406 | - | pdf.set_text_color(*TEXT) | |
| 407 | - | pdf.cell(8, 6, num) | |
| 408 | - | pdf.cell(0, 6, title, new_x="LMARGIN", new_y="NEXT") | |
| 409 | - | pdf.set_x(pdf.l_margin + 8) | |
| 410 | - | pdf.set_font("Helvetica", "", 9.5) | |
| 411 | - | pdf.set_text_color(*MUTED) | |
| 412 | - | pdf.multi_cell(pdf.epw - 8, 4.8, desc, new_x="LMARGIN", new_y="NEXT") | |
| 413 | - | pdf.ln(3) | |
| 414 | - | ||
| 415 | - | pdf.ln(4) | |
| 416 | - | ||
| 417 | - | # Feedback | |
| 418 | - | pdf.set_font("Helvetica", "B", 13) | |
| 419 | - | pdf.set_text_color(*TEXT) | |
| 420 | - | pdf.cell(0, 8, "Sending Feedback", new_x="LMARGIN", new_y="NEXT") | |
| 421 | - | pdf.ln(2) | |
| 422 | - | ||
| 423 | - | pdf.set_font("Helvetica", "", 10.5) | |
| 424 | - | pdf.set_text_color(*MUTED) | |
| 425 | - | pdf.multi_cell(0, 5.5, | |
| 426 | - | "Your feedback is invaluable during this alpha period. You can:" | |
| 427 | - | ) | |
| 428 | - | pdf.ln(2) | |
| 429 | - | ||
| 430 | - | feedback = [ | |
| 431 | - | "Take a screenshot in the app and share it with any notes", | |
| 432 | - | "Use TestFlight's built-in feedback tool (shake your iPhone or tap \"Send Beta Feedback\" in TestFlight)", | |
| 433 | - | "Message me directly -- anything from bug reports to feature ideas", | |
| 434 | - | ] | |
| 435 | - | for item in feedback: | |
| 436 | - | pdf.set_x(pdf.l_margin + 4) | |
| 437 | - | pdf.set_font("Helvetica", "", 10) | |
| 438 | - | pdf.multi_cell(pdf.epw - 4, 5, f"- {item}", new_x="LMARGIN", new_y="NEXT") | |
| 439 | - | ||
| 440 | - | pdf.ln(10) | |
| 441 | - | ||
| 442 | - | # Thank you | |
| 443 | - | pdf.set_font("Helvetica", "B", 14) | |
| 444 | - | pdf.set_text_color(*TEXT) | |
| 445 | - | pdf.cell(0, 10, "Thank you for testing GoingsOn!", align="C", new_x="LMARGIN", new_y="NEXT") | |
| 446 | - | ||
| 447 | - | ||
| 448 | - | # ========================================================= | |
| 449 | - | # OUTPUT | |
| 450 | - | # ========================================================= | |
| 451 | - | output_path = "/Users/max/Git/active/goingson/GoingsOn-Pitch.pdf" | |
| 452 | - | pdf.output(output_path) | |
| 453 | - | print(f"PDF generated: {output_path}") | |
| 454 | - | ||
| 455 | - | ||
| 456 | - | if __name__ == "__main__": | |
| 457 | - | build_pdf() |