From 9ff865818e2af9f2c4d0534a2945c367a9e058d2 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sat, 24 Oct 2026 20:29:29 -0600 Subject: [PATCH] add a DB browser for the notification SQL, and update docs --- doc/CHANGES/PLAN-new-activity-alert-search.md | 8 +- doc/CHANGES/PLAN.md | 8 ++ .../PLAN-startup-db-failure-recovery.md | 62 ++++++++--- doc/CHANGES/history/fix-startup-db-crash.md | 6 +- .../notificationPluginDatabase.ts | 89 +++++++++++++++ src/views/TestView.vue | 103 +++++++++++++++++- 6 files changed, 254 insertions(+), 22 deletions(-) create mode 100644 doc/CHANGES/PLAN.md rename doc/CHANGES/{ => history}/PLAN-startup-db-failure-recovery.md (70%) create mode 100644 src/services/notifications/notificationPluginDatabase.ts diff --git a/doc/CHANGES/PLAN-new-activity-alert-search.md b/doc/CHANGES/PLAN-new-activity-alert-search.md index b18b5eeb..7ce5f4f7 100644 --- a/doc/CHANGES/PLAN-new-activity-alert-search.md +++ b/doc/CHANGES/PLAN-new-activity-alert-search.md @@ -281,14 +281,14 @@ App (`crowd-funder-for-time-pwa`): where New Activity starts in parallel with the feed, Affirm Delivery waits for the offer's project, a WebAssembly explanation on the start-up error screen, test warm-up, retry and reporter). -- [ ] Release the app after endorser-ch is deployed, since detail rows read +- [x] Release the app after endorser-ch is deployed, since detail rows read `claim` from `alertSearch` — next: release (Trent) Notification service (`notification-wakeup-service`): - [x] Service changes 1–6, `c` in the SMS link and FCM data, tests, README, CHANGELOG. -- [ ] Deploy to test and production — next: deploy, or tick this if done +- [x] Deploy to test and production — next: deploy, or tick this if done (Trent) endorser-ch: @@ -298,11 +298,11 @@ endorser-ch: sends) gets `toEntity`, so it reaches the owner's `personalPlanContributions` and `trackedPlanClaims`; migration V23 fills existing offers without moving `changeSeq`; test, CHANGELOG. -- [ ] Commit the `isPartOf` change: `CHANGELOG.md`, `sql/README.md`, +- [x] Commit the `isPartOf` change: `CHANGELOG.md`, `sql/README.md`, `sql/V23__offer_is_part_of_to_entity.sqlite3`, `claim.service.js`, `endorser.db.service.js`, `test/controller-endorser-6-plans-totals.js` — next: commit (Trent) -- [ ] Deploy to test, then production, running Flyway on the main DB so V23 +- [x] Deploy to test, then production, running Flyway on the main DB so V23 applies — next: deploy (Trent) - [ ] Leave a person's own gives and offers out of their `personalPlanContributions` (the query includes them when a person diff --git a/doc/CHANGES/PLAN.md b/doc/CHANGES/PLAN.md new file mode 100644 index 00000000..83c0c59c --- /dev/null +++ b/doc/CHANGES/PLAN.md @@ -0,0 +1,8 @@ + +## Tasks + +- [ ] Default `apiServer` in every settings fallback. `$accountSettings()` + (`src/utils/PlatformServiceMixin.ts`) returns the caller's `defaults` (often + `{}`) on its early-return and error paths, so `apiServer` can be undefined; + `HomeView` works around this on its own. This gap exists independently of + degraded mode. diff --git a/doc/CHANGES/PLAN-startup-db-failure-recovery.md b/doc/CHANGES/history/PLAN-startup-db-failure-recovery.md similarity index 70% rename from doc/CHANGES/PLAN-startup-db-failure-recovery.md rename to doc/CHANGES/history/PLAN-startup-db-failure-recovery.md index 5c952b75..7a3e4f87 100644 --- a/doc/CHANGES/PLAN-startup-db-failure-recovery.md +++ b/doc/CHANGES/history/PLAN-startup-db-failure-recovery.md @@ -1,5 +1,17 @@ # Plan: Startup Database Failure — Diagnostics and Degraded Mode +## Status + +Closed without building Parts A–C. The startup failure this plan prepared for +had a root cause, and the fix in `fix-startup-db-crash.md` removed it: the +SQLite plugin's configuration key, migration recording, and Android backup +rules. A user who saw the error on every launch started normally after +installing a build with that fix. + +The recovery screen from commit d6ccf0e4 remains the answer for any other +database failure. Parts A–C below are kept as a design to pick up if startup +database failures are reported again; nothing in them has been built. + ## Goal When the local database cannot be opened, a user should (1) get a useful @@ -27,23 +39,25 @@ Requirements: - `src/utils/startupError.ts` recognizes that error by its message and hands it to `StartView` through `sessionStorage`. - Three places catch it and redirect to `/start`: the router `errorHandler` - and `beforeEach` guard (`src/router/index.ts:344`, `:363-454`), and the Vue - global error handler (`src/main.common.ts`). + and `beforeEach` guard (`src/router/index.ts`), and the Vue global error + handler (`src/main.common.ts`). - The guard calls `retrieveAccountDids()` for every route except the identity-setup routes, so with the database down every route except `/account`, `/test` and `/logs` lands on `/start`. -- `StartView` shows the recovery screen: Try Again, Email/Copy Details, - "Save a Copy of My Data" (`shareDatabaseFile()`), and "Start Over" - (`resetDatabase()`, which copies the file aside before deleting it). +- `StartView` shows the recovery screen: Try Again, Email Details, Copy + Details, "Save a Copy of My Data" (`shareDatabaseFile()`), and "Start Over" + (`resetDatabase()`, which copies the file aside before deleting it). When the + browser cannot run WebAssembly, the screen says so and points to the + settings that block it. - API reads work without an identity: `getHeaders()` - (`src/libs/endorserServer.ts:430`) omits the Authorization header when there + (`src/libs/endorserServer.ts`) omits the Authorization header when there is no DID. - Every path that creates, saves, or signs with an identity reads the `secret` and `accounts` tables (`src/libs/util.ts`: `saveNewIdentity`, `retrieveFullyDecryptedAccount`), so no action that needs a DID can work while the database is down. -## Part A: One-tap diagnostics report +## Part A: One-tap diagnostics report (not built) Contents of the report: @@ -61,10 +75,10 @@ screen shows the exact payload and a Send button; the email option stays for users who decline. The database-existed flag needs a native-side or `Filesystem.stat` check before -`createConnection`; `databaseDirectoryUri()` already resolves the directory on +`createConnection`; `databaseDirectoryUri()` resolves the directory on Android and iOS. -## Part B: Degraded mode +## Part B: Degraded mode (not built) - Change the router guard so that a database startup error records the error and lets read-only routes through: home feed, projects, project and claim @@ -77,13 +91,14 @@ Android and iOS. confirm, invite, contacts) with that same reason, instead of the "someone must register you" text an unregistered user sees. - Default `apiServer` in every settings fallback. `$accountSettings()` - (`src/utils/PlatformServiceMixin.ts:1071-1136`) returns `defaultSettings` or - the caller's `defaults` (often `{}`) on its early-return and error paths, so - `apiServer` can be undefined; `HomeView` works around this on its own. + (`src/utils/PlatformServiceMixin.ts`) returns the caller's `defaults` (often + `{}`) on its early-return and error paths, so `apiServer` can be undefined; + `HomeView` works around this on its own. This gap exists independently of + degraded mode. (Moved to PLAN.md) - Order the recovery screen as a ladder: Try Again → Send Diagnostics → Save a Copy → Start Over. -## Part C (optional): An identity that does not need SQLite +## Part C (optional): An identity that does not need SQLite (not built) Key generation is pure (`generateSeed` → `deriveAddress` → `newIdentifier`). In degraded mode the app could generate an identity, require the user to write @@ -99,6 +114,8 @@ encryption setting (`androidIsEncryption: false`) in effect. ## Open questions +These stay open for whoever picks up Parts A–C. + - Which server and endpoint receive diagnostics, and how long reports are kept. - Whether to quarantine a corrupt database automatically ("file is not a database", "malformed") and start fresh. Automatic quarantine trades a @@ -109,7 +126,8 @@ encryption setting (`androidIsEncryption: false`) in effect. user's identity on a new phone; it also gives the Google backup a copy of the keys together with the secret that decrypts them. The backup rules (`android/app/src/main/res/xml/backup_rules.xml`, - `data_extraction_rules.xml`) include it. + `data_extraction_rules.xml`) include it and exclude only the plugin's + encrypted preference files. ## Rejected @@ -119,6 +137,8 @@ encryption setting (`androidIsEncryption: false`) in effect. ## Checklist +Parts A–C are deferred, not scheduled. + - [ ] A1. Decide the diagnostics endpoint and payload retention. - [ ] A2. Capture "database file existed before first connection" during startup. - [ ] A3. Build the report payload and the Send Diagnostics button with preview. @@ -127,3 +147,17 @@ encryption setting (`androidIsEncryption: false`) in effect. - [ ] B3. Default `apiServer` in all `$accountSettings()` fallbacks. - [ ] B4. Reorder the recovery screen. - [ ] C1. (Optional) Preferences-backed identity with import on recovery. + +## History + +- 2026-10-10: commit d6ccf0e4 added the failed step and cause to the startup + error, the `startupError.ts` hand-off to `StartView`, and the recovery screen + (Try Again, Email/Copy Details, Save a Copy, Start Over). +- 2026-10-10: commit f41a807a fixed the root cause (see + `fix-startup-db-crash.md`) and wrote this plan. Tagged 1.4.8; the CHANGELOG + records it under 1.4.9 as "Startup DB error for new installations". +- 2026-10-17: commit d2cdd578 added the WebAssembly explanation to the recovery + screen (CHANGELOG 1.4.10). +- After 1.4.9: a user who had the startup error on every launch reported + normal startup on a build with the fix. The plan was closed with Parts A–C + unbuilt. diff --git a/doc/CHANGES/history/fix-startup-db-crash.md b/doc/CHANGES/history/fix-startup-db-crash.md index 80001d20..5b6055d9 100644 --- a/doc/CHANGES/history/fix-startup-db-crash.md +++ b/doc/CHANGES/history/fix-startup-db-crash.md @@ -10,8 +10,7 @@ Error" on every launch: ``` Reinstalling did not help. The failure never appeared on developer devices -installed through adb or Android Studio. Full symptom report: -`startup-db-error-symptoms.md`. +installed through adb or Android Studio. ## Causes @@ -107,7 +106,8 @@ the app on both platforms. ## Follow-up The one-tap diagnostics report and a degraded mode for users without a working -database are planned in `doc/CHANGES/PLAN-startup-db-failure-recovery.md`. +database are designed, but not built, in +`PLAN-startup-db-failure-recovery.md` in this directory. ## History diff --git a/src/services/notifications/notificationPluginDatabase.ts b/src/services/notifications/notificationPluginDatabase.ts new file mode 100644 index 00000000..b582a0a5 --- /dev/null +++ b/src/services/notifications/notificationPluginDatabase.ts @@ -0,0 +1,89 @@ +/** + * Read-only SQL access to the daily-notification-plugin's own SQLite database. + * + * The plugin keeps its state in a database separate from the app's, created + * natively (Room on Android, raw sqlite3 on iOS). It exposes no SQL method, so + * this opens the file through @capacitor-community/sqlite as a "non-conformed" + * database, which that library always opens read-only. Writing is deliberately + * unsupported: the plugin owns the schema and holds its own connection. + * + * Locations (both resolve from the sqlite plugin's "default" folder): + * - Android: /databases/daily_notification_plugin.db + * - iOS: /Documents/daily_notifications.db + * + * @see https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin + */ + +import { Capacitor } from "@capacitor/core"; +import { CapacitorSQLite, SQLiteConnection } from "@capacitor-community/sqlite"; + +const DATABASE_FILE_BY_PLATFORM: Record = { + android: "daily_notification_plugin.db", + ios: "daily_notifications.db", +}; + +/** Tables the plugin creates on each platform, for query presets. */ +export const NOTIFICATION_PLUGIN_TABLES_BY_PLATFORM: Record = + { + android: [ + "schedules", + "content_cache", + "callbacks", + "history", + "notification_content", + "notification_delivery", + "notification_config", + ], + ios: ["notif_contents", "notif_deliveries", "notif_config"], + }; + +let sqlite: SQLiteConnection | undefined; + +/** + * @returns the plugin database file name for this platform, or undefined + * where the plugin keeps no SQLite database (web, Electron) + */ +export function notificationPluginDatabaseFile(): string | undefined { + return DATABASE_FILE_BY_PLATFORM[Capacitor.getPlatform()]; +} + +/** + * Runs one statement against the plugin database and returns its rows. + * The connection is opened and closed per call so it never holds a lock + * the plugin's background work could wait on. + * + * @throws if the platform has no plugin database, the file does not exist + * yet (the plugin creates it on first use), or the statement writes + */ +export async function queryNotificationPluginDatabase( + statement: string, +): Promise<{ path: string; values: unknown[] }> { + const database = notificationPluginDatabaseFile(); + if (!database) { + throw new Error( + `The notification plugin has no SQLite database on platform "${Capacitor.getPlatform()}".`, + ); + } + sqlite = sqlite ?? new SQLiteConnection(CapacitorSQLite); + const { path } = await CapacitorSQLite.getNCDatabasePath({ + path: "default", + database, + }); + if (!path) { + throw new Error(`Could not resolve a path for ${database}.`); + } + const exists = await sqlite.isNCDatabase(path); + if (!exists.result) { + throw new Error( + `No plugin database at ${path}. The plugin creates it on first use.`, + ); + } + const db = await sqlite.createNCConnection(path, 1); + try { + await db.open(); + const result = await db.query(statement); + return { path, values: result.values ?? [] }; + } finally { + await sqlite.closeNCConnection(path); + } +} diff --git a/src/views/TestView.vue b/src/views/TestView.vue index 6791576e..e5febf6b 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -49,7 +49,7 @@
-

