mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-15 16:14:13 +00:00
1225 lines
42 KiB
Markdown
1225 lines
42 KiB
Markdown
# Cloud settings sync — complete options & format reference
|
||
|
||
Exhaustive reference for every value that **is** or **would be** included in a BetterSEQTA Cloud settings backup, the exact JSON shapes stored in `chrome.storage.local`, and how those shapes appear on the wire.
|
||
|
||
**Related docs**
|
||
|
||
- Server HTTP contract: [CLOUD_SETTINGS_SYNC_SERVER.md](./CLOUD_SETTINGS_SYNC_SERVER.md)
|
||
- Client upload/download: `src/seqta/utils/cloudSettingsSync.ts`
|
||
- Auto-sync (debounce, poll, triggers): `src/background/cloudSettingsAutoSync.ts`
|
||
- Core defaults: `src/seqta/utils/defaultSettings.ts`
|
||
- Full-schema initializer: `src/seqta/utils/ensureSyncableStorageDefaults.ts`
|
||
- In-memory settings + persistence: `src/seqta/utils/listeners/SettingsState.ts`
|
||
- Type definitions: `src/types/storage.ts`
|
||
|
||
---
|
||
|
||
## Table of contents
|
||
|
||
1. [Architecture](#architecture)
|
||
2. [Sync lifecycle](#sync-lifecycle)
|
||
3. [Wire format (HTTP body)](#wire-format-http-body)
|
||
4. [Local storage model](#local-storage-model)
|
||
5. [Exclusion rules](#exclusion-rules)
|
||
6. [Legacy key migration](#legacy-key-migration)
|
||
7. [Core extension settings](#core-extension-settings)
|
||
8. [Plugin settings objects](#plugin-settings-objects)
|
||
9. [Plugin runtime storage keys](#plugin-runtime-storage-keys)
|
||
10. [Excluded caches (local-only schemas)](#excluded-caches-local-only-schemas)
|
||
11. [Data outside `chrome.storage.local`](#data-outside-chromestoragelocal)
|
||
12. [Catch-all & forward compatibility](#catch-all--forward-compatibility)
|
||
13. [UI accessibility](#ui-accessibility)
|
||
14. [Default schema initialization](#default-schema-initialization)
|
||
|
||
---
|
||
|
||
## Architecture
|
||
|
||
Cloud settings sync is a **whole-snapshot backup** of extension local storage:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ chrome.storage.local (flat string-keyed JSON values) │
|
||
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
|
||
│ │ Core keys │ │ plugin.* │ │ Excluded (see §5) │ │
|
||
│ │ onoff, │ │ .settings │ │ OAuth, device caches, │ │
|
||
│ │ DarkMode, │ │ .storage.* │ │ client watermarks │ │
|
||
│ │ menuitems… │ │ │ │ │ │
|
||
│ └─────────────┘ └──────────────┘ └────────────────────────┘ │
|
||
└────────────────────────────┬────────────────────────────────────┘
|
||
│ buildUploadPatch()
|
||
│ • ensureSyncableStorageDefaults (local)
|
||
│ • normalize + diff vs last upload / defaults
|
||
▼
|
||
PUT /api/bsplus/settings/sync (sparse data patch)
|
||
{ schemaVersion, themeId, data: { … } }
|
||
│
|
||
▼
|
||
accounts.betterseqta.org (one row per user)
|
||
```
|
||
|
||
**Design principle:** inclusive by default. Any new key written to `chrome.storage.local` is uploaded unless explicitly added to an omit list in `cloudSettingsSync.ts`.
|
||
|
||
**Not included:** IndexedDB, `localStorage`, `sessionStorage`, or SEQTA page DOM state.
|
||
|
||
---
|
||
|
||
## Sync lifecycle
|
||
|
||
### When upload runs
|
||
|
||
| Trigger | Behaviour |
|
||
|---------|-----------|
|
||
| Any **included** `chrome.storage.local` key changes | Debounced upload after **2000 ms** (`UPLOAD_DEBOUNCE_MS`) |
|
||
| Manual upload (settings UI) | Immediate `PUT` |
|
||
| First poll with no cloud backup and no local watermark | Baseline upload |
|
||
| `requestCloudSettingsDebouncedUpload()` | Same debounced path (e.g. after theme install) |
|
||
|
||
Upload is skipped when:
|
||
|
||
- `autoCloudSettingsSync === false`
|
||
- No `bsplus_token` (not signed in)
|
||
- `suppressAutoUploadDuringRestore` is true (during download)
|
||
|
||
### When download runs
|
||
|
||
| Trigger | Behaviour |
|
||
|---------|-----------|
|
||
| Manual restore (settings UI) | `GET` → `applyDownloadedEnvelope` → theme prefetch → reload SEQTA tabs |
|
||
| Auto poll | If `bsplus.updated_at` from `GET /api/user/cloud-summary` is newer than local watermark |
|
||
| First poll with cloud backup but no watermark | Full download |
|
||
|
||
Download is skipped when server `schemaVersion` > client `CLOUD_SETTINGS_SYNC_SCHEMA_VERSION` (currently `1`).
|
||
|
||
### Poll throttle
|
||
|
||
- Key: `bsplus_lastCloudPoll` — **never uploaded**
|
||
- Value: `number` (Unix ms timestamp)
|
||
- Minimum interval between poll runs: **24 hours** (`POLL_THROTTLE_MS`)
|
||
|
||
### Restore semantics
|
||
|
||
`applyDownloadedEnvelope` calls `browser.storage.local.set(remoteSanitized)` with **only** keys from the server (after migration + strip). It does **not** wipe storage:
|
||
|
||
- **OAuth keys** remain because they are stripped from the remote blob and never overwritten.
|
||
- **Excluded device caches** remain for the same reason.
|
||
- **Client-only keys** (`bsplus_cloud_settings_known_remote_updated_at`, etc.) remain unless accidentally present in an old server payload (stripped defensively).
|
||
- Keys present locally but **absent** from an older cloud snapshot are **not deleted**.
|
||
|
||
After download, if `themeId` / `selectedTheme` is non-empty, the service worker sets `bsplus_pending_theme_ensure_after_cloud` so the page `ThemeManager` can download missing theme assets from the store.
|
||
|
||
### Local schema before diff (upload)
|
||
|
||
`ensureSyncableStorageDefaults()` (`src/seqta/utils/ensureSyncableStorageDefaults.ts`) ensures every **cloud-syncable** key exists in `chrome.storage.local` with its default value if it was previously absent. This runs **locally only** so diffs against the last-uploaded snapshot (or schema defaults) are consistent. The PUT body is a **sparse patch** of changed keys only — defaults are not bulk-uploaded.
|
||
|
||
| When it runs | Context |
|
||
|--------------|---------|
|
||
| `browser.runtime.onInstalled` | Service worker (install + update) |
|
||
| `browser.runtime.onStartup` | Service worker |
|
||
| Service worker load | `background.ts` (once at startup) |
|
||
| `initializeSettingsState()` | SEQTA content script + extension settings page (first init) |
|
||
| Before each cloud upload | `putSettingsOnce()` in `cloudSettingsAutoSync.ts` |
|
||
|
||
Rules:
|
||
|
||
- Builds defaults from `getDefaultSettingsState()` plus each plugin’s `plugin.{id}.settings` defaults from `getAllPluginSettings()`.
|
||
- **Does not backfill legacy keys** (`animatedbk`, `bksliderinput`, etc.) — missing `plugin.*.settings` are derived from legacy via `migrateLegacyToPluginSettings(existing)` when patched.
|
||
- **Does not backfill optional keys** where `undefined` is intentional (`timeFormat`, `selectedFont`, dev/privacy/announcement flags, etc.) so existing behaviour is unchanged.
|
||
- Skips keys in the cloud omit lists (`isKeyIncludedInCloudUploadPayload`).
|
||
- **Never overwrites** existing storage values — only patches keys absent from storage (`key in existing` is false).
|
||
- Settings writes during bootstrap do not fire UI listeners (`SettingsState.bootstrapping`); user edits persist **only the changed key**, not the whole in-memory object.
|
||
|
||
---
|
||
|
||
## Wire format (HTTP body)
|
||
|
||
### `PUT /api/bsplus/settings/sync` request
|
||
|
||
```http
|
||
PUT /api/bsplus/settings/sync HTTP/1.1
|
||
Host: accounts.betterseqta.org
|
||
Authorization: Bearer <access_token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
```json
|
||
{
|
||
"schemaVersion": 1,
|
||
"themeId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||
"data": {
|
||
"DarkMode": true,
|
||
"selectedTheme": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||
}
|
||
}
|
||
```
|
||
|
||
Only keys that changed since the last successful upload appear in `data`. The server merges this patch into stored JSON; omitted keys are not deleted. Dev JSON export (`getSnapshotForUpload`) still returns the full normalized map for debugging.
|
||
|
||
| Top-level field | JSON type | Rules |
|
||
|-----------------|-----------|-------|
|
||
| `schemaVersion` | `number` | Always `1` today (`CLOUD_SETTINGS_SYNC_SCHEMA_VERSION`) |
|
||
| `themeId` | `string` | `normalizeThemeIdForSync(selectedTheme)` — trimmed; `""` if unset/invalid |
|
||
| `data` | `object` | Flat map: storage key → value. Same types as `chrome.storage.local`. No nesting convention beyond whatever each key stores. |
|
||
|
||
### `GET /api/bsplus/settings/sync` response
|
||
|
||
Same shape as the PUT body, plus optional:
|
||
|
||
```json
|
||
{
|
||
"schemaVersion": 1,
|
||
"themeId": "…",
|
||
"data": { },
|
||
"updated_at": "2026-04-07T12:00:00.000Z"
|
||
}
|
||
```
|
||
|
||
- `updated_at`: ISO 8601 UTC string; written to `bsplus_cloud_settings_known_remote_updated_at` locally (never re-uploaded).
|
||
|
||
### JSON encoding notes
|
||
|
||
- All values must be JSON-serializable (`boolean`, `number`, `string`, `null`, arrays, plain objects).
|
||
- `undefined` is never stored by the WebExtension storage API.
|
||
- Dates are stored as **strings** (ISO or calendar formats), not `Date` objects.
|
||
- `chrome.storage.local` may historically stringify some booleans in edge cases; the client treats them as booleans after read.
|
||
|
||
---
|
||
|
||
## Local storage model
|
||
|
||
### Flat key namespace
|
||
|
||
Every persisted setting is a **top-level key** in `chrome.storage.local`:
|
||
|
||
| Pattern | Example | Value shape |
|
||
|---------|---------|---------------|
|
||
| Core setting | `DarkMode` | scalar or structured JSON |
|
||
| Plugin settings | `plugin.global-search.settings` | single JSON **object** with all plugin prefs |
|
||
| Plugin storage | `plugin.messageFolders.storage.folders` | one JSON value per property |
|
||
| Analytics cache | `bsplus.analytics.v2.https://school.seqta.com.au.12345` | structured cache (excluded) |
|
||
|
||
Plugin settings use `plugin.{pluginId}.settings` where `pluginId` matches the plugin registration id (e.g. `messageFolders`, not `message-folders`).
|
||
|
||
Plugin runtime storage uses `plugin.{pluginId}.storage.{propertyName}` — each property is a **separate** storage key, not nested under one object.
|
||
|
||
### Settings vs storage
|
||
|
||
| Mechanism | API | Persisted keys | Synced? |
|
||
|-----------|-----|----------------|---------|
|
||
| `settingsState` / `SettingsState` | Proxy over in-memory + `storage.local` | Top-level keys (`onoff`, `menuitems`, …) | Yes (unless excluded) |
|
||
| Plugin `api.settings` | Proxy; one object per plugin | `plugin.{id}.settings` | Yes |
|
||
| Plugin `api.storage` | Proxy; one key per property | `plugin.{id}.storage.{prop}` | Yes, except excluded prefixes |
|
||
|
||
Component/button plugin settings (`type: "component"` | `"button"`) are **not** written by the settings proxy loop; only scalar settings defined in `plugin.settings` are persisted automatically. The settings UI may still write `enabled` and other keys via `browser.storage.local.set` directly.
|
||
|
||
---
|
||
|
||
## Exclusion rules
|
||
|
||
Implemented in `shouldOmitKeyFromCloudPayload(key)`:
|
||
|
||
### Exact key exclusions
|
||
|
||
| Key | Format if present locally | On restore |
|
||
|-----|---------------------------|------------|
|
||
| `bsplus_token` | `string` JWT | Keep device value |
|
||
| `bsplus_refresh_token` | `string` | Keep device value |
|
||
| `bsplus_client_id` | `string` UUID | Keep device value |
|
||
| `bsplus_user` | `CloudUser` object (see below) | Keep device value |
|
||
| `cloudAccessToken` | `string` (legacy) | Keep device value |
|
||
| `cloudUsername` | `string` (legacy) | Keep device value |
|
||
| `plugin.assessments-average.storage.assessments` | object | Keep device value |
|
||
| `plugin.assessments-average.storage.weightings` | object | Keep device value |
|
||
| `bsplus_cloud_settings_known_remote_updated_at` | ISO `string` | Keep device value |
|
||
| `bsplus_cloud_settings_last_uploaded_snapshot` | `object` (normalized syncable map) | Keep device value |
|
||
| `bsplus_lastCloudPoll` | `number` (ms) | Keep device value |
|
||
| `bsplus_pending_theme_ensure_after_cloud` | `string` (theme id) | Keep device value |
|
||
|
||
#### `bsplus_user` shape (local only, never uploaded)
|
||
|
||
```json
|
||
{
|
||
"id": "uuid",
|
||
"email": "user@example.com",
|
||
"username": "optional",
|
||
"displayName": "optional",
|
||
"pfpUrl": "https://…",
|
||
"pfpHash": "abc123" ,
|
||
"admin_level": 0
|
||
}
|
||
```
|
||
|
||
All fields except `id` are optional.
|
||
|
||
### Prefix exclusions
|
||
|
||
| Prefix | Matches |
|
||
|--------|---------|
|
||
| `plugin.global-search.storage.` | Any Global Search device cache key |
|
||
| `bsplus.analytics.` | Grade Analytics caches and chart mode prefs |
|
||
|
||
Prefix check: `key.startsWith(prefix)`.
|
||
|
||
---
|
||
|
||
## Legacy key migration
|
||
|
||
Runs in `migrateLegacyToPluginSettings()` on **both** upload and download. Legacy keys are **removed** from `data` after migration. Migration only fills plugin settings fields that are still `undefined`.
|
||
|
||
| Legacy key | Legacy format | Target key | Target field | Conversion |
|
||
|------------|---------------|------------|--------------|------------|
|
||
| `animatedbk` | `boolean` | `plugin.animated-background.settings` | `enabled` | `!!animatedbk` |
|
||
| `bksliderinput` | `string` `"0"`–`"100"` | `plugin.animated-background.settings` | `speed` | `speed = round((0.1 + (parseFloat(s) / 100) * 1.9) * 100) / 100` → range **0.1–2.0** |
|
||
| `assessmentsAverage` | `boolean` | `plugin.assessments-average.settings` | `enabled` | `!!assessmentsAverage` |
|
||
| `lettergrade` | `boolean` | `plugin.assessments-average.settings` | `lettergrade` | `!!lettergrade` |
|
||
| `notificationCollector` | `boolean` only | `plugin.notificationCollector.settings` | `enabled` | copy boolean |
|
||
|
||
**Example:** legacy `bksliderinput: "50"` → `speed: 1.05` because `0.1 + 0.5 * 1.9 = 1.05`.
|
||
|
||
Modern clients should only see plugin-format keys in uploaded payloads; legacy keys may still exist on very old profiles until first sync.
|
||
|
||
---
|
||
|
||
## Core extension settings
|
||
|
||
Top-level `chrome.storage.local` keys from `SettingsState` (`src/types/storage.ts`). Defaults from `getDefaultSettingsState()` in `src/seqta/utils/defaultSettings.ts` unless noted. Missing keys are backfilled by `ensureSyncableStorageDefaults()` (see [Default schema initialization](#default-schema-initialization)).
|
||
|
||
### Master & appearance
|
||
|
||
#### `onoff`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **Default** | `true` |
|
||
| **UI** | Settings → “BetterSEQTA+” master switch |
|
||
| **Example** | `true` |
|
||
|
||
#### `DarkMode`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **Default** | `true` |
|
||
| **UI** | SEQTA UI / theme system |
|
||
| **Example** | `true` |
|
||
|
||
#### `selectedTheme`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `string` |
|
||
| **Default** | `""` |
|
||
| **UI** | Theme selector / store install |
|
||
| **Format** | BetterSEQTA store theme UUID, **flavour (slave) variant** id, or empty string for no store theme |
|
||
| **Wire** | Duplicated as top-level `themeId` on upload (trimmed) |
|
||
| **Example** | `"f47ac10b-58cc-4372-a567-0e02b2c3d479"` or `""` |
|
||
|
||
Theme **asset blobs** (CSS, images) live in **localforage/IndexedDB**, not in this key. After restore, only the id is synced; assets are re-fetched if missing.
|
||
|
||
#### `selectedColor`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `string` |
|
||
| **Default** | `"linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)"` |
|
||
| **UI** | Colour picker |
|
||
| **Format** | Any valid CSS `background` value: hex (`#c93d00`), `rgb()`, `rgba()`, `linear-gradient(...)`, etc. |
|
||
| **Example** | `"#c93d00"` or `"linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)"` |
|
||
|
||
#### `originalSelectedColor`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `string` |
|
||
| **Default** | `""` |
|
||
| **Purpose** | Colour before theme preview; restored when clearing theme |
|
||
| **Example** | `"#c93d00"` |
|
||
|
||
#### `originalDarkMode`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **Default** | undefined until theme preview |
|
||
| **Purpose** | Dark mode before theme preview |
|
||
| **Example** | `true` |
|
||
|
||
#### `transparencyEffects`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **Default** | `false` |
|
||
| **UI** | Settings → Transparency Effects |
|
||
| **Example** | `false` |
|
||
|
||
#### `animations`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **Default** | `true` on normal devices; `false` if low-end (`hardwareConcurrency < 4` or `deviceMemory <= 2`) |
|
||
| **UI** | Settings → Animations |
|
||
| **Example** | `true` |
|
||
|
||
#### `iconOnlySidebar`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **Default** | `false` |
|
||
| **UI** | Settings → Icon Only Sidebar |
|
||
| **Example** | `false` |
|
||
|
||
#### `selectedFont`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `string` |
|
||
| **Default** | undefined → runtime treats as `"rubik"` |
|
||
| **UI** | Settings → Interface Font |
|
||
| **Format** | Font preset id from `src/seqta/ui/fonts/presets.ts` |
|
||
|
||
Allowed ids:
|
||
|
||
`rubik`, `inter`, `poppins`, `nunito`, `montserrat`, `open-sans`, `lato`, `source-sans-3`, `raleway`, `dm-sans`, `plus-jakarta-sans`, `outfit`, `roboto`, `work-sans`, `manrope`, `figtree`, `lexend`, `ubuntu`, `karla`, `quicksand`, `ibm-plex-sans`, `space-grotesk`, `mulish`, `cabin`, `oswald`, `merriweather`, `playfair-display`, `lora`, `crimson-pro`, `libre-baskerville`, `system`
|
||
|
||
**Example:** `"inter"`
|
||
|
||
#### `adaptiveThemeColour`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **Default** | `false` |
|
||
| **Example** | `false` |
|
||
|
||
#### `adaptiveThemeGradient`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **Default** | `false` |
|
||
| **Example** | `false` |
|
||
|
||
#### `adaptiveThemeColourTransition`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **Default** | `true` |
|
||
| **Example** | `true` |
|
||
|
||
### Navigation & layout
|
||
|
||
#### `defaultPage`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `string` |
|
||
| **Default** | `"home"` |
|
||
| **UI** | Settings → Default Page |
|
||
| **Allowed** | `"home"`, `"dashboard"`, `"timetable"`, `"welcome"`, `"messages"`, `"documents"`, `"reports"` |
|
||
| **Example** | `"home"` |
|
||
|
||
#### `timeFormat`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `string` |
|
||
| **Default** | undefined (24-hour behaviour) |
|
||
| **UI** | Settings → “12 Hour Time” switch |
|
||
| **Values** | `"12"` when enabled; `"24"` when disabled |
|
||
| **Example** | `"12"` |
|
||
|
||
#### `lessonalert`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **Default** | `true` |
|
||
| **Purpose** | Lesson alert on home page |
|
||
| **Example** | `true` |
|
||
|
||
#### `menuitems`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `object` |
|
||
| **Default** | All sidebar items `{ toggle: true }` |
|
||
| **UI** | Menu editor overlay |
|
||
| **Shape** | `{ [menuKey]: { toggle: boolean } }` |
|
||
|
||
Keys (from `SettingsState`):
|
||
|
||
`assessments`, `courses`, `dashboard`, `documents`, `forums`, `goals`, `home`, `messages`, `myed`, `news`, `notices`, `portals`, `reports`, `settings`, `timetable`, `welcome`
|
||
|
||
**Example:**
|
||
|
||
```json
|
||
{
|
||
"home": { "toggle": true },
|
||
"timetable": { "toggle": false },
|
||
"messages": { "toggle": true }
|
||
}
|
||
```
|
||
|
||
#### `menuorder`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `array` |
|
||
| **Default** | `[]` |
|
||
| **UI** | Menu editor (drag order) |
|
||
| **Item type** | `string` — SEQTA menu `data-key` values (e.g. `"home"`, `"timetable"`, `"analytics"` for plugin-injected items) |
|
||
| **Example** | `["home", "timetable", "assessments", "messages"]` |
|
||
|
||
#### `defaultmenuorder`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `array` |
|
||
| **Default** | `[]` (populated from DOM on first menu edit) |
|
||
| **Item type** | Same as `menuorder` — snapshot of school default order |
|
||
| **Example** | `["home", "dashboard", "timetable"]` |
|
||
|
||
### Shortcuts
|
||
|
||
#### `shortcuts`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `array` |
|
||
| **Default** | Outlook, Office, Google — each `enabled: true` |
|
||
| **UI** | Settings → Shortcuts |
|
||
| **Item shape** | `{ "name": string, "enabled": boolean }` |
|
||
| **Notes** | `name` matches keys in `src/seqta/content/links.json` (e.g. `"Outlook"`, `"YouTube"`) |
|
||
|
||
**Example:**
|
||
|
||
```json
|
||
[
|
||
{ "name": "Outlook", "enabled": true },
|
||
{ "name": "Office", "enabled": false },
|
||
{ "name": "Google", "enabled": true }
|
||
]
|
||
```
|
||
|
||
#### `customshortcuts`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `array` |
|
||
| **Default** | `[]` |
|
||
| **UI** | Settings → Shortcuts → add custom |
|
||
| **Item shape** | `{ "name": string, "url": string, "icon": string }` |
|
||
| **`url`** | Full URL with protocol, e.g. `"https://example.com"` |
|
||
| **`icon`** | Inline **SVG markup string** OR single fallback character (first letter of title) |
|
||
|
||
**Example:**
|
||
|
||
```json
|
||
[
|
||
{
|
||
"name": "School Portal",
|
||
"url": "https://portal.school.edu.au",
|
||
"icon": "<svg viewBox=\"0 0 24 24\">…</svg>"
|
||
}
|
||
]
|
||
```
|
||
|
||
### Subjects & news
|
||
|
||
#### `subjectfilters`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `object` |
|
||
| **Default** | `{}` |
|
||
| **UI** | Home / assessments subject filters |
|
||
| **Shape** | `{ [subjectCode: string]: boolean }` — `false` hides subject; missing key = visible |
|
||
| **Example** | `{ "10MAT": false, "10ENG": true }` |
|
||
|
||
#### `newsSource`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `string` |
|
||
| **Default** | `"australia"` |
|
||
| **UI** | Settings → News Feed Source |
|
||
| **Allowed** | `australia`, `usa`, `uk`, `taiwan`, `hong_kong`, `panama`, `canada`, `singapore`, `japan`, `netherlands` |
|
||
| **Example** | `"australia"` |
|
||
|
||
### Cloud sync preference
|
||
|
||
#### `autoCloudSettingsSync`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **Default** | `true` |
|
||
| **UI** | Settings → Cloud Settings Sync |
|
||
| **Semantics** | Sync runs when **not** strictly `false` (default-on) |
|
||
| **Example** | `true` |
|
||
|
||
### Theme of the Month
|
||
|
||
#### `themeOfTheMonthDisabled`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **Default** | `false` |
|
||
| **UI** | Global Search plugin section → Theme of the Month switch (inverted in UI) |
|
||
| **Example** | `false` |
|
||
|
||
#### `themeOfTheMonthDismissedMonth`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `string` |
|
||
| **Format** | `"YYYY-MM"` calendar month |
|
||
| **Example** | `"2026-06"` |
|
||
|
||
#### `themeOfTheMonthLastSeenId`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `string` |
|
||
| **Status** | Deprecated; may still exist in old profiles |
|
||
| **Synced** | Yes if present |
|
||
|
||
### One-time announcements & privacy
|
||
|
||
#### `privacyStatementShown`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **Example** | `true` |
|
||
|
||
#### `privacyStatementLastUpdated`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `string` |
|
||
| **Format** | ISO date, e.g. `"2025-12-20"` |
|
||
|
||
#### `engageParentsAnnouncementShown`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **Purpose** | SEQTA Engage parents announcement dismissed |
|
||
|
||
#### `bsCloudAutoSyncAnnouncementShown`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **Purpose** | Cloud auto-sync announcement dismissed |
|
||
|
||
### Update & developer flags
|
||
|
||
#### `justupdated`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **Purpose** | Set `true` after extension update; drives startup popup queue |
|
||
| **Example** | `true` |
|
||
|
||
#### `devMode`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **Default** | undefined / false |
|
||
| **UI** | Hidden unlock in settings |
|
||
| **Example** | `true` |
|
||
|
||
#### `hideSensitiveContent`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **UI** | Dev mode → Sensitive Hider |
|
||
| **Example** | `false` |
|
||
|
||
#### `mockNotices`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `boolean` |
|
||
| **UI** | Dev mode → Mock Notices |
|
||
| **Example** | `false` |
|
||
|
||
#### `devGhReleaseVersionOverride`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `string` |
|
||
| **UI** | Dev mode → version override field |
|
||
| **Format** | Semver string, e.g. `"3.7.0"` |
|
||
| **Example** | `"99.0.0"` |
|
||
|
||
#### `lastSeenNightlyPublishedAt`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `string` |
|
||
| **Format** | ISO 8601 timestamp from GitHub release `published_at` |
|
||
| **Example** | `"2026-06-01T04:30:00Z"` |
|
||
|
||
### Profile picture helper
|
||
|
||
#### `profile_picture_revision`
|
||
|
||
| | |
|
||
|-|-|
|
||
| **Type** | `number` |
|
||
| **Purpose** | Incremented when cloud/local profile picture changes; triggers UI refresh |
|
||
| **Not** | The image itself (stored in localforage `profile-picture-store`) |
|
||
| **Example** | `3` |
|
||
|
||
---
|
||
|
||
## Plugin settings objects
|
||
|
||
Each plugin stores one object at `plugin.{pluginId}.settings`. Plugins with `disableToggle: true` also store `enabled: boolean` (written from settings UI, not the settings proxy).
|
||
|
||
Settings of type `component` or `button` are **not** auto-persisted by the plugin settings proxy.
|
||
|
||
### `plugin.animated-background.settings`
|
||
|
||
| Field | Type | Default | Range / notes |
|
||
|-------|------|---------|---------------|
|
||
| `enabled` | `boolean` | `true` | From `disableToggle` UI |
|
||
| `speed` | `number` | `1` | **0.1–2.0**, step 0.05 in UI |
|
||
|
||
```json
|
||
{ "enabled": true, "speed": 1.05 }
|
||
```
|
||
|
||
### `plugin.assessments-average.settings`
|
||
|
||
| Field | Type | Default |
|
||
|-------|------|---------|
|
||
| `enabled` | `boolean` | `false` |
|
||
| `lettergrade` | `boolean` | `false` |
|
||
|
||
```json
|
||
{ "enabled": true, "lettergrade": false }
|
||
```
|
||
|
||
### `plugin.notificationCollector.settings`
|
||
|
||
| Field | Type | Default |
|
||
|-------|------|---------|
|
||
| `enabled` | `boolean` | `true` |
|
||
|
||
```json
|
||
{ "enabled": true }
|
||
```
|
||
|
||
### `plugin.timetable.settings`
|
||
|
||
| Field | Type | Default |
|
||
|-------|------|---------|
|
||
| `enabled` | `boolean` | `true` |
|
||
|
||
```json
|
||
{ "enabled": true }
|
||
```
|
||
|
||
### `plugin.timetableEdit.settings`
|
||
|
||
| Field | Type | Default |
|
||
|-------|------|---------|
|
||
| `enabled` | `boolean` | `true` |
|
||
|
||
```json
|
||
{ "enabled": true }
|
||
```
|
||
|
||
### `plugin.global-search.settings`
|
||
|
||
| Field | Type | Default | Notes |
|
||
|-------|------|---------|-------|
|
||
| `enabled` | `boolean` | `false` | Plugin default off |
|
||
| `searchHotkey` | `string` | `"ctrl+k"` or `"cmd+k"` | See hotkey format below |
|
||
| `showRecentFirst` | `boolean` | `true` | |
|
||
| `transparencyEffects` | `boolean` | `true` | Search bar blur |
|
||
| `runIndexingOnLoad` | `boolean` | `true` | |
|
||
| `passiveIndexing` | `boolean` | `true` | Index visited pages |
|
||
|
||
```json
|
||
{
|
||
"enabled": true,
|
||
"searchHotkey": "ctrl+shift+f",
|
||
"showRecentFirst": true,
|
||
"transparencyEffects": true,
|
||
"runIndexingOnLoad": true,
|
||
"passiveIndexing": true
|
||
}
|
||
```
|
||
|
||
#### Hotkey string format (`searchHotkey`)
|
||
|
||
- Lowercase, `+`-separated: `modifier[+modifier]+key`
|
||
- Modifiers: `ctrl`/`control`, `cmd`/`meta`/`command`, `alt`/`option`, `shift`
|
||
- Key: single character or name (`k`, `f`, etc.) — matched against `event.key` case-insensitively
|
||
- Valid iff at least one non-modifier key is present (`isValidHotkey`)
|
||
- **Examples:** `"ctrl+k"`, `"cmd+shift+p"`, `"alt+f"`
|
||
|
||
### `plugin.profile-picture.settings`
|
||
|
||
| Field | Type | Default |
|
||
|-------|------|---------|
|
||
| `enabled` | `boolean` | `false` |
|
||
| `useCloudPfp` | `boolean` | `false` |
|
||
|
||
```json
|
||
{ "enabled": true, "useCloudPfp": true }
|
||
```
|
||
|
||
When `useCloudPfp` is true, avatar URL comes from `bsplus_user.pfpUrl` (local auth, not synced) — other devices need their own login.
|
||
|
||
### `plugin.messageFolders.settings`
|
||
|
||
| Field | Type | Default |
|
||
|-------|------|---------|
|
||
| `enabled` | `boolean` | `true` |
|
||
| `showTagsInAllMessages` | `boolean` | `true` |
|
||
| `hideFolderedMessagesInAll` | `boolean` | `true` |
|
||
|
||
```json
|
||
{
|
||
"enabled": true,
|
||
"showTagsInAllMessages": true,
|
||
"hideFolderedMessagesInAll": true
|
||
}
|
||
```
|
||
|
||
### `plugin.enhanced-navigation.settings`
|
||
|
||
| Field | Type | Default |
|
||
|-------|------|---------|
|
||
| `enabled` | `boolean` | `true` |
|
||
| `autoScrollOnClick` | `boolean` | `false` |
|
||
|
||
```json
|
||
{ "enabled": true, "autoScrollOnClick": false }
|
||
```
|
||
|
||
### `plugin.background-music.settings`
|
||
|
||
| Field | Type | Default | Range |
|
||
|-------|------|---------|-------|
|
||
| `enabled` | `boolean` | `false` | |
|
||
| `volume` | `number` | `0.5` | 0–1 |
|
||
| `pauseOnHidden` | `boolean` | `true` | |
|
||
|
||
```json
|
||
{ "enabled": true, "volume": 0.75, "pauseOnHidden": true }
|
||
```
|
||
|
||
Audio blob: localforage `background-music-store` / `music` / key `audio-blob` — **not synced**.
|
||
|
||
### `plugin.grade-analytics.settings`
|
||
|
||
| Field | Type | Default | Range |
|
||
|-------|------|---------|-------|
|
||
| `cacheTtlHours` | `number` | `24` | 1–168 |
|
||
|
||
```json
|
||
{ "cacheTtlHours": 48 }
|
||
```
|
||
|
||
No `enabled` field — plugin uses `disableToggle: false` and always runs when registered (except Engage).
|
||
|
||
### Unused plugin settings keys
|
||
|
||
- `plugin.themes.settings` — not written (empty plugin settings)
|
||
- `plugin.assessments-overview.settings` — not written
|
||
|
||
---
|
||
|
||
## Plugin runtime storage keys
|
||
|
||
Separate top-level keys per property: `plugin.{pluginId}.storage.{propertyName}`.
|
||
|
||
### Message Folders (`plugin.messageFolders`)
|
||
|
||
#### `plugin.messageFolders.storage.folders`
|
||
|
||
```json
|
||
[
|
||
{
|
||
"id": "m1abc2def",
|
||
"name": "Important",
|
||
"color": "#3b82f6",
|
||
"emoji": "<svg …></svg>"
|
||
}
|
||
]
|
||
```
|
||
|
||
| Field | Type | Notes |
|
||
|-------|------|-------|
|
||
| `id` | `string` | `Date.now().toString(36) + random` |
|
||
| `name` | `string` | Display name |
|
||
| `color` | `string` | Hex colour from preset palette |
|
||
| `emoji` | `string` | Inline SVG icon markup (not unicode emoji) |
|
||
|
||
#### `plugin.messageFolders.storage.messageAssignments`
|
||
|
||
```json
|
||
{
|
||
"msg-uuid-1": ["folderId1"],
|
||
"msg-uuid-2": ["folderId1", "folderId2"]
|
||
}
|
||
```
|
||
|
||
Keys: SEQTA message identifiers. Values: arrays of folder `id` strings.
|
||
|
||
### Timetable Edit (`plugin.timetableEdit`)
|
||
|
||
#### `plugin.timetableEdit.storage.timetableOverrides`
|
||
|
||
Keyed by class instance id (`ci` as string):
|
||
|
||
```json
|
||
{
|
||
"42": { "room": "B204", "staff": "Mr Smith" },
|
||
"17": { "staff": "Ms Jones" }
|
||
}
|
||
```
|
||
|
||
#### `plugin.timetableEdit.storage.timetableOverridesBySubject`
|
||
|
||
Keyed by subject description string:
|
||
|
||
```json
|
||
{
|
||
"10 Mathematics": { "room": "MA1" }
|
||
}
|
||
```
|
||
|
||
Entry shape: `{ "room"?: string, "staff"?: string }` — either field optional.
|
||
|
||
### Assessment Averages (`plugin.assessments-average`)
|
||
|
||
#### `plugin.assessments-average.storage.weightingOverrides` — **synced**
|
||
|
||
User manual weight overrides by assessment id:
|
||
|
||
```json
|
||
{
|
||
"123456": "25",
|
||
"123457": "N/A"
|
||
}
|
||
```
|
||
|
||
Values are weight strings as shown in UI (percentage text or `"N/A"`).
|
||
|
||
#### `plugin.assessments-average.storage.assessments` — **NOT synced**
|
||
|
||
Title → assessment id map (school data):
|
||
|
||
```json
|
||
{
|
||
"Semester 1 Exam": "123456"
|
||
}
|
||
```
|
||
|
||
#### `plugin.assessments-average.storage.weightings` — **NOT synced**
|
||
|
||
```json
|
||
{
|
||
"123456": {
|
||
"weight": "25",
|
||
"fingerprint": "[\"GRADED\",true,\"…\"]",
|
||
"pluginVersion": 1,
|
||
"refreshing": false
|
||
}
|
||
}
|
||
```
|
||
|
||
| Field | Type | Notes |
|
||
|-------|------|-------|
|
||
| `weight` | `string` | e.g. `"25"`, `"processing"`, `"N/A"` |
|
||
| `fingerprint` | `string` | JSON-stringified assessment state fingerprint |
|
||
| `pluginVersion` | `number` | `WEIGHTING_SCHEMA_VERSION` (= 1) |
|
||
| `refreshing` | `boolean` | optional; background refetch in progress |
|
||
|
||
### Notification Collector (`plugin.notificationCollector`)
|
||
|
||
| Key | Type | Example |
|
||
|-----|------|---------|
|
||
| `plugin.notificationCollector.storage.lastNotificationCount` | `number` | `3` |
|
||
| `plugin.notificationCollector.storage.consecutiveErrors` | `number` | `0` |
|
||
| `plugin.notificationCollector.storage.lastCheckedTime` | `string` | `"2026-06-09T14:30:00.000Z"` |
|
||
|
||
All **synced** (not under an excluded prefix).
|
||
|
||
---
|
||
|
||
## Excluded caches (local-only schemas)
|
||
|
||
### Grade Analytics — prefix `bsplus.analytics.`
|
||
|
||
#### `bsplus.analytics.v2.{origin}.{studentId}`
|
||
|
||
- `origin`: full school origin, e.g. `https://school.seqta.com.au` (dots in hostname appear in key)
|
||
- `studentId`: numeric SEQTA student id
|
||
|
||
```json
|
||
{
|
||
"updatedAt": 1717939200000,
|
||
"assessments": [
|
||
{
|
||
"id": 1,
|
||
"title": "Assignment 1",
|
||
"subject": "10 Mathematics",
|
||
"status": "MARKS_RELEASED",
|
||
"due": "2026-03-15",
|
||
"code": "10MAT",
|
||
"metaclassID": 100,
|
||
"programmeID": 10,
|
||
"graded": true,
|
||
"overdue": false,
|
||
"hasFeedback": true,
|
||
"expectationsEnabled": false,
|
||
"expectationsCompleted": false,
|
||
"reflectionsEnabled": false,
|
||
"reflectionsCompleted": false,
|
||
"availability": "FULL",
|
||
"finalGrade": 85,
|
||
"letterGrade": "A"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
`status` enum: `"OVERDUE"` | `"MARKS_RELEASED"` | `"PENDING"`.
|
||
|
||
#### `bsplus.analytics.distMode.v1.{origin}.{studentId}`
|
||
|
||
Scalar string: `"auto"` | `"letter"` | `"percent"`.
|
||
|
||
### Global Search — prefix `plugin.global-search.storage.`
|
||
|
||
Reserved for future device-local caches. No keys in use today; any key matching the prefix is excluded.
|
||
|
||
---
|
||
|
||
## Data outside `chrome.storage.local`
|
||
|
||
These affect UX but **never** appear in the cloud `data` blob:
|
||
|
||
| Store | Location | Contents |
|
||
|-------|----------|----------|
|
||
| Global Search structured index | IndexedDB `betterseqta-index` | Searchable page text |
|
||
| Global Search vectors | IndexedDB `embeddiaDB` | Embedding index |
|
||
| Global Search schema version | `localStorage` key `betterseqta-index-version` | Index migration |
|
||
| Installed themes | localforage (default instance) | `CustomTheme` objects keyed by theme id; index `customThemes` |
|
||
| Profile picture upload | localforage `profile-picture-store` / `profilePicture` | Image blob |
|
||
| Cloud PFP cache | localforage `cloud-pfp-store` / `cloudPfp` | Cached avatar blobs |
|
||
| Background music | localforage `background-music-store` / `music` / `audio-blob` | Audio blob |
|
||
| Dev API override | `sessionStorage` `bsplus_dev_api_base` | Staging server URL |
|
||
| Engage student context | `localStorage` `bsplus.engageTimetable.student.{origin}` | Current student id |
|
||
|
||
After cloud restore, devices may need to **rebuild** indexes, **re-upload** media, or **re-download** themes even when synced settings reference them.
|
||
|
||
---
|
||
|
||
## Catch-all & forward compatibility
|
||
|
||
### Inclusion test
|
||
|
||
```ts
|
||
isKeyIncludedInCloudUploadPayload(key) === !shouldOmitKeyFromCloudPayload(key)
|
||
```
|
||
|
||
Any new `chrome.storage.local` key syncs automatically unless added to:
|
||
|
||
- `KEYS_OMITTED_FROM_CLOUD_UPLOAD`
|
||
- `SENSITIVE_DEVICE_STORAGE_KEYS_EXACT`
|
||
- `CLIENT_ONLY_CLOUD_KEYS_EXACT`
|
||
- `SENSITIVE_DEVICE_STORAGE_KEY_PREFIXES`
|
||
|
||
### Upload trigger flow
|
||
|
||
1. `browser.storage.onChanged` (area `local`)
|
||
2. At least one changed key passes `isKeyIncludedInCloudUploadPayload`
|
||
3. `autoCloudSettingsSync !== false`
|
||
4. Valid access token
|
||
5. Not during restore → schedule 2 s debounce → `PUT` sparse patch (skip if empty)
|
||
|
||
### Server storage suggestion
|
||
|
||
Store the full PUT body (or at minimum `{ schemaVersion, themeId, data }`) as JSONB per user. `data` alone is sufficient for restore if `themeId` is duplicated inside `data.selectedTheme`.
|
||
|
||
### Versioning
|
||
|
||
- Client `schemaVersion`: **1**
|
||
- If server returns higher `schemaVersion` in cloud-summary, auto-download is skipped until client is updated.
|
||
|
||
---
|
||
|
||
## UI accessibility
|
||
|
||
Which synced keys can be changed directly by the user in the extension **settings popup** (SEQTA → BetterSEQTA+ settings) or other in-product UI — vs keys that are written only by the app, popups, or plugins at runtime.
|
||
|
||
**Settings popup tabs:** Settings · Shortcuts · Themes (`src/interface/pages/settings.svelte`).
|
||
|
||
### Extension popup — Settings tab (`general.svelte`)
|
||
|
||
| Storage key / path | UI label | Control |
|
||
|--------------------|----------|---------|
|
||
| `iconOnlySidebar` | Icon Only Sidebar | Switch |
|
||
| `animations` | Animations | Switch |
|
||
| `timeFormat` | 12 Hour Time | Switch (`"12"` / `"24"`) |
|
||
| `transparencyEffects` | Transparency Effects | Switch |
|
||
| `defaultPage` | Default Page | Select |
|
||
| `newsSource` | News Feed Source | Select |
|
||
| `selectedColor` | Custom Theme Colour | Button → colour picker modal |
|
||
| `selectedFont` | Interface Font | Button → font picker modal |
|
||
| `adaptiveThemeColour` | Adaptive Theme Colour | Switch |
|
||
| `adaptiveThemeGradient` | Soft Gradient | Switch (when adaptive colour on) |
|
||
| `adaptiveThemeColourTransition` | Smooth colour transition | Switch (when adaptive colour on) |
|
||
| `menuorder`, `menuitems`, `defaultmenuorder` | Edit Sidebar Layout | Button → opens editor **on the SEQTA tab** (not inside the popup) |
|
||
| `themeOfTheMonthDisabled` | Theme of the Month | Switch (under Global Search plugin block) |
|
||
| `autoCloudSettingsSync` | Automatic sync | Switch (BetterSEQTA Cloud card, signed in only) |
|
||
| `onoff` | BetterSEQTA+ | Switch (bottom of tab) |
|
||
|
||
#### Plugin blocks on Settings tab
|
||
|
||
Each row is `plugin.{id}.settings.{field}` unless noted. Component settings (upload UIs) change synced **preference** keys but not blob data in IndexedDB.
|
||
|
||
| Plugin | UI | Synced fields from UI |
|
||
|--------|-----|------------------------|
|
||
| **Animated Background** | Enable + Animation Speed slider | `enabled`, `speed` |
|
||
| **Assessment Averages** | Enable (+ disclaimer) + Letter Grades | `enabled`, `lettergrade` |
|
||
| **Notification Collector** | Enable only | `enabled` |
|
||
| **Timetable Enhancer** | Enable only | `enabled` |
|
||
| **Edit Rooms & Teachers** | Enable only | `enabled` |
|
||
| **Global Search** | Enable + hotkey + 4 toggles + Reset Index button | `enabled`, `searchHotkey`, `showRecentFirst`, `transparencyEffects`, `runIndexingOnLoad`, `passiveIndexing` — Reset Index does **not** sync index data |
|
||
| **Custom Profile Picture** | Enable + Use cloud PFP (if signed in) + upload/remove | `enabled`, `useCloudPfp`; upload updates `profile_picture_revision` (image blob **not** synced) |
|
||
| **Message Folders** | Enable + 2 toggles | `enabled`, `showTagsInAllMessages`, `hideFolderedMessagesInAll` |
|
||
| **Enhanced Navigation** | Enable + Auto-scroll | `enabled`, `autoScrollOnClick` |
|
||
| **Background Music** | Enable + volume + pause on hidden + upload | `enabled`, `volume`, `pauseOnHidden`; audio blob **not** synced |
|
||
| **Grade Analytics** | Cache duration (hours) slider only | `cacheTtlHours` |
|
||
|
||
**Not shown on Settings tab** (empty `settings` + no enable toggle → card hidden):
|
||
|
||
- **Themes** plugin — use **Themes** tab instead
|
||
- **Assessments Overview** plugin — no settings UI
|
||
|
||
#### Dev mode only (Settings tab, after typing “dev” on logo)
|
||
|
||
| Storage key | UI label |
|
||
|-------------|----------|
|
||
| `devMode` | Developer Mode |
|
||
| `hideSensitiveContent` | Sensitive Hider |
|
||
| `mockNotices` | Mock Notices |
|
||
| `privacyStatementShown`, `privacyStatementLastUpdated` | Show Privacy Notification (resets to show popup) |
|
||
| `devGhReleaseVersionOverride` | GitHub latest version override (text field) |
|
||
|
||
Dev **Export cloud settings JSON** downloads the upload payload; it does not change storage.
|
||
|
||
### Extension popup — Shortcuts tab (`shortcuts.svelte`)
|
||
|
||
| Storage key | UI |
|
||
|-------------|-----|
|
||
| `shortcuts` | Toggle built-in shortcuts (Outlook, Office, Google, … from `links.json`) |
|
||
| `customshortcuts` | Add / delete custom shortcuts (name, URL, optional SVG icon) |
|
||
|
||
Changes use `settingsState.setKey()` so the SEQTA home page updates immediately via `StorageChangeHandler` → `renderShortcuts()` (embedded settings run in the content script, where `storage.onChanged` does not fire for local writes). Empty arrays are persisted explicitly (`customshortcuts: []`) so cloud restore and other devices clear removed shortcuts.
|
||
|
||
### Extension popup — Themes tab (`theme.svelte`)
|
||
|
||
| Storage key | UI |
|
||
|-------------|-----|
|
||
| `selectedTheme` | Install / select / clear store themes (`ThemeSelector`) |
|
||
| `selectedColor` | May change when applying a theme with a default colour |
|
||
| `originalSelectedColor`, `originalDarkMode` | Set internally during theme **preview** in theme manager (not dedicated controls) |
|
||
|
||
Animated background selection uses `BackgroundSelector` (local/custom backgrounds); store theme id still goes to `selectedTheme`.
|
||
|
||
### SEQTA page UI (outside settings popup)
|
||
|
||
| Storage key / path | Where | Control |
|
||
|--------------------|-------|---------|
|
||
| `DarkMode` | SEQTA top bar | Sun/moon **Light/Dark** button |
|
||
| `menuorder`, `menuitems` | SEQTA sidebar | **Edit Sidebar Layout** overlay (drag order + per-item toggles) |
|
||
| `subjectfilters` | Home → upcoming assessments | Per-subject checkboxes (`#upcoming-filters`) |
|
||
| `subjectfilters` | Assessments → Overview | Subject filter UI in overview grid |
|
||
| `plugin.messageFolders.storage.folders` | Messages page | Create/edit/delete folders |
|
||
| `plugin.messageFolders.storage.messageAssignments` | Messages page | Assign messages to folders |
|
||
| `plugin.timetableEdit.storage.timetableOverrides` | Timetable | Edit room/teacher on a class |
|
||
| `plugin.timetableEdit.storage.timetableOverridesBySubject` | Timetable | Subject-level overrides |
|
||
| `plugin.assessments-average.storage.weightingOverrides` | Assessments page | Per-assessment “Override %” inputs |
|
||
| Global Search | SEQTA | Hotkey opens search bar (hotkey set in Settings) |
|
||
|
||
### Automatic / popup-only (synced, no dedicated settings control)
|
||
|
||
| Storage key / path | How it changes |
|
||
|--------------------|----------------|
|
||
| `lessonalert` | Default only — **no UI** (read by home loader) |
|
||
| `privacyStatementShown`, `privacyStatementLastUpdated` | Privacy popup on first run (dev can reset) |
|
||
| `engageParentsAnnouncementShown` | Dismiss Engage parents announcement |
|
||
| `bsCloudAutoSyncAnnouncementShown` | Dismiss cloud sync announcement |
|
||
| `themeOfTheMonthDismissedMonth` | Dismiss Theme of the Month popup |
|
||
| `justupdated` | Set `true` after extension update |
|
||
| `lastSeenNightlyPublishedAt` | Dismiss / acknowledge GitHub nightly update badge |
|
||
| `profile_picture_revision` | Incremented when profile picture changes |
|
||
| `plugin.notificationCollector.storage.*` | Written by notification poll loop |
|
||
| Legacy `animatedbk`, `bksliderinput`, etc. | Migrated to plugin settings; no longer shown in UI |
|
||
|
||
### Synced plugin prefs with UI vs storage-only side effects
|
||
|
||
| User action in UI | Synced | Not synced |
|
||
|-------------------|--------|------------|
|
||
| Upload profile picture | `useCloudPfp`, `profile_picture_revision` | Image blob (localforage) |
|
||
| Upload background music | `enabled`, `volume`, `pauseOnHidden` | Audio blob (localforage) |
|
||
| Install store theme | `selectedTheme`, often `selectedColor` / `DarkMode` | Theme assets (localforage) |
|
||
| Global Search indexing | Plugin settings toggles | IndexedDB `betterseqta-index`, `embeddiaDB` |
|
||
| Assessment averages | `lettergrade`, overrides | `storage.assessments`, `storage.weightings` caches |
|
||
| Grade Analytics TTL slider | `cacheTtlHours` | `bsplus.analytics.*` caches |
|
||
|
||
### Never in settings UI (excluded from sync entirely)
|
||
|
||
OAuth keys, analytics caches, assessment weighting caches, global-search storage prefix, client watermarks — see [Exclusion rules](#exclusion-rules).
|
||
|
||
---
|
||
|
||
## Default schema initialization
|
||
|
||
Implementation: `ensureSyncableStorageDefaults()` + `getSyncableStorageDefaults()`.
|
||
|
||
### What gets written when a key is missing
|
||
|
||
1. **All core `SettingsState` fields** from `getDefaultSettingsState()` — including `shortcuts`, `customshortcuts: []`, `menuitems`, `selectedFont: "rubik"`, `timeFormat: "24"`, announcement flags defaulting to `false`, etc.
|
||
2. **Every registered plugin’s** `plugin.{pluginId}.settings` object (defaults from plugin definitions; `enabled` included for `disableToggle` plugins).
|
||
3. **Legacy migration** on that flat map so upload-shaped storage uses `plugin.animated-background.settings` rather than `animatedbk` / `bksliderinput`.
|
||
|
||
### What is not auto-initialized
|
||
|
||
| Category | Reason |
|
||
|----------|--------|
|
||
| OAuth / session keys | Excluded from cloud; device-local |
|
||
| `plugin.*.storage.*` runtime data | Created when plugins run (folders, timetable overrides, etc.) |
|
||
| `bsplus.analytics.*` | Excluded device/school caches |
|
||
| `plugin.assessments-average.storage.assessments` / `.weightings` | Excluded school caches |
|
||
| `plugin.global-search.storage.*` | Excluded prefix |
|
||
| Client-only watermarks | Never uploaded |
|
||
|
||
### Settings persistence and live UI
|
||
|
||
`SettingsState` (`src/seqta/utils/listeners/SettingsState.ts`):
|
||
|
||
- Assignments (`settingsState.foo = …`) and `setKey()` persist to `chrome.storage.local` and **notify registered listeners** in the same context (required for embedded SEQTA settings).
|
||
- `saveToStorage()` omits `undefined` values so optional keys are not accidentally stripped.
|