Add Outline integration: push lesson notes to a topic collection
Pick a topic on a lesson's note and it pushes to that topic's collection in a self-hosted Outline instance when you leave the page; notes without a topic stay local-only. Settings gets a new Outline Integration card (URL/token, Save, Test Connection). Credential handling: the API token lives in its own outline_config.json in DATA_DIR, deliberately kept out of the general settings flow (GET /api/settings is fetched on every page load by theme.js - no place for a secret to ride along). GET /api/outline/config only ever returns whether it's configured, never the token itself. The topic chooser reflects Outline's live collection list rather than a locally cached copy, and resolves a newly-typed topic name to a real collection immediately (find-by-name-or-create) rather than waiting until the note is pushed. That's not just an optimization: the push itself fires via navigator.sendBeacon() on pagehide, which can't read a response, so a page that fires pagehide more than once for the same load (a back/forward- cache restore, for instance) would otherwise re-send the same "create a new topic" intent every time and spawn duplicate collections. Verified live against a local mock Outline server that firing pagehide repeatedly for the same lesson creates the collection/document once and updates thereafter. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -205,6 +205,49 @@ you need real data for every result you return).
|
||||
progress+notes file (stdlib `zipfile`, no new dependency) - not the
|
||||
course files themselves.
|
||||
|
||||
## Outline integration (this session)
|
||||
|
||||
Push lesson notes to a self-hosted Outline instance
|
||||
(`https://mikeline.michaelsitz.com`), organized by a "topic" the user picks
|
||||
per note - each topic is its own Outline collection. Only notes with a
|
||||
topic selected get pushed; everything else stays local-only, same as
|
||||
before.
|
||||
|
||||
- **Credential handling**: the API token lives in its own
|
||||
`OUTLINE_CONFIG_FILE` (`outline_config.json` in `DATA_DIR`), deliberately
|
||||
*not* part of `DEFAULT_SETTINGS`/`load_settings` - those flow through
|
||||
`GET /api/settings`, which `theme.js` fetches on every page load, and a
|
||||
secret has no business riding along in that response. `GET
|
||||
/api/outline/config` only ever returns `{configured, base_url}`, never
|
||||
the token. Confirmed via a real (mocked-backend) test: token round-trips
|
||||
through save but never comes back out on GET.
|
||||
- **Topic chooser = live Outline collections**, not a locally-cached list -
|
||||
`GET /api/outline/collections` proxies `collections.list` fresh every
|
||||
time. Naming a new topic resolves it to a real collection **immediately**
|
||||
(find-by-exact-name-or-create, `resolve_outline_topic()` /
|
||||
`POST /api/outline/resolve-topic`) the moment you tab out of the "new
|
||||
topic" field, rather than waiting until the note is pushed.
|
||||
- **Why eager resolution, not deferred**: the first design deferred
|
||||
collection creation to push-time (avoid an empty collection if the topic
|
||||
was never actually used) - caught a real bug testing it: the push fires
|
||||
via `navigator.sendBeacon()` on `pagehide`, which can't read a response,
|
||||
so a page that fires `pagehide` more than once for the same load (a
|
||||
browser back/forward-cache restore, for instance) would silently
|
||||
re-create a *new* collection every time, since the client had no way to
|
||||
learn the topic name had already been resolved. Fixed by resolving up
|
||||
front and by making the deferred fallback path dedupe-by-name too
|
||||
(`resolve_outline_topic`) - verified live (mocked Outline backend) that
|
||||
firing multiple sequential `pagehide` events for the same lesson creates
|
||||
the collection/document once and updates thereafter, never duplicates.
|
||||
- **Push flow**: `POST /api/outline/push` - creates the Outline document on
|
||||
first push, updates the same one (by the id stashed in the lesson's
|
||||
progress-file entry, alongside the existing `note` field) on every push
|
||||
after. No-ops if the note is empty.
|
||||
- Verified end-to-end against a local mock Outline server (not the real
|
||||
instance - no write-testing against live external services this
|
||||
session), including Settings → Test Connection hitting the real HTTP
|
||||
path.
|
||||
|
||||
## Known limitations still open
|
||||
- App is unauthenticated by design (matches upstream) — settings and hidden-path
|
||||
curation apply app-wide, not per-browser/per-user.
|
||||
|
||||
Reference in New Issue
Block a user