add a DB browser for the notification SQL, and update docs
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
+48
-14
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: <app data>/databases/daily_notification_plugin.db
|
||||
* - iOS: <app>/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<string, string> = {
|
||||
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<string, string[]> =
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
+102
-1
@@ -49,7 +49,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<h2 class="text-xl font-bold mb-4">SQL Operations</h2>
|
||||
<h2 class="text-xl font-bold mb-4">App SQL</h2>
|
||||
<div class="flex gap-2 mt-2">
|
||||
<button :class="sqlLinkClasses" @click="setAllTablesQuery">
|
||||
All Tables
|
||||
@@ -95,6 +95,61 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<h2 class="text-xl font-bold mb-4">
|
||||
Notification Plugin SQL (read-only)
|
||||
</h2>
|
||||
<div v-if="!notificationPluginDatabaseFile">
|
||||
The daily-notification-plugin keeps a database only on Android and iOS.
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="text-sm mb-2">
|
||||
Separate from the app database: {{ notificationPluginDatabaseFile }}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
<button
|
||||
:class="sqlLinkClasses"
|
||||
@click="
|
||||
setNotificationPluginQuery(
|
||||
`SELECT * FROM sqlite_master WHERE type='table';`,
|
||||
)
|
||||
"
|
||||
>
|
||||
All Tables
|
||||
</button>
|
||||
<button
|
||||
v-for="table in notificationPluginTables"
|
||||
:key="table"
|
||||
:class="sqlLinkClasses"
|
||||
@click="setNotificationPluginQuery(`SELECT * FROM ${table};`)"
|
||||
>
|
||||
{{ table }}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<textarea
|
||||
v-model="notificationPluginSqlQuery"
|
||||
class="w-full h-32 p-2 border border-gray-300 rounded-md font-mono"
|
||||
placeholder="Enter a SELECT or PRAGMA query here..."
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button
|
||||
:class="primaryButtonClasses"
|
||||
@click="executeNotificationPluginSql"
|
||||
>
|
||||
Execute
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="notificationPluginSqlResult" class="mt-4">
|
||||
<h3 class="text-lg font-semibold mb-2">Result:</h3>
|
||||
<pre class="bg-gray-100 p-4 rounded-md overflow-x-auto">{{
|
||||
JSON.stringify(notificationPluginSqlResult, null, 2)
|
||||
}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<h2 class="text-xl font-bold mb-4">Image Sharing</h2>
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user