Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1457a8c
Give the markup language a serializer, escapes, and references
amyjko Aug 22, 2026
bf6631f
Add the editor's DOM layer: render, read back, and locate the caret
amyjko Aug 22, 2026
2db7b56
Add the editor's editing operations as transforms, not DOM surgery
amyjko Aug 22, 2026
6414d7e
Make the markup editor a rich text editor
amyjko Aug 22, 2026
24f2189
Stop promising two shortcuts that were never implemented
amyjko Aug 22, 2026
6d794dd
Keep references when content is copied across paragraphs
amyjko Aug 22, 2026
eb84feb
Leave copy and cut to the browser everywhere
amyjko Aug 22, 2026
80e6d54
Make enter replace a selection, and cover the boundaries
amyjko Aug 22, 2026
792fd72
Make the toolbar reflect the formatting the caret is sitting in
amyjko Aug 22, 2026
4db7348
Add the picker for inserting and editing references
amyjko Aug 22, 2026
f4c3750
Let bold and italic be chosen before there is anything to type into
amyjko Aug 22, 2026
0b3a6c2
Test the editor through an input method, and against axe
amyjko Aug 22, 2026
56a85df
Ask the audit the question that actually matters
amyjko Aug 22, 2026
3c5285d
Print the audit summary where it can be seen
amyjko Aug 22, 2026
d6c3630
Point the connection docs at the pooler, and say so when it matters
amyjko Aug 22, 2026
d54b065
Let the audit carry a certificate authority
amyjko Aug 22, 2026
0967f2f
Name the mistake when a pooler username lands in the database's place
amyjko Aug 22, 2026
77b0411
Stop the serializer taking spaces people typed
amyjko Aug 22, 2026
565c882
Record what the audit found against production
amyjko Aug 22, 2026
6569837
Act on the first round of testing by hand
amyjko Aug 22, 2026
ba89812
Even the toolbar up, cut the footer, and slide it in
amyjko Aug 22, 2026
d3cb4d6
Slide the toolbar in only, not out
amyjko Aug 22, 2026
5be650b
Make the toolbar leave the way it arrived
amyjko Aug 22, 2026
0937106
Stop the editor losing half its width on the way out
amyjko Aug 22, 2026
884de66
Format references test
amyjko Aug 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"test": "npm run test:unit && npm run test:e2e",
"test": "npm run test:unit && npm run test:e2e && npm run test:integration",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"test:integration": "playwright test",
Expand All @@ -15,6 +15,7 @@
"test:e2e": "vitest run --config vitest.e2e.config.ts",
"csv": "node csv2org.js",
"snapshot": "node restore/snapshot.js",
"audit:markup": "vitest run restore/markup-audit.test.ts",
"restore": "node restore/restore.js",
"start": "cp .env.local .env; supabase start -x storage-api,imgproxy,logflare,vector,edge-runtime",
"stop": "cp .env.prod .env; supabase stop",
Expand All @@ -27,11 +28,13 @@
"node": ">=22"
},
"devDependencies": {
"@axe-core/playwright": "^4.13.0",
"@playwright/test": "^1",
"@supabase/supabase-js": "^2",
"@sveltejs/kit": "^2",
"@sveltejs/vite-plugin-svelte": "^7",
"@types/uuid": "^10",
"happy-dom": "^20.11.6",
"pg": "^8",
"prettier": "^3",
"prettier-plugin-svelte": "^4",
Expand Down
37 changes: 32 additions & 5 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,39 @@
import type { PlaywrightTestConfig } from '@playwright/test';

/**
* Two servers, because the two kinds of test need different things.
*
* The application is checked against a production build, which is the only way to catch what only
* breaks once it is built. The editor is checked against the dev server, which serves the source
* modules, so that the editor can be mounted on a bare element and driven directly rather than
* reached through a signed in page.
*/
const config: PlaywrightTestConfig = {
webServer: {
command: 'npm run build && npm run preview',
port: 4173
},
webServer: [
{
command: 'npm run build && npm run preview',
port: 4173,
reuseExistingServer: !process.env.CI
},
{
command: 'npm run dev -- --port 5173 --strictPort',
port: 5173,
reuseExistingServer: !process.env.CI
}
],
testDir: 'tests',
testMatch: /(.+\.)?(test|spec)\.[jt]s/
projects: [
{
name: 'app',
testMatch: 'test.ts',
use: { baseURL: 'http://localhost:4173' }
},
{
name: 'editor',
testMatch: 'editor.test.ts',
use: { baseURL: 'http://localhost:5173' }
}
]
};

export default config;
25 changes: 22 additions & 3 deletions restore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Take a snapshot of production **before** you change anything. It is your only re
pre-restore state, and it makes the whole operation auditable.

```sh
export PROD_DB_URL='postgresql://postgres:...@db.xxxx.supabase.co:5432/postgres'
export PROD_DB_URL='postgresql://postgres.xxxx:...@aws-0-REGION.pooler.supabase.com:5432/postgres'
npm run snapshot -- --source-env PROD_DB_URL
```

Expand All @@ -50,10 +50,29 @@ No downtime is needed and nothing is locked for long — the tool only reads her
Then write down what was lost, roughly when, and who reported it. You will need it in step 4,
because the diff cannot tell an accidental deletion from a deliberate one.

Get the connection string from **Dashboard → Connect**. Use the **direct connection** or the
**session pooler**. Not the transaction pooler on port 6543 — it does not hold session state across
Get the connection string from **Dashboard → Connect**, and take the **session pooler**.

Not the **direct connection**: `db.<ref>.supabase.co` resolves to IPv6 only unless the project has
the IPv4 add-on, so on a network without IPv6 it fails at name resolution with `ENOTFOUND` before
anything is even attempted. Check with `dig +short A db.<ref>.supabase.co` — no answer means no.

Not the **transaction pooler** on port 6543 either: it does not hold session state across
statements, and the tool refuses it.

The session pooler's username is `postgres.<project-ref>`, not `postgres`. Getting that wrong
fails as a password error, which sends you looking in the wrong place.

Supabase signs its own certificates — the pooler presents one from `Supabase Intermediate 2021 CA`,
which no public trust store carries — so every connection outside the local stack also needs
`--ca`. Download it from **Dashboard → Settings → Database**:

```sh
npm run snapshot -- --source-env PROD_DB_URL --ca ~/Downloads/prod-ca-2021.crt
```

Do not reach for a way to skip verification instead. A connection that cannot prove what it is
talking to is not one to send a production password down.

## 2. Get the backup

**Dashboard → Database → Backups**, pick the day, **Download**. The timestamps are UTC.
Expand Down
28 changes: 28 additions & 0 deletions restore/lib/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ export async function connect(url, { ca, label } = {}) {
);
}

/*
* The session pooler's username is postgres.<project-ref>, which is easy to paste into the
* database's place at the end of the string instead. Postgres then reports that a database of
* that name does not exist, and the search goes looking for a missing database rather than for
* a username in the wrong half of the connection string.
*/
const database = parsed.pathname.replace(/^\//, '');
if (/^postgres\.\w/.test(database)) {
throw new Error(
`${label ?? 'connection'} names a database of "${database}", which looks like the session ` +
`pooler's username in the database's place. The username goes before the @ and the ` +
`database after the host:\n` +
` postgresql://${database}:PASSWORD@${parsed.hostname}:${parsed.port || '5432'}/postgres`
);
}

const local = isLocal(parsed.hostname);
/** @type {import('pg').ClientConfig} */
const config = { connectionString: url };
Expand All @@ -88,6 +104,18 @@ export async function connect(url, { ca, label } = {}) {
`pass it with --ca <path>. Do not disable certificate verification.`
);
}
// A direct connection host resolves to IPv6 only unless the project has the IPv4 add-on, so
// on a network without IPv6 it fails here rather than anywhere informative. Saying so is
// worth more than the raw resolver error, since this is read during an emergency.
if (/ENOTFOUND|EAI_AGAIN/i.test(message) && /^db\./.test(parsed.hostname)) {
throw new Error(
`${label ?? 'connection'} (${describe(url)}): ${message}\n` +
`${parsed.hostname} is a direct connection host, which resolves to IPv6 only unless ` +
`the project has the IPv4 add-on. Use the session pooler instead: Dashboard -> ` +
`Connect -> Session pooler, on port 5432. Note its username is postgres.<project-ref> ` +
`rather than postgres.`
);
}
throw new Error(`${label ?? 'connection'} (${describe(url)}): ${message}`);
}
return client;
Expand Down
Loading
Loading