oficina / memoria Public

πŸ€– chore(copy): the connector's string index, for review

Every user-facing string the Notion connector adds, pulled out of the
templates and handlers with the context needed to judge it: 63 of them,
including the two that carry the most weight β€” what happens when you
disconnect, and the fact that memoria keeps what it imports.

Committed so the review survives this worktree. The server's own output
is not source and is ignored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VauJkQsPeGRErbWFiQNZzC
Paul Campbell pushed by paul@keymail.dev 1cfef19287e54943f6454ac000f6871c3047bde5 parent c70d6ff
2 files changed, +456 −0
  • .gitignore +5 −0
  • copy-review/strings.json +451 −0
diff --git a/.gitignore b/.gitignore
index 5c0d95e..68911ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,8 @@
/memoria.db
/memoria.db-wal
/memoria.db-shm
+
+# Copy review: the string index is committed so a review survives the
+# worktree, but the server's own output is not source.
+/copy-review/result.json
+/copy-review/server.log
diff --git a/copy-review/strings.json b/copy-review/strings.json
new file mode 100644
index 0000000..e5b05d6
--- /dev/null
+++ b/copy-review/strings.json
@@ -0,0 +1,451 @@
+[
+ {
+ "id": "nav.sources",
+ "section": "Navigation",
+ "label": "Header link",
+ "text": "Sources",
+ "context": "A link in the top bar of every page, next to Sign out. It leads to the list of places memoria can pull work in from, which right now is only Notion.",
+ "notes": "One or two words; sits beside a Sign out button"
+ },
+ {
+ "id": "connectors.heading",
+ "section": "Sources list",
+ "label": "Page heading",
+ "text": "Sources",
+ "context": "The heading of the page listing everywhere memoria can import from."
+ },
+ {
+ "id": "connectors.lede",
+ "section": "Sources list",
+ "label": "Introduction",
+ "text": "memoria can pull in work that lives outside the suite. What you connect, memoria keeps: disconnecting a source stops memoria seeing new changes, it does not remove what memoria already holds.",
+ "context": "Directly under the Sources heading. Its second job is the important one: to warn, before anyone connects anything, that importing is a one-way door. Content that comes in stays in until someone deletes it here.",
+ "notes": "The 'what you connect, memoria keeps' idea has to survive any rewrite β€” it is the promise the whole feature is built on"
+ },
+ {
+ "id": "connectors.notion.name",
+ "section": "Sources list",
+ "label": "Source name",
+ "text": "Notion",
+ "context": "The name of the one source in the list, as a link to its settings."
+ },
+ {
+ "id": "connectors.notion.connected",
+ "section": "Sources list",
+ "label": "Status when set up",
+ "text": "Connected.",
+ "context": "Shown under the Notion link when a token is stored."
+ },
+ {
+ "id": "connectors.notion.notconnected",
+ "section": "Sources list",
+ "label": "Status when not set up",
+ "text": "Not connected.",
+ "context": "Shown under the Notion link when nothing has been set up yet."
+ },
+ {
+ "id": "connectors.notion.description",
+ "section": "Sources list",
+ "label": "Source description",
+ "text": "Imports the pages you share with a Notion integration, and everything nested under them.",
+ "context": "One line under the Notion link saying what connecting it would do. The 'and everything nested under them' half matters: people expect to share one top page and get the whole tree, and they do."
+ },
+
+ {
+ "id": "notion.heading",
+ "section": "Notion β€” setup",
+ "label": "Page heading",
+ "text": "Notion",
+ "context": "Heading of the Notion settings page, in both the not-yet-connected and connected states."
+ },
+ {
+ "id": "notion.lede",
+ "section": "Notion β€” setup",
+ "label": "Introduction",
+ "text": "memoria imports the Notion pages you share with an integration, and everything nested underneath them.",
+ "context": "The first line on the Notion page before anything is connected. Says what is about to happen."
+ },
+ {
+ "id": "notion.keeps.callout",
+ "section": "Notion β€” setup",
+ "label": "The one-way-door warning",
+ "text": "What you connect, memoria keeps. Once a page is imported, it stays in memoria and stays readable by everyone here β€” even if you later archive it in Notion, unshare it, or revoke this token. Removing it is something you do in memoria.",
+ "context": "A boxed warning shown before the token field, so it is read before anyone connects rather than discovered afterwards. This is the single most consequential sentence in the feature: memoria deliberately does not delete things when they disappear from Notion, because it is meant to replace Notion, and someone archiving a migrated page must not lose it. If this is soft or vague, people will be surprised later.",
+ "notes": "The opening phrase is bold on the page. Accuracy matters more than brevity here."
+ },
+ {
+ "id": "notion.connect.heading",
+ "section": "Notion β€” setup",
+ "label": "Section heading",
+ "text": "Connect",
+ "context": "Heading above the three setup steps and the token field."
+ },
+ {
+ "id": "notion.step1",
+ "section": "Notion β€” setup",
+ "label": "Step 1",
+ "text": "In Notion, create an internal integration with read-only capabilities and no user information.",
+ "context": "First of three numbered steps. It asks for the narrowest possible access, because memoria cannot enforce that itself β€” Notion decides what a token can do, so all we can do is ask for the right kind.",
+ "notes": "'internal integration' is Notion's own term and has to stay recognisable"
+ },
+ {
+ "id": "notion.step2",
+ "section": "Notion β€” setup",
+ "label": "Step 2",
+ "text": "Share the pages you want memoria to hold with that integration. Sharing a page is what publishes it β€” memoria imports what the integration can see and nothing else.",
+ "context": "Second step. The point being made is that there is no second list of pages to choose inside memoria β€” sharing in Notion IS the choice, and it is the only one.",
+ "notes": "Should make clear the user is in control of scope without implying memoria has its own picker"
+ },
+ {
+ "id": "notion.step3",
+ "section": "Notion β€” setup",
+ "label": "Step 3",
+ "text": "Paste the integration token below.",
+ "context": "Third and last step, immediately above the token field."
+ },
+ {
+ "id": "notion.token.label",
+ "section": "Notion β€” setup",
+ "label": "Token field label",
+ "text": "Integration token",
+ "context": "Label on the password field where the Notion token is pasted."
+ },
+ {
+ "id": "notion.token.help",
+ "section": "Notion β€” setup",
+ "label": "Token field help",
+ "text": "Checked with Notion before it is saved. It is stored sealed and is never shown again.",
+ "context": "Small text under the token field. Two reassurances: a wrong token fails here rather than silently at 3am, and the token is encrypted at rest and never displayed back β€” not even partly masked."
+ },
+ {
+ "id": "notion.connect.button",
+ "section": "Notion β€” setup",
+ "label": "Submit button",
+ "text": "Connect",
+ "context": "The button that saves the token. It does not start an import."
+ },
+
+ {
+ "id": "notion.connected.heading",
+ "section": "Notion β€” connected",
+ "label": "Section heading",
+ "text": "Connected",
+ "context": "Heading of the first section once a token is stored."
+ },
+ {
+ "id": "notion.verified",
+ "section": "Notion β€” connected",
+ "label": "Token last checked",
+ "text": "Token last checked",
+ "context": "Followed by a date and time, e.g. 'Token last checked 5 Sep 2026, 00:37.' Says when Notion last confirmed the token works.",
+ "notes": "A date is appended after this text, then a full stop"
+ },
+ {
+ "id": "notion.stored",
+ "section": "Notion β€” connected",
+ "label": "Token stored (no check date)",
+ "text": "Token stored.",
+ "context": "Shown instead of the line above when there is no record of when the token was last checked."
+ },
+ {
+ "id": "notion.counts",
+ "section": "Notion β€” connected",
+ "label": "Import progress summary",
+ "text": "of",
+ "context": "Renders as e.g. '4 of 12 known pages imported.' Two numbers with this word between them, then 'known pages imported.' β€” how many of the pages memoria has heard of have actually been brought in.",
+ "notes": "The full sentence is: {n} of {n} known page(s) imported. Only the wording around the numbers is up for review."
+ },
+ {
+ "id": "notion.counts.unknown",
+ "section": "Notion β€” connected",
+ "label": "When the counts cannot be read",
+ "text": "The page counts could not be read, so this says nothing about how much has been imported.",
+ "context": "Replaces the numbers when the database could not be queried. It exists because showing '0 of 0 imported' after a failure would state something false β€” that nothing is there β€” when in fact we simply failed to look.",
+ "notes": "Must not read as 'nothing has been imported'"
+ },
+ {
+ "id": "notion.import.button",
+ "section": "Notion β€” connected",
+ "label": "Start import button",
+ "text": "Import now",
+ "context": "Starts an import. Also used to start one again after a previous run finished or was interrupted."
+ },
+ {
+ "id": "notion.import.button.running",
+ "section": "Notion β€” connected",
+ "label": "Start import button, while running",
+ "text": "Importing…",
+ "context": "The same button while an import is in progress, when it is disabled."
+ },
+
+ {
+ "id": "run.running",
+ "section": "Notion β€” import status",
+ "label": "Status: running",
+ "text": "Importing now. This runs while this page is open, and picks up where it stopped if it is interrupted.",
+ "context": "Shown while an import is going. The second half is an honest admission rather than a nicety: memoria has no background worker, the app is put to sleep when nobody is using it, and a large workspace takes tens of minutes. Keeping the page open is genuinely what keeps the import alive.",
+ "notes": "Should not imply the import continues after the tab is closed, because it does not"
+ },
+ {
+ "id": "run.done",
+ "section": "Notion β€” import status",
+ "label": "Status: finished",
+ "text": "Import finished.",
+ "context": "Shown when an import completed with nothing going wrong."
+ },
+ {
+ "id": "run.done.withfailures",
+ "section": "Notion β€” import status",
+ "label": "Status: finished, some pages unreadable",
+ "text": "Import finished, but some pages could not be read. They are listed as failed below rather than left out of the count.",
+ "context": "Shown when the import finished but individual pages could not be fetched β€” usually because someone deleted or unshared one mid-run. The second sentence is the point: those pages are counted and named, never silently dropped, because a few missing pages inside a couple of thousand would otherwise look like a clean import."
+ },
+ {
+ "id": "run.interrupted",
+ "section": "Notion β€” import status",
+ "label": "Status: interrupted",
+ "text": "The import stopped before it finished β€” usually because this app went to sleep. Nothing was lost. Start it again to carry on from where it stopped.",
+ "context": "Shown when a run was killed part-way, which is the normal outcome for a big workspace since the platform sleeps idle apps. Needs to be reassuring and specific: work already done is kept, and restarting resumes rather than starts over.",
+ "notes": "This is the most common non-success state, not an edge case"
+ },
+ {
+ "id": "facts.started",
+ "section": "Notion β€” import status",
+ "label": "Detail label",
+ "text": "Started",
+ "context": "Label in the list of import details, followed by a date and time."
+ },
+ {
+ "id": "facts.finished",
+ "section": "Notion β€” import status",
+ "label": "Detail label",
+ "text": "Finished",
+ "context": "Label in the list of import details, followed by a date and time. Only appears once a run has ended."
+ },
+ {
+ "id": "facts.roots",
+ "section": "Notion β€” import status",
+ "label": "Detail label",
+ "text": "Top-level pages shared with the integration",
+ "context": "Followed by a number. This is how many pages were shared with the integration directly, as opposed to found by walking down inside them. It is here so that someone who expected more can tell whether the problem is what they shared in Notion.",
+ "notes": "Long, but it is doing real work β€” a shorter label loses the distinction between shared and discovered"
+ },
+ {
+ "id": "facts.seen",
+ "section": "Notion β€” import status",
+ "label": "Detail label",
+ "text": "Pages seen",
+ "context": "Followed by a number: how many pages the import found in total, including nested ones."
+ },
+ {
+ "id": "facts.seen.partial",
+ "section": "Notion β€” import status",
+ "label": "Page count while still running",
+ "text": "so far β€” the total is not known until the import finishes",
+ "context": "Follows a number while an import is in progress, e.g. '412 so far β€” the total is not known until the import finishes.' memoria discovers the tree as it walks it, so there is genuinely no denominator until the end. This says that instead of inventing a percentage.",
+ "notes": "Must not imply we know how many are left"
+ },
+ {
+ "id": "facts.imported",
+ "section": "Notion β€” import status",
+ "label": "Detail label",
+ "text": "Imported",
+ "context": "Followed by a number: pages actually brought in or updated on this run."
+ },
+ {
+ "id": "facts.uptodate",
+ "section": "Notion β€” import status",
+ "label": "Detail label",
+ "text": "Already up to date",
+ "context": "Followed by a number: pages that were checked and had not changed since last time, so nothing needed doing."
+ },
+ {
+ "id": "facts.empty",
+ "section": "Notion β€” import status",
+ "label": "Detail label",
+ "text": "Empty pages",
+ "context": "Followed by a number: pages with a title and no content. They are still imported, so that they can be found by their title, and counted here so they do not look like a failure."
+ },
+ {
+ "id": "facts.failed",
+ "section": "Notion β€” import status",
+ "label": "Detail label",
+ "text": "Could not be read",
+ "context": "Followed by a number: pages that were found but could not be fetched β€” usually deleted or unshared during the import."
+ },
+ {
+ "id": "facts.failed.note",
+ "section": "Notion β€” import status",
+ "label": "Note beside the failure count",
+ "text": "β€” counted here rather than left out of the totals",
+ "context": "Small text after the number of unreadable pages, when there are any. It is there to say the number is trustworthy: nothing quietly vanished."
+ },
+ {
+ "id": "facts.unread",
+ "section": "Notion β€” import status",
+ "label": "Detail label",
+ "text": "Blocks memoria does not read",
+ "context": "Followed by a number. Notion keeps adding new kinds of content block; memoria skips ones it does not understand rather than failing the whole import. This counts them.",
+ "notes": "'Blocks' is Notion's own word for the pieces a page is made of"
+ },
+ {
+ "id": "facts.unread.note",
+ "section": "Notion β€” import status",
+ "label": "Note beside the unread-block count",
+ "text": "β€” a page that came in thin may be one of these",
+ "context": "Small text after that number. Its job is to give someone an explanation for a page that imported with less content than they expected, instead of leaving them to assume the import is broken."
+ },
+ {
+ "id": "notion.lastimport.heading",
+ "section": "Notion β€” import status",
+ "label": "Section heading",
+ "text": "Last import",
+ "context": "Heading above the status sentence and the list of details."
+ },
+
+ {
+ "id": "fail.unauthorized",
+ "section": "Notion β€” failures",
+ "label": "Token rejected",
+ "text": "Notion refused the token. Check it is still valid and still shared with the pages you want.",
+ "context": "Shown when an import failed because Notion would not accept the stored token β€” it was revoked, expired, or unshared from everything."
+ },
+ {
+ "id": "fail.ratelimited",
+ "section": "Notion β€” failures",
+ "label": "Rate limited",
+ "text": "Notion asked us to slow down for longer than we waited. Nothing was lost β€” start the import again.",
+ "context": "Shown when Notion throttled us past the point we were willing to keep waiting. Not the user's fault and not damaging; restarting picks up where it stopped."
+ },
+ {
+ "id": "fail.traversal",
+ "section": "Notion β€” failures",
+ "label": "Could not read everything",
+ "text": "The import could not read all of your Notion, so it stopped rather than finish with pages missing. Nothing was lost.",
+ "context": "Shown when part of the workspace could not be read. memoria deliberately stops instead of finishing, because an import that quietly ends with pages missing would look identical to a successful one β€” and someone would then search for something that was never brought in and conclude it does not exist.",
+ "notes": "The 'stopped rather than finish with pages missing' idea is the whole reason this state exists"
+ },
+ {
+ "id": "fail.network",
+ "section": "Notion β€” failures",
+ "label": "Could not reach Notion",
+ "text": "We could not reach Notion. Nothing was lost β€” start the import again.",
+ "context": "Shown when Notion could not be contacted at all."
+ },
+ {
+ "id": "fail.interrupted",
+ "section": "Notion β€” failures",
+ "label": "Stopped part-way",
+ "text": "The import stopped before it finished. Start it again to carry on.",
+ "context": "A shorter interrupted message, used in the failure list rather than as the main status line."
+ },
+ {
+ "id": "fail.default",
+ "section": "Notion β€” failures",
+ "label": "Unknown failure",
+ "text": "The import did not finish.",
+ "context": "Fallback when the reason for a failure is not one we have specific words for. Deliberately says only what we actually know."
+ },
+
+ {
+ "id": "flash.saved",
+ "section": "Notion β€” messages",
+ "label": "Token saved",
+ "text": "Connected. Start an import when you are ready.",
+ "context": "A green message at the top of the page after a token is saved. It also makes clear that saving did not start an import β€” that is a separate, deliberate act."
+ },
+ {
+ "id": "flash.token",
+ "section": "Notion β€” messages",
+ "label": "Empty token",
+ "text": "That did not look like a Notion integration token.",
+ "context": "Shown when the token field was submitted empty."
+ },
+ {
+ "id": "flash.verify",
+ "section": "Notion β€” messages",
+ "label": "Token rejected on save",
+ "text": "Notion would not accept that token. Check it is an internal integration token and that it has been shared with at least one page.",
+ "context": "Shown when a pasted token was checked against Notion and refused. The two things named are the two mistakes people actually make: using the wrong kind of token, or making one and never sharing any page with it."
+ },
+ {
+ "id": "flash.inflight",
+ "section": "Notion β€” messages",
+ "label": "Import already running",
+ "text": "An import is already running.",
+ "context": "Shown when someone presses Import while one is in progress. Nothing bad happened; nothing was started."
+ },
+ {
+ "id": "flash.save",
+ "section": "Notion β€” messages",
+ "label": "Could not save",
+ "text": "We could not save that.",
+ "context": "Shown when storing the token failed for a reason on our side."
+ },
+ {
+ "id": "flash.readsettings",
+ "section": "Notion β€” messages",
+ "label": "Could not read settings",
+ "text": "We could not read the connector settings.",
+ "context": "Shown when the page loaded but the stored connector could not be read from the database."
+ },
+ {
+ "id": "flash.default",
+ "section": "Notion β€” messages",
+ "label": "Fallback message",
+ "text": "That did not work.",
+ "context": "Last-resort message when something failed and we have nothing more specific to say."
+ },
+
+ {
+ "id": "notion.replace.heading",
+ "section": "Notion β€” replace token",
+ "label": "Section heading",
+ "text": "Replace the token",
+ "context": "Heading of the section for rotating to a new Notion token without disconnecting."
+ },
+ {
+ "id": "notion.replace.help",
+ "section": "Notion β€” replace token",
+ "label": "Explanation",
+ "text": "Rotating a token replaces the stored one. The old token is not kept. Nothing already imported is affected.",
+ "context": "Under the Replace heading. Answers the two questions someone rotating a credential has: what happens to the old one, and does this disturb anything already brought in."
+ },
+ {
+ "id": "notion.replace.label",
+ "section": "Notion β€” replace token",
+ "label": "Field label",
+ "text": "New integration token",
+ "context": "Label on the field for the replacement token. The stored one is never shown here."
+ },
+ {
+ "id": "notion.replace.button",
+ "section": "Notion β€” replace token",
+ "label": "Submit button",
+ "text": "Replace",
+ "context": "The button that swaps in the new token."
+ },
+
+ {
+ "id": "notion.disconnect.heading",
+ "section": "Notion β€” disconnect",
+ "label": "Section heading",
+ "text": "Disconnect",
+ "context": "Heading of the last section on the connected page."
+ },
+ {
+ "id": "notion.disconnect.body",
+ "section": "Notion β€” disconnect",
+ "label": "What disconnecting does",
+ "text": "This removes the token and stops future imports. It deletes nothing memoria has already imported β€” those pages stay here until someone deletes them in memoria.",
+ "context": "Sits above the Disconnect button, so it is read before the button is pressed rather than after. People reasonably expect disconnecting to remove the content too; it does not, and that is deliberate. Getting this wrong means someone disconnects believing they have withdrawn the content.",
+ "notes": "The 'deletes nothing' half is bold on the page. This is the same promise as the warning on the setup page, at the moment it matters most."
+ },
+ {
+ "id": "notion.disconnect.button",
+ "section": "Notion β€” disconnect",
+ "label": "Button",
+ "text": "Disconnect Notion",
+ "context": "The button itself, styled as a destructive action."
+ }
+]