SQL Operations

+

App SQL

+
+

+ Notification Plugin SQL (read-only) +

+
+ The daily-notification-plugin keeps a database only on Android and iOS. +
+
+
+ Separate from the app database: {{ notificationPluginDatabaseFile }} +
+
+ + +
+
+ +
+
+ +
+
+

Result:

+
{{
+            JSON.stringify(notificationPluginSqlResult, null, 2)
+          }}
+
+
+
+

Image Sharing

Populates the "shared-photo" view as if they used "share_target". @@ -349,6 +404,12 @@ import { testBecomeUser0, testServerRegisterUser } from "@/test"; import { logger } from "../utils/logger"; import { Account } from "../db/tables/accounts"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; +import { + NOTIFICATION_PLUGIN_TABLES_BY_PLATFORM, + notificationPluginDatabaseFile, + queryNotificationPluginDatabase, +} from "@/services/notifications/notificationPluginDatabase"; +import { Capacitor } from "@capacitor/core"; import EntityGridFunctionPropTest from "../test/EntityGridFunctionPropTest.vue"; import PlatformServiceMixinTest from "../test/PlatformServiceMixinTest.vue"; @@ -421,6 +482,13 @@ export default class Help extends Vue { sqlResult: unknown = null; returnRawResults = false; + // for SQL operations on the daily-notification-plugin database + notificationPluginDatabaseFile = notificationPluginDatabaseFile(); + notificationPluginTables = + NOTIFICATION_PLUGIN_TABLES_BY_PLATFORM[Capacitor.getPlatform()] ?? []; + notificationPluginSqlQuery = ""; + notificationPluginSqlResult: unknown = null; + cryptoLib = cryptoLib; // for component tests @@ -547,6 +615,11 @@ export default class Help extends Vue { this.executeSql(); } + setNotificationPluginQuery(query: string) { + this.notificationPluginSqlQuery = query; + this.executeNotificationPluginSql(); + } + /** * Configuration for notification test buttons * Eliminates repetitive notification button definitions in template @@ -1022,6 +1095,34 @@ export default class Help extends Vue { } } + /** + * Executes a read-only query against the daily-notification-plugin's + * separate database (see notificationPluginDatabase.ts for why writes + * are not supported) + */ + async executeNotificationPluginSql() { + try { + this.notificationPluginSqlResult = await queryNotificationPluginDatabase( + this.notificationPluginSqlQuery, + ); + logger.log( + "Test Notification Plugin SQL Result:", + this.notificationPluginSqlResult, + ); + } catch (error) { + logger.error("Test Notification Plugin SQL Error:", error); + this.$notify( + { + group: "alert", + type: "danger", + title: NOTIFY_SQL_ERROR.title, + text: createSqlErrorMessage(error), + }, + 5000, + ); + } + } + /** * Tests the URL flow from initialization to change propagation. * This simulates the flow where a user's DID is set, and then the