rename: the server is Inkwell — package, env vars, image, compose, export marker
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 15s
CI & Build / integration (push) Successful in 45s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 4m17s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 7m33s
Desktop (Tauri) / Update manifest (push) Successful in 7s
Android / Kotlin + Rust (APK) (push) Successful in 11m25s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 15s
CI & Build / integration (push) Successful in 45s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 4m17s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 7m33s
Desktop (Tauri) / Update manifest (push) Successful in 7s
Android / Kotlin + Rust (APK) (push) Successful in 11m25s
Step 2 of milestone 481. The operator chose a full rename (Scribe note 5071), so this goes past the display strings into the identities: - src/thoughtsync → src/inkwell; every import, the Dockerfile and both compose commands, alembic env, pyproject - THOUGHTSYNC_* → INKWELL_* (database URL, secret key, log level, tag/port/bind) - container data dir /var/thoughtsync → /var/inkwell - image git.fabledsword.com/bvandeusen/inkwell; Postgres user/db default inkwell; CI's integration service follows - the files the image serves are inkwell.*. fetch-clients.sh still fetches the thoughtsync-named release assets, because the lanes that publish them are renamed in steps 3 and 4 - exports are written with app "inkwell" Two deliberate exceptions, both because data rides on them: - compose volumes are now named explicitly and overridable (INKWELL_DB_VOLUME, INKWELL_DATA_VOLUME), so a deployment installed as ThoughtSync points at the volumes and DB identity it already has. .env.example says exactly what to set - import still accepts app "thoughtsync", because exports written before the rename are backups. Tested both ways Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+24
-5
@@ -1,4 +1,4 @@
|
|||||||
# ThoughtSync production settings. Copy to `.env` and edit:
|
# Inkwell production settings. Copy to `.env` and edit:
|
||||||
#
|
#
|
||||||
# cp .env.example .env
|
# cp .env.example .env
|
||||||
#
|
#
|
||||||
@@ -29,15 +29,15 @@ POSTGRES_PASSWORD=
|
|||||||
#
|
#
|
||||||
# NOTE: `main` can sit well behind `dev`. If a feature you expect is missing,
|
# NOTE: `main` can sit well behind `dev`. If a feature you expect is missing,
|
||||||
# check which branch it actually landed on before assuming a bug.
|
# check which branch it actually landed on before assuming a bug.
|
||||||
#THOUGHTSYNC_TAG=latest
|
#INKWELL_TAG=latest
|
||||||
|
|
||||||
# The host port the app is published on.
|
# The host port the app is published on.
|
||||||
#THOUGHTSYNC_PORT=5000
|
#INKWELL_PORT=5000
|
||||||
|
|
||||||
# Which interface to bind. The default (all interfaces) is what lets desktop
|
# Which interface to bind. The default (all interfaces) is what lets desktop
|
||||||
# clients on your network reach the server. Behind a reverse proxy, set this to
|
# clients on your network reach the server. Behind a reverse proxy, set this to
|
||||||
# 127.0.0.1 so only the proxy can talk to it.
|
# 127.0.0.1 so only the proxy can talk to it.
|
||||||
#THOUGHTSYNC_BIND=0.0.0.0
|
#INKWELL_BIND=0.0.0.0
|
||||||
|
|
||||||
# NOTE: how many proxies sit in front of this app is a SETTING, not an env var —
|
# NOTE: how many proxies sit in front of this app is a SETTING, not an env var —
|
||||||
# Settings → Security → "Trusted proxy hops" in the admin UI. It defaults to 1 (one
|
# Settings → Security → "Trusted proxy hops" in the admin UI. It defaults to 1 (one
|
||||||
@@ -47,12 +47,31 @@ POSTGRES_PASSWORD=
|
|||||||
# How much the app says. Credential events (sign-ins, failures, throttles, new
|
# How much the app says. Credential events (sign-ins, failures, throttles, new
|
||||||
# accounts, device tokens issued) are logged at INFO and read with
|
# accounts, device tokens issued) are logged at INFO and read with
|
||||||
# `docker compose logs app`.
|
# `docker compose logs app`.
|
||||||
#THOUGHTSYNC_LOG_LEVEL=INFO
|
#INKWELL_LOG_LEVEL=INFO
|
||||||
|
|
||||||
# Database identity. Changing these AFTER the first start does not rename anything
|
# Database identity. Changing these AFTER the first start does not rename anything
|
||||||
# that already exists — the volume keeps whatever the first run created.
|
# that already exists — the volume keeps whatever the first run created.
|
||||||
|
#POSTGRES_USER=inkwell
|
||||||
|
#POSTGRES_DB=inkwell
|
||||||
|
|
||||||
|
# --- Upgrading from ThoughtSync ---------------------------------------------
|
||||||
|
#
|
||||||
|
# Inkwell was called ThoughtSync, and a deployment installed under that name has
|
||||||
|
# its data in volumes and a database named for it. Point at them instead of
|
||||||
|
# renaming anything. Leaving these unset on such a deployment starts Inkwell
|
||||||
|
# against EMPTY volumes; the old data is untouched, but you would not see it.
|
||||||
|
#
|
||||||
|
# Use the full names `docker volume ls` prints — compose prefixes them with the
|
||||||
|
# project name, so they usually look like `thoughtsync_thoughtsync-db`:
|
||||||
|
#INKWELL_DB_VOLUME=thoughtsync_thoughtsync-db
|
||||||
|
#INKWELL_DATA_VOLUME=thoughtsync_thoughtsync-data
|
||||||
|
#
|
||||||
|
# And the database identity the first start created, which a volume keeps:
|
||||||
#POSTGRES_USER=thoughtsync
|
#POSTGRES_USER=thoughtsync
|
||||||
#POSTGRES_DB=thoughtsync
|
#POSTGRES_DB=thoughtsync
|
||||||
|
#
|
||||||
|
# Rename any THOUGHTSYNC_* lines already in your .env to INKWELL_* — the old
|
||||||
|
# names are no longer read.
|
||||||
|
|
||||||
# --- a note on HTTPS --------------------------------------------------------
|
# --- a note on HTTPS --------------------------------------------------------
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ permissions:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: git.fabledsword.com
|
REGISTRY: git.fabledsword.com
|
||||||
IMAGE: git.fabledsword.com/bvandeusen/thoughtsync
|
IMAGE: git.fabledsword.com/bvandeusen/inkwell
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Should this push build an image now, or is the Android lane about to publish a
|
# Should this push build an image now, or is the Android lane about to publish a
|
||||||
@@ -215,11 +215,11 @@ jobs:
|
|||||||
# Postgres it will actually meet.
|
# Postgres it will actually meet.
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
env:
|
env:
|
||||||
POSTGRES_USER: thoughtsync
|
POSTGRES_USER: inkwell
|
||||||
POSTGRES_PASSWORD: ci_integration
|
POSTGRES_PASSWORD: ci_integration
|
||||||
POSTGRES_DB: thoughtsync_test
|
POSTGRES_DB: inkwell_test
|
||||||
options: >-
|
options: >-
|
||||||
--health-cmd "pg_isready -U thoughtsync"
|
--health-cmd "pg_isready -U inkwell"
|
||||||
--health-interval 10s
|
--health-interval 10s
|
||||||
--health-timeout 5s
|
--health-timeout 5s
|
||||||
--health-retries 10
|
--health-retries 10
|
||||||
@@ -243,7 +243,7 @@ jobs:
|
|||||||
test -n "$PG"
|
test -n "$PG"
|
||||||
PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG")
|
PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG")
|
||||||
test -n "$PG_IP"
|
test -n "$PG_IP"
|
||||||
export THOUGHTSYNC_DATABASE_URL="postgresql+asyncpg://thoughtsync:ci_integration@${PG_IP}:5432/thoughtsync_test"
|
export INKWELL_DATABASE_URL="postgresql+asyncpg://inkwell:ci_integration@${PG_IP}:5432/inkwell_test"
|
||||||
# Wait for Postgres to accept connections. `run:` is busybox sh (rule 81) —
|
# Wait for Postgres to accept connections. `run:` is busybox sh (rule 81) —
|
||||||
# no bash /dev/tcp — so use the Python that is always present here.
|
# no bash /dev/tcp — so use the Python that is always present here.
|
||||||
/opt/venv/bin/python - "$PG_IP" <<'PY'
|
/opt/venv/bin/python - "$PG_IP" <<'PY'
|
||||||
|
|||||||
+3
-3
@@ -20,7 +20,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
|
|||||||
# Bake the built SPA into the package's static dir (served by app.py). PYTHONPATH
|
# Bake the built SPA into the package's static dir (served by app.py). PYTHONPATH
|
||||||
# points at /app/src so the runtime imports this source tree (with static/ present),
|
# points at /app/src so the runtime imports this source tree (with static/ present),
|
||||||
# not the pip-installed copy.
|
# not the pip-installed copy.
|
||||||
COPY --from=build-frontend /build/dist/ src/thoughtsync/static/
|
COPY --from=build-frontend /build/dist/ src/inkwell/static/
|
||||||
COPY alembic.ini .
|
COPY alembic.ini .
|
||||||
COPY alembic/ alembic/
|
COPY alembic/ alembic/
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ COPY alembic/ alembic/
|
|||||||
# that step never ran. An image with no clients — or with some and not others — is
|
# that step never ran. An image with no clients — or with some and not others — is
|
||||||
# a supported state: the server advertises what it has and the web UI hides the
|
# a supported state: the server advertises what it has and the web UI hides the
|
||||||
# rest (client_dist.py).
|
# rest (client_dist.py).
|
||||||
COPY client/ src/thoughtsync/client/
|
COPY client/ src/inkwell/client/
|
||||||
|
|
||||||
ENV PYTHONPATH=/app/src
|
ENV PYTHONPATH=/app/src
|
||||||
|
|
||||||
@@ -52,4 +52,4 @@ EXPOSE 5000
|
|||||||
# Wait for the database, run migrations, then serve. The DB wait keeps a briefly
|
# Wait for the database, run migrations, then serve. The DB wait keeps a briefly
|
||||||
# slow/unready database from crash-looping the container. Family convention
|
# slow/unready database from crash-looping the container. Family convention
|
||||||
# (rule 82): schema is built by real migrations, never metadata.create_all.
|
# (rule 82): schema is built by real migrations, never metadata.create_all.
|
||||||
CMD ["sh", "-c", "python -m thoughtsync.dbwait && alembic upgrade head && hypercorn 'thoughtsync.app:create_app()' --bind 0.0.0.0:5000 --keep-alive 600"]
|
CMD ["sh", "-c", "python -m inkwell.dbwait && alembic upgrade head && hypercorn 'inkwell.app:create_app()' --bind 0.0.0.0:5000 --keep-alive 600"]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ under a second, designed to grow into a lightweight second brain (labels, search
|
|||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
src/thoughtsync/ Quart app (app factory, auth, models, ACL, config, db)
|
src/inkwell/ Quart app (app factory, auth, models, ACL, config, db)
|
||||||
alembic/ async migrations (schema built via `alembic upgrade head`)
|
alembic/ async migrations (schema built via `alembic upgrade head`)
|
||||||
tests/ DB-free unit tests (pytest)
|
tests/ DB-free unit tests (pytest)
|
||||||
frontend/ Vue 3 + Vite + TypeScript + Tailwind SPA
|
frontend/ Vue 3 + Vite + TypeScript + Tailwind SPA
|
||||||
@@ -28,7 +28,7 @@ Backend (needs a Postgres reachable at `THOUGHTSYNC_DATABASE_URL`):
|
|||||||
```sh
|
```sh
|
||||||
pip install -e ".[dev]"
|
pip install -e ".[dev]"
|
||||||
alembic upgrade head
|
alembic upgrade head
|
||||||
hypercorn 'thoughtsync.app:create_app()' --bind 0.0.0.0:5000
|
hypercorn 'inkwell.app:create_app()' --bind 0.0.0.0:5000
|
||||||
```
|
```
|
||||||
|
|
||||||
Frontend (proxies `/api` to `:5000`):
|
Frontend (proxies `/api` to `:5000`):
|
||||||
|
|||||||
+2
-2
@@ -5,8 +5,8 @@ script_location = %(here)s/alembic
|
|||||||
prepend_sys_path = . src
|
prepend_sys_path = . src
|
||||||
path_separator = os
|
path_separator = os
|
||||||
|
|
||||||
# Local-dev default; overridden at runtime by THOUGHTSYNC_DATABASE_URL (see env.py).
|
# Local-dev default; overridden at runtime by INKWELL_DATABASE_URL (see env.py).
|
||||||
sqlalchemy.url = postgresql+asyncpg://thoughtsync:thoughtsync@localhost:5432/thoughtsync
|
sqlalchemy.url = postgresql+asyncpg://inkwell:inkwell@localhost:5432/inkwell
|
||||||
|
|
||||||
|
|
||||||
[loggers]
|
[loggers]
|
||||||
|
|||||||
+3
-3
@@ -8,8 +8,8 @@ from sqlalchemy.ext.asyncio import async_engine_from_config
|
|||||||
|
|
||||||
from alembic import context
|
from alembic import context
|
||||||
|
|
||||||
from thoughtsync.models import Base
|
from inkwell.models import Base
|
||||||
import thoughtsync.models.all # noqa: F401 — registers every model on Base.metadata
|
import inkwell.models.all # noqa: F401 — registers every model on Base.metadata
|
||||||
|
|
||||||
config = context.config
|
config = context.config
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ if config.config_file_name is not None:
|
|||||||
|
|
||||||
config.set_main_option(
|
config.set_main_option(
|
||||||
"sqlalchemy.url",
|
"sqlalchemy.url",
|
||||||
os.environ.get("THOUGHTSYNC_DATABASE_URL", config.get_main_option("sqlalchemy.url")),
|
os.environ.get("INKWELL_DATABASE_URL", config.get_main_option("sqlalchemy.url")),
|
||||||
)
|
)
|
||||||
|
|
||||||
target_metadata = Base.metadata
|
target_metadata = Base.metadata
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Recurring-reminder math: where a reminder goes when it is marked done.
|
//! Recurring-reminder math: where a reminder goes when it is marked done.
|
||||||
//!
|
//!
|
||||||
//! A deliberate port of the server's `src/thoughtsync/notes/recurrence.py`, kept
|
//! A deliberate port of the server's `src/inkwell/notes/recurrence.py`, kept
|
||||||
//! behaviourally identical rather than merely similar. The same note can be
|
//! behaviourally identical rather than merely similar. The same note can be
|
||||||
//! completed from the web (server code) or from the desktop and Android (this
|
//! completed from the web (server code) or from the desktop and Android (this
|
||||||
//! code), and the two must land on the same instant — otherwise completing a
|
//! code), and the two must land on the same instant — otherwise completing a
|
||||||
|
|||||||
+11
-11
@@ -14,15 +14,15 @@ services:
|
|||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: thoughtsync
|
POSTGRES_USER: inkwell
|
||||||
POSTGRES_PASSWORD: thoughtsync
|
POSTGRES_PASSWORD: inkwell
|
||||||
POSTGRES_DB: thoughtsync
|
POSTGRES_DB: inkwell
|
||||||
volumes:
|
volumes:
|
||||||
- thoughtsync-dev-db:/var/lib/postgresql/data
|
- inkwell-dev-db:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U thoughtsync"]
|
test: ["CMD-SHELL", "pg_isready -U inkwell"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
@@ -34,22 +34,22 @@ services:
|
|||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
THOUGHTSYNC_DATABASE_URL: postgresql+asyncpg://thoughtsync:thoughtsync@db:5432/thoughtsync
|
INKWELL_DATABASE_URL: postgresql+asyncpg://inkwell:inkwell@db:5432/inkwell
|
||||||
PYTHONPATH: /app/src
|
PYTHONPATH: /app/src
|
||||||
volumes:
|
volumes:
|
||||||
- ./pyproject.toml:/app/pyproject.toml
|
- ./pyproject.toml:/app/pyproject.toml
|
||||||
- ./alembic.ini:/app/alembic.ini
|
- ./alembic.ini:/app/alembic.ini
|
||||||
- ./alembic:/app/alembic
|
- ./alembic:/app/alembic
|
||||||
- ./src:/app/src
|
- ./src:/app/src
|
||||||
- thoughtsync-dev-data:/var/thoughtsync
|
- inkwell-dev-data:/var/inkwell
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
# Install deps, wait for the DB, run migrations, then serve with live reload.
|
# Install deps, wait for the DB, run migrations, then serve with live reload.
|
||||||
command: >
|
command: >
|
||||||
sh -c "pip install --quiet -e . &&
|
sh -c "pip install --quiet -e . &&
|
||||||
python -m thoughtsync.dbwait &&
|
python -m inkwell.dbwait &&
|
||||||
alembic upgrade head &&
|
alembic upgrade head &&
|
||||||
hypercorn 'thoughtsync.app:create_app()' --bind 0.0.0.0:5000 --reload"
|
hypercorn 'inkwell.app:create_app()' --bind 0.0.0.0:5000 --reload"
|
||||||
|
|
||||||
web:
|
web:
|
||||||
image: node:22-alpine
|
image: node:22-alpine
|
||||||
@@ -67,5 +67,5 @@ services:
|
|||||||
command: sh -c "npm install && npm run dev -- --host 0.0.0.0"
|
command: sh -c "npm install && npm run dev -- --host 0.0.0.0"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
thoughtsync-dev-db:
|
inkwell-dev-db:
|
||||||
thoughtsync-dev-data:
|
inkwell-dev-data:
|
||||||
|
|||||||
+27
-21
@@ -1,4 +1,4 @@
|
|||||||
# ThoughtSync — PRODUCTION stack (app + Postgres).
|
# Inkwell — PRODUCTION stack (app + Postgres).
|
||||||
#
|
#
|
||||||
# This is the default compose file: `docker compose up -d` runs a real deployment
|
# This is the default compose file: `docker compose up -d` runs a real deployment
|
||||||
# from the published image. Development lives in docker-compose.dev.yml (hot-reload,
|
# from the published image. Development lives in docker-compose.dev.yml (hot-reload,
|
||||||
@@ -10,15 +10,15 @@
|
|||||||
# Per family rule 12 the agent does NOT start this — run it yourself.
|
# Per family rule 12 the agent does NOT start this — run it yourself.
|
||||||
#
|
#
|
||||||
# Upgrades: docker compose pull && docker compose up -d
|
# Upgrades: docker compose pull && docker compose up -d
|
||||||
# Rollback: set THOUGHTSYNC_TAG to a commit sha in .env, then the same two commands.
|
# Rollback: set INKWELL_TAG to a commit sha in .env, then the same two commands.
|
||||||
# Every push publishes an immutable :<sha> image for exactly this.
|
# Every push publishes an immutable :<sha> image for exactly this.
|
||||||
#
|
#
|
||||||
# Schema migrations run automatically at container start (see the Dockerfile CMD),
|
# Schema migrations run automatically at container start (see the Dockerfile CMD),
|
||||||
# so an upgrade is just a pull and a restart. Take a backup first anyway:
|
# so an upgrade is just a pull and a restart. Take a backup first anyway:
|
||||||
#
|
#
|
||||||
# docker compose exec -T db pg_dump -U thoughtsync thoughtsync > backup.sql
|
# docker compose exec -T db pg_dump -U inkwell inkwell > backup.sql
|
||||||
#
|
#
|
||||||
# Attachments are files, not rows — they live in the `thoughtsync-data` volume and
|
# Attachments are files, not rows — they live in the `inkwell-data` volume and
|
||||||
# a pg_dump does NOT contain them. Back up both or you'll restore notes whose images
|
# a pg_dump does NOT contain them. Back up both or you'll restore notes whose images
|
||||||
# are gone.
|
# are gone.
|
||||||
|
|
||||||
@@ -27,22 +27,21 @@ services:
|
|||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-thoughtsync}
|
POSTGRES_USER: ${POSTGRES_USER:-inkwell}
|
||||||
# No default on purpose. A production compose that ships a known password is
|
# No default on purpose. A production compose that ships a known password is
|
||||||
# how self-hosted databases end up in search engines; compose fails fast here
|
# how self-hosted databases end up in search engines; compose fails fast here
|
||||||
# instead, with the message below.
|
# instead, with the message below.
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env — see .env.example}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env — see .env.example}
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-thoughtsync}
|
POSTGRES_DB: ${POSTGRES_DB:-inkwell}
|
||||||
volumes:
|
volumes:
|
||||||
# Volume names kept from the previous compose file so an existing deployment
|
# Which volume this resolves to is set at the bottom of this file, and an
|
||||||
# upgrades in place. Renaming them would silently start against an empty
|
# existing deployment overrides it from .env rather than renaming anything.
|
||||||
# database while the old one sat there, orphaned and looking like data loss.
|
- inkwell-db:/var/lib/postgresql/data
|
||||||
- thoughtsync-db:/var/lib/postgresql/data
|
|
||||||
# Deliberately NOT published to the host. The app reaches Postgres over the
|
# Deliberately NOT published to the host. The app reaches Postgres over the
|
||||||
# compose network; exposing 5432 only widens the attack surface. If you need
|
# compose network; exposing 5432 only widens the attack surface. If you need
|
||||||
# psql, `docker compose exec db psql -U thoughtsync` gets you there without it.
|
# psql, `docker compose exec db psql -U inkwell` gets you there without it.
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-thoughtsync} -d ${POSTGRES_DB:-thoughtsync}"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-inkwell} -d ${POSTGRES_DB:-inkwell}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
@@ -56,9 +55,9 @@ services:
|
|||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
app:
|
app:
|
||||||
# :latest tracks `main`. Set THOUGHTSYNC_TAG=dev in .env to follow the
|
# :latest tracks `main`. Set INKWELL_TAG=dev in .env to follow the
|
||||||
# development line instead, or a commit sha to pin exactly.
|
# development line instead, or a commit sha to pin exactly.
|
||||||
image: git.fabledsword.com/bvandeusen/thoughtsync:${THOUGHTSYNC_TAG:-latest}
|
image: git.fabledsword.com/bvandeusen/inkwell:${INKWELL_TAG:-latest}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
@@ -66,11 +65,11 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
# The only required application setting. Everything else a person might want
|
# The only required application setting. Everything else a person might want
|
||||||
# to tune lives in the admin Settings UI, backed by the database (rule 25).
|
# to tune lives in the admin Settings UI, backed by the database (rule 25).
|
||||||
THOUGHTSYNC_DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-thoughtsync}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-thoughtsync}
|
INKWELL_DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-inkwell}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-inkwell}
|
||||||
volumes:
|
volumes:
|
||||||
# Uploaded attachments. /var/thoughtsync is fixed in the app (Config.DATA_DIR),
|
# Uploaded attachments. /var/inkwell is fixed in the app (Config.DATA_DIR),
|
||||||
# not configurable — mount it or lose every image on container recreation.
|
# not configurable — mount it or lose every image on container recreation.
|
||||||
- thoughtsync-data:/var/thoughtsync
|
- inkwell-data:/var/inkwell
|
||||||
# WHERE THE APP IS REACHABLE FROM. Three shapes, and the right answer is
|
# WHERE THE APP IS REACHABLE FROM. Three shapes, and the right answer is
|
||||||
# different for each — the default serves the first.
|
# different for each — the default serves the first.
|
||||||
#
|
#
|
||||||
@@ -86,14 +85,14 @@ services:
|
|||||||
# and publishing one is a second, unauthenticated way in that bypasses the
|
# and publishing one is a second, unauthenticated way in that bypasses the
|
||||||
# proxy — including whatever the proxy is doing about TLS and auth.
|
# proxy — including whatever the proxy is doing about TLS and auth.
|
||||||
#
|
#
|
||||||
# 3. Reverse proxy on the HOST (not in Docker). Set THOUGHTSYNC_BIND=127.0.0.1 in
|
# 3. Reverse proxy on the HOST (not in Docker). Set INKWELL_BIND=127.0.0.1 in
|
||||||
# .env, so the port exists but only the host itself can reach it.
|
# .env, so the port exists but only the host itself can reach it.
|
||||||
#
|
#
|
||||||
# If you are exposing this to the internet, you want 2 or 3. Leaving it at 1
|
# If you are exposing this to the internet, you want 2 or 3. Leaving it at 1
|
||||||
# means the app is reachable directly on port 5000, past everything your proxy
|
# means the app is reachable directly on port 5000, past everything your proxy
|
||||||
# does.
|
# does.
|
||||||
ports:
|
ports:
|
||||||
- "${THOUGHTSYNC_BIND:-0.0.0.0}:${THOUGHTSYNC_PORT:-5000}:5000"
|
- "${INKWELL_BIND:-0.0.0.0}:${INKWELL_PORT:-5000}:5000"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
# python rather than curl: the runtime image is python:3.12-slim and carries no
|
# python rather than curl: the runtime image is python:3.12-slim and carries no
|
||||||
# HTTP client binary. Hits the app's own /api/health.
|
# HTTP client binary. Hits the app's own /api/health.
|
||||||
@@ -113,5 +112,12 @@ services:
|
|||||||
logging: *logging
|
logging: *logging
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
thoughtsync-db:
|
# Named explicitly so the name is one a person can type, and overridable so a
|
||||||
thoughtsync-data:
|
# deployment that predates the rename to Inkwell keeps the volumes it already has
|
||||||
|
# (see "Upgrading from ThoughtSync" in .env.example). Pointing these at the wrong
|
||||||
|
# name does not fail: it starts against an EMPTY database and the old one sits
|
||||||
|
# there untouched, which looks exactly like data loss.
|
||||||
|
inkwell-db:
|
||||||
|
name: ${INKWELL_DB_VOLUME:-inkwell-db}
|
||||||
|
inkwell-data:
|
||||||
|
name: ${INKWELL_DATA_VOLUME:-inkwell-data}
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ an older release than the desktop app for months. So the wire protocol is
|
|||||||
versioned **separately from either program's release version**, and each side
|
versioned **separately from either program's release version**, and each side
|
||||||
declares two numbers: what it speaks, and the oldest counterpart it accepts.
|
declares two numbers: what it speaks, and the oldest counterpart it accepts.
|
||||||
|
|
||||||
| | server (`src/thoughtsync/sync.py`) | client (`desktop/src-tauri/src/sync/compat.rs`) |
|
| | server (`src/inkwell/sync.py`) | client (`desktop/src-tauri/src/sync/compat.rs`) |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| speaks | `SYNC_PROTOCOL_VERSION` | `CLIENT_PROTOCOL_VERSION` |
|
| speaks | `SYNC_PROTOCOL_VERSION` | `CLIENT_PROTOCOL_VERSION` |
|
||||||
| accepts down to | `MIN_CLIENT_PROTOCOL_VERSION` | `MIN_SERVER_PROTOCOL_VERSION` |
|
| accepts down to | `MIN_CLIENT_PROTOCOL_VERSION` | `MIN_SERVER_PROTOCOL_VERSION` |
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function parseInline(text: string): InlineToken[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A checklist item: the third implementation of one grammar, alongside
|
// A checklist item: the third implementation of one grammar, alongside
|
||||||
// core/src/local/derive.rs and src/thoughtsync/notes/checklist.py. A difference
|
// core/src/local/derive.rs and src/inkwell/notes/checklist.py. A difference
|
||||||
// between any two of them is a checklist that changes shape when it syncs (M304).
|
// between any two of them is a checklist that changes shape when it syncs (M304).
|
||||||
//
|
//
|
||||||
// `-` and `*` only, deliberately, even though the `ul` matcher below also takes `+`.
|
// `-` and `*` only, deliberately, even though the `ul` matcher below also takes `+`.
|
||||||
|
|||||||
+10
-10
@@ -37,7 +37,7 @@ case "$channel" in dev|stable) : ;; *)
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
SERVER="${GITHUB_SERVER_URL:-https://git.fabledsword.com}"
|
SERVER="${GITHUB_SERVER_URL:-https://git.fabledsword.com}"
|
||||||
REPO="${GITHUB_REPOSITORY:-bvandeusen/thoughtsync}"
|
REPO="${GITHUB_REPOSITORY:-bvandeusen/inkwell}"
|
||||||
# The channel's release tag, not its name — `dev` lives on `dev-rolling`
|
# The channel's release tag, not its name — `dev` lives on `dev-rolling`
|
||||||
# (packaging/channel-tag.sh, Scribe #2184).
|
# (packaging/channel-tag.sh, Scribe #2184).
|
||||||
tag="$(sh "$(dirname "$0")/channel-tag.sh" "$channel")"
|
tag="$(sh "$(dirname "$0")/channel-tag.sh" "$channel")"
|
||||||
@@ -88,15 +88,15 @@ echo "==> Collecting the $channel clients"
|
|||||||
# Its sidecar is published whole by the Android lane — an APK keeps its version in
|
# Its sidecar is published whole by the Android lane — an APK keeps its version in
|
||||||
# a binary AXML manifest, so the values are recorded where they were already known.
|
# a binary AXML manifest, so the values are recorded where they were already known.
|
||||||
# Copied verbatim rather than rebuilt here.
|
# Copied verbatim rather than rebuilt here.
|
||||||
if fetch "$BASE/thoughtsync.apk" "$dest/thoughtsync.apk" &&
|
if fetch "$BASE/thoughtsync.apk" "$dest/inkwell.apk" &&
|
||||||
fetch "$BASE/thoughtsync-android.json" "$dest/thoughtsync-android.json"; then
|
fetch "$BASE/thoughtsync-android.json" "$dest/inkwell-android.json"; then
|
||||||
echo " android $(field "$dest/thoughtsync-android.json" version_name)"
|
echo " android $(field "$dest/inkwell-android.json" version_name)"
|
||||||
else
|
else
|
||||||
# Both or neither. Half a pair is worse than none: the server would read a
|
# Both or neither. Half a pair is worse than none: the server would read a
|
||||||
# sidecar describing an APK that is not there, or an APK it cannot state a
|
# sidecar describing an APK that is not there, or an APK it cannot state a
|
||||||
# version for.
|
# version for.
|
||||||
echo "::warning::No Android client on the $channel channel — this image ships without one."
|
echo "::warning::No Android client on the $channel channel — this image ships without one."
|
||||||
rm -f "$dest/thoughtsync.apk" "$dest/thoughtsync-android.json"
|
rm -f "$dest/inkwell.apk" "$dest/inkwell-android.json"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- desktop -----------------------------------------------------------------
|
# --- desktop -----------------------------------------------------------------
|
||||||
@@ -132,10 +132,10 @@ bake_desktop() {
|
|||||||
#
|
#
|
||||||
# `<platform id>|<published name>|<name on disk>`
|
# `<platform id>|<published name>|<name on disk>`
|
||||||
for row in \
|
for row in \
|
||||||
"linux-deb|ThoughtSync_${key}_amd64.deb|thoughtsync.deb" \
|
"linux-deb|ThoughtSync_${key}_amd64.deb|inkwell.deb" \
|
||||||
"linux-pacman|thoughtsync-${key}-1-x86_64.pkg.tar.zst|thoughtsync.pkg.tar.zst" \
|
"linux-pacman|thoughtsync-${key}-1-x86_64.pkg.tar.zst|inkwell.pkg.tar.zst" \
|
||||||
"linux-appimage|ThoughtSync_${key}_amd64.AppImage|thoughtsync.AppImage" \
|
"linux-appimage|ThoughtSync_${key}_amd64.AppImage|inkwell.AppImage" \
|
||||||
"windows|ThoughtSync_${key}_x64-setup.exe|thoughtsync-setup.exe"
|
"windows|ThoughtSync_${key}_x64-setup.exe|inkwell-setup.exe"
|
||||||
do
|
do
|
||||||
id="${row%%|*}"; rest="${row#*|}"
|
id="${row%%|*}"; rest="${row#*|}"
|
||||||
remote="${rest%%|*}"; local_name="${rest#*|}"
|
remote="${rest%%|*}"; local_name="${rest#*|}"
|
||||||
@@ -158,7 +158,7 @@ bake_desktop() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sidecar "$dest/$local_name" "$dest/thoughtsync-$id.json" "$name" "$key"
|
sidecar "$dest/$local_name" "$dest/inkwell-$id.json" "$name" "$key"
|
||||||
echo " $id $(bytes "$dest/$local_name") bytes"
|
echo " $id $(bytes "$dest/$local_name") bytes"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ set -eu
|
|||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
SERVER="${GITHUB_SERVER_URL:-https://git.fabledsword.com}"
|
SERVER="${GITHUB_SERVER_URL:-https://git.fabledsword.com}"
|
||||||
REPO="${GITHUB_REPOSITORY:-bvandeusen/thoughtsync}"
|
REPO="${GITHUB_REPOSITORY:-bvandeusen/inkwell}"
|
||||||
|
|
||||||
artifact="${1:?usage: guard-forward.sh <desktop|android> <dev|stable>}"
|
artifact="${1:?usage: guard-forward.sh <desktop|android> <dev|stable>}"
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ requires = ["setuptools>=68.0"]
|
|||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "thoughtsync"
|
name = "inkwell"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
description = "Self-hosted personal thought-capture web app (FabledSword family)"
|
description = "Self-hosted personal thought-capture web app (FabledSword family)"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""ThoughtSync — self-hosted personal thought-capture web app (FabledSword family)."""
|
"""Inkwell — self-hosted personal thought-capture web app (FabledSword family)."""
|
||||||
|
|
||||||
# PACKAGING METADATA, and nothing else. Not the version any running server reports.
|
# PACKAGING METADATA, and nothing else. Not the version any running server reports.
|
||||||
#
|
#
|
||||||
@@ -32,7 +32,7 @@ from .sync import bp as sync_bp, protocol_advertisement
|
|||||||
# `force=False` (the default) so a host that has already configured logging keeps its
|
# `force=False` (the default) so a host that has already configured logging keeps its
|
||||||
# own setup; LOG_LEVEL lets an operator turn it up without a code change.
|
# own setup; LOG_LEVEL lets an operator turn it up without a code change.
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=os.environ.get("THOUGHTSYNC_LOG_LEVEL", "INFO").upper(),
|
level=os.environ.get("INKWELL_LOG_LEVEL", "INFO").upper(),
|
||||||
format="%(asctime)s %(levelname)s %(name)s %(message)s",
|
format="%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,8 +38,8 @@ wins — would mean replacing one client silently retracts the other four.
|
|||||||
|
|
||||||
Two files, and both must be present:
|
Two files, and both must be present:
|
||||||
|
|
||||||
- the artifact, at a FIXED name (`thoughtsync.deb`, not
|
- the artifact, at a FIXED name (`inkwell.deb`, not
|
||||||
`ThoughtSync_2026.08.30.0307_amd64.deb`)
|
`Inkwell_2026.08.30.0307_amd64.deb`)
|
||||||
- its sidecar, `{version_name, version_code, size, sha256}`
|
- its sidecar, `{version_name, version_code, size, sha256}`
|
||||||
|
|
||||||
**Fixed names, and the version only in the sidecar.** A version-stamped filename
|
**Fixed names, and the version only in the sidecar.** A version-stamped filename
|
||||||
@@ -53,7 +53,7 @@ an APK keeps it in a binary AXML manifest Python cannot parse without the Androi
|
|||||||
build tools, and a `.deb` or an AppImage would each need a different unpacker. CI
|
build tools, and a `.deb` or an AppImage would each need a different unpacker. CI
|
||||||
writes the sidecar where the real value is already known.
|
writes the sidecar where the real value is already known.
|
||||||
|
|
||||||
The AppImage needs a THIRD file, `thoughtsync.AppImage.sig`. That is the minisign
|
The AppImage needs a THIRD file, `inkwell.AppImage.sig`. That is the minisign
|
||||||
signature the desktop updater verifies before replacing the running binary, and a
|
signature the desktop updater verifies before replacing the running binary, and a
|
||||||
bundle that cannot be verified cannot be offered as an update — so a missing
|
bundle that cannot be verified cannot be offered as an update — so a missing
|
||||||
signature makes the AppImage absent rather than merely unsigned.
|
signature makes the AppImage absent rather than merely unsigned.
|
||||||
@@ -132,31 +132,31 @@ PLATFORMS: tuple[Platform, ...] = (
|
|||||||
# exact names, CI bakes them in under them, and clients in the field poll
|
# exact names, CI bakes them in under them, and clients in the field poll
|
||||||
# `/api/client/android`. Renaming them to match the pattern below would
|
# `/api/client/android`. Renaming them to match the pattern below would
|
||||||
# buy tidiness and strand every installed phone.
|
# buy tidiness and strand every installed phone.
|
||||||
artifact="thoughtsync.apk",
|
artifact="inkwell.apk",
|
||||||
sidecar="thoughtsync-android.json",
|
sidecar="inkwell-android.json",
|
||||||
mimetype="application/vnd.android.package-archive",
|
mimetype="application/vnd.android.package-archive",
|
||||||
),
|
),
|
||||||
Platform(
|
Platform(
|
||||||
id="linux-deb",
|
id="linux-deb",
|
||||||
label="Debian / Ubuntu",
|
label="Debian / Ubuntu",
|
||||||
artifact="thoughtsync.deb",
|
artifact="inkwell.deb",
|
||||||
sidecar="thoughtsync-linux-deb.json",
|
sidecar="inkwell-linux-deb.json",
|
||||||
mimetype="application/vnd.debian.binary-package",
|
mimetype="application/vnd.debian.binary-package",
|
||||||
code_is_int=False,
|
code_is_int=False,
|
||||||
),
|
),
|
||||||
Platform(
|
Platform(
|
||||||
id="linux-pacman",
|
id="linux-pacman",
|
||||||
label="Arch / CachyOS",
|
label="Arch / CachyOS",
|
||||||
artifact="thoughtsync.pkg.tar.zst",
|
artifact="inkwell.pkg.tar.zst",
|
||||||
sidecar="thoughtsync-linux-pacman.json",
|
sidecar="inkwell-linux-pacman.json",
|
||||||
mimetype="application/zstd",
|
mimetype="application/zstd",
|
||||||
code_is_int=False,
|
code_is_int=False,
|
||||||
),
|
),
|
||||||
Platform(
|
Platform(
|
||||||
id="linux-appimage",
|
id="linux-appimage",
|
||||||
label="Other Linux (AppImage)",
|
label="Other Linux (AppImage)",
|
||||||
artifact="thoughtsync.AppImage",
|
artifact="inkwell.AppImage",
|
||||||
sidecar="thoughtsync-linux-appimage.json",
|
sidecar="inkwell-linux-appimage.json",
|
||||||
# No registered type for an AppImage, and guessing one buys nothing: it is
|
# No registered type for an AppImage, and guessing one buys nothing: it is
|
||||||
# served as an attachment either way, and octet-stream is the answer that
|
# served as an attachment either way, and octet-stream is the answer that
|
||||||
# cannot be wrong.
|
# cannot be wrong.
|
||||||
@@ -167,8 +167,8 @@ PLATFORMS: tuple[Platform, ...] = (
|
|||||||
Platform(
|
Platform(
|
||||||
id="windows",
|
id="windows",
|
||||||
label="Windows",
|
label="Windows",
|
||||||
artifact="thoughtsync-setup.exe",
|
artifact="inkwell-setup.exe",
|
||||||
sidecar="thoughtsync-windows.json",
|
sidecar="inkwell-windows.json",
|
||||||
mimetype="application/vnd.microsoft.portable-executable",
|
mimetype="application/vnd.microsoft.portable-executable",
|
||||||
code_is_int=False,
|
code_is_int=False,
|
||||||
),
|
),
|
||||||
@@ -7,26 +7,26 @@ from pathlib import Path
|
|||||||
class Config:
|
class Config:
|
||||||
"""Bootstrap configuration.
|
"""Bootstrap configuration.
|
||||||
|
|
||||||
For a basic install, ``THOUGHTSYNC_DATABASE_URL`` is the ONLY required env var —
|
For a basic install, ``INKWELL_DATABASE_URL`` is the ONLY required env var —
|
||||||
every other tunable lives in the DB-backed Settings UI (rule 25). The only other
|
every other tunable lives in the DB-backed Settings UI (rule 25). The only other
|
||||||
env var is an optional "break-glass" item:
|
env var is an optional "break-glass" item:
|
||||||
|
|
||||||
- ``THOUGHTSYNC_SECRET_KEY`` — optional override for the cookie-signing secret.
|
- ``INKWELL_SECRET_KEY`` — optional override for the cookie-signing secret.
|
||||||
If unset, a key is generated and persisted in the DB (see
|
If unset, a key is generated and persisted in the DB (see
|
||||||
``thoughtsync.settings.load_or_create_secret_key``), so sessions survive
|
``inkwell.settings.load_or_create_secret_key``), so sessions survive
|
||||||
restarts with no volume required.
|
restarts with no volume required.
|
||||||
|
|
||||||
Uploaded media lives under ``DATA_DIR`` — a fixed, authoritative path
|
Uploaded media lives under ``DATA_DIR`` — a fixed, authoritative path
|
||||||
(``/var/thoughtsync``), intentionally NOT configurable (a mutable data path only
|
(``/var/inkwell``), intentionally NOT configurable (a mutable data path only
|
||||||
invites breakage). Mount a volume there if you want uploads to persist across
|
invites breakage). Mount a volume there if you want uploads to persist across
|
||||||
container recreation; a text-notes-only install never writes to it.
|
container recreation; a text-notes-only install never writes to it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DATA_DIR = "/var/thoughtsync"
|
DATA_DIR = "/var/inkwell"
|
||||||
|
|
||||||
DATABASE_URL = os.environ.get(
|
DATABASE_URL = os.environ.get(
|
||||||
"THOUGHTSYNC_DATABASE_URL",
|
"INKWELL_DATABASE_URL",
|
||||||
"postgresql+asyncpg://thoughtsync:thoughtsync@localhost:5432/thoughtsync",
|
"postgresql+asyncpg://inkwell:inkwell@localhost:5432/inkwell",
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -50,5 +50,5 @@ class Config:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def secret_key_env(cls) -> str | None:
|
def secret_key_env(cls) -> str | None:
|
||||||
"""Optional break-glass override for the cookie-signing secret."""
|
"""Optional break-glass override for the cookie-signing secret."""
|
||||||
return os.environ.get("THOUGHTSYNC_SECRET_KEY") or None
|
return os.environ.get("INKWELL_SECRET_KEY") or None
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ from . import Base
|
|||||||
|
|
||||||
class Group(Base):
|
class Group(Base):
|
||||||
"""A named set of users. Sharing a resource with a group grants it to every
|
"""A named set of users. Sharing a resource with a group grants it to every
|
||||||
member (see thoughtsync.acl.visible_to_user)."""
|
member (see inkwell.acl.visible_to_user)."""
|
||||||
|
|
||||||
__tablename__ = "groups"
|
__tablename__ = "groups"
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ from . import Base
|
|||||||
|
|
||||||
class Setting(Base):
|
class Setting(Base):
|
||||||
"""A single persisted key/value setting. Values are JSON-encoded text; the
|
"""A single persisted key/value setting. Values are JSON-encoded text; the
|
||||||
typed defaults + metadata live in the code registry (thoughtsync.settings), so
|
typed defaults + metadata live in the code registry (inkwell.settings), so
|
||||||
an empty table means "all defaults" (rule 26)."""
|
an empty table means "all defaults" (rule 26)."""
|
||||||
|
|
||||||
__tablename__ = "settings"
|
__tablename__ = "settings"
|
||||||
@@ -6,7 +6,7 @@ small text/query helpers (`helpers`), and export/import (`import_export`). The
|
|||||||
route handlers themselves stay here so blueprint registration is in one place, and
|
route handlers themselves stay here so blueprint registration is in one place, and
|
||||||
`bp` is defined in `_bp` so every module can import it without a cycle.
|
`bp` is defined in `_bp` so every module can import it without a cycle.
|
||||||
|
|
||||||
External callers import from `thoughtsync.notes` (see `__all__`); those names are
|
External callers import from `inkwell.notes` (see `__all__`); those names are
|
||||||
re-exported here so the package is a drop-in replacement for the old module."""
|
re-exported here so the package is a drop-in replacement for the old module."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -237,7 +237,7 @@ async def export_notes():
|
|||||||
).all()
|
).all()
|
||||||
|
|
||||||
payload: dict = {
|
payload: dict = {
|
||||||
"app": "thoughtsync",
|
"app": "inkwell",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"exported_at": datetime.now(timezone.utc).isoformat(),
|
"exported_at": datetime.now(timezone.utc).isoformat(),
|
||||||
"labels": [{"name": lb.name, "color": lb.color} for lb in all_labels],
|
"labels": [{"name": lb.name, "color": lb.color} for lb in all_labels],
|
||||||
@@ -278,7 +278,7 @@ async def export_notes():
|
|||||||
data,
|
data,
|
||||||
headers={
|
headers={
|
||||||
"Content-Type": "application/zip",
|
"Content-Type": "application/zip",
|
||||||
"Content-Disposition": f'attachment; filename="thoughtsync-export-{stamp}.zip"',
|
"Content-Disposition": f'attachment; filename="inkwell-export-{stamp}.zip"',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ THE GRAMMAR IS SHARED. Three implementations exist and they have to agree, becau
|
|||||||
difference between any two of them is a checklist that changes shape when it syncs:
|
difference between any two of them is a checklist that changes shape when it syncs:
|
||||||
|
|
||||||
core/src/local/derive.rs the native clients (desktop + Android)
|
core/src/local/derive.rs the native clients (desktop + Android)
|
||||||
src/thoughtsync/notes/checklist.py this file, the server
|
src/inkwell/notes/checklist.py this file, the server
|
||||||
frontend/src/notes/markdown.ts the browser
|
frontend/src/notes/markdown.ts the browser
|
||||||
|
|
||||||
optional indent, `-` or `*`, one-or-more spaces, `[ ]`/`[x]`/`[X]`,
|
optional indent, `-` or `*`, one-or-more spaces, `[ ]`/`[x]`/`[X]`,
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
"""Export/import helpers (the non-route logic). Export renders each note as Markdown;
|
"""Export/import helpers (the non-route logic). Export renders each note as Markdown;
|
||||||
import normalizes a ThoughtSync export OR a Google Keep Takeout zip into a common spec
|
import normalizes an Inkwell export OR a Google Keep Takeout zip into a common spec
|
||||||
and materializes notes — with a decompression-size budget so an import zip bomb can't
|
and materializes notes — with a decompression-size budget so an import zip bomb can't
|
||||||
exhaust memory/disk."""
|
exhaust memory/disk."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -58,7 +58,13 @@ def _note_markdown(note: Note, labels: list) -> str:
|
|||||||
return "\n".join(fm) + "\n"
|
return "\n".join(fm) + "\n"
|
||||||
|
|
||||||
|
|
||||||
# --- Import: ThoughtSync's own export (round-trip) OR a Google Keep Takeout zip ---
|
# --- Import: Inkwell's own export (round-trip) OR a Google Keep Takeout zip ---
|
||||||
|
|
||||||
|
# The `app` value an export's notes.json carries. "thoughtsync" is what every export
|
||||||
|
# written before the rename to Inkwell says (Scribe note 5071), and those zips are
|
||||||
|
# people's backups, so they keep importing. That is the one reason the old name is
|
||||||
|
# still read here; nothing writes it any more.
|
||||||
|
NATIVE_APP_MARKERS = ("inkwell", "thoughtsync")
|
||||||
|
|
||||||
# Reverse of ALLOWED_IMAGE_MIMES, for inferring an attachment's mime from its
|
# Reverse of ALLOWED_IMAGE_MIMES, for inferring an attachment's mime from its
|
||||||
# filename when the source didn't record one (Keep usually does; be defensive).
|
# filename when the source didn't record one (Keep usually does; be defensive).
|
||||||
@@ -75,7 +81,7 @@ def _usec_to_dt(usec: object) -> datetime | None:
|
|||||||
|
|
||||||
|
|
||||||
def _native_spec(n: dict) -> dict:
|
def _native_spec(n: dict) -> dict:
|
||||||
"""Normalize one note from a ThoughtSync export's notes.json into the common
|
"""Normalize one note from an Inkwell export's notes.json into the common
|
||||||
import spec consumed by _create_imported_note."""
|
import spec consumed by _create_imported_note."""
|
||||||
return {
|
return {
|
||||||
"title": n.get("title"),
|
"title": n.get("title"),
|
||||||
@@ -186,9 +192,10 @@ class _ImportBudget:
|
|||||||
|
|
||||||
|
|
||||||
def _read_import_specs(zf: zipfile.ZipFile, budget: _ImportBudget) -> tuple[list[dict], str]:
|
def _read_import_specs(zf: zipfile.ZipFile, budget: _ImportBudget) -> tuple[list[dict], str]:
|
||||||
"""Detect the archive format and return (specs, source). A ThoughtSync export
|
"""Detect the archive format and return (specs, source). An Inkwell export
|
||||||
is recognized by its notes.json (app == thoughtsync); otherwise each Keep-shaped
|
is recognized by its notes.json (app in NATIVE_APP_MARKERS); otherwise each
|
||||||
<note>.json is imported. Returns ([], "") when nothing importable is found."""
|
Keep-shaped <note>.json is imported. Returns ([], "") when nothing importable
|
||||||
|
is found."""
|
||||||
names = zf.namelist()
|
names = zf.namelist()
|
||||||
for name in names:
|
for name in names:
|
||||||
if posixpath.basename(name) == "notes.json":
|
if posixpath.basename(name) == "notes.json":
|
||||||
@@ -196,9 +203,9 @@ def _read_import_specs(zf: zipfile.ZipFile, budget: _ImportBudget) -> tuple[list
|
|||||||
doc = json.loads(budget.read(zf, name))
|
doc = json.loads(budget.read(zf, name))
|
||||||
except (ValueError, KeyError):
|
except (ValueError, KeyError):
|
||||||
continue
|
continue
|
||||||
if isinstance(doc, dict) and doc.get("app") == "thoughtsync":
|
if isinstance(doc, dict) and doc.get("app") in NATIVE_APP_MARKERS:
|
||||||
specs = [_native_spec(n) for n in (doc.get("notes") or []) if isinstance(n, dict)]
|
specs = [_native_spec(n) for n in (doc.get("notes") or []) if isinstance(n, dict)]
|
||||||
return specs, "thoughtsync"
|
return specs, "inkwell"
|
||||||
|
|
||||||
keep_specs: list[dict] = []
|
keep_specs: list[dict] = []
|
||||||
keep_keys = ("textContent", "listContent", "isPinned", "isArchived", "isTrashed", "userEditedTimestampUsec")
|
keep_keys = ("textContent", "listContent", "isPinned", "isArchived", "isTrashed", "userEditedTimestampUsec")
|
||||||
@@ -255,7 +262,7 @@ async def _create_imported_note(
|
|||||||
body = spec.get("body") or ""
|
body = spec.get("body") or ""
|
||||||
# An imported title becomes the note's FIRST BODY LINE.
|
# An imported title becomes the note's FIRST BODY LINE.
|
||||||
#
|
#
|
||||||
# ThoughtSync has no title field any more (M13 step 3), but the things people
|
# Inkwell has no title field any more (M13 step 3), but the things people
|
||||||
# import from do — Keep notes carry one, and so does any export taken before this.
|
# import from do — Keep notes carry one, and so does any export taken before this.
|
||||||
# Dropping it would silently lose text someone wrote; folding it into the body puts
|
# Dropping it would silently lose text someone wrote; folding it into the body puts
|
||||||
# it exactly where a name now lives, so the note comes in named the way it was.
|
# it exactly where a name now lives, so the note comes in named the way it was.
|
||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from thoughtsync.config import Config
|
from inkwell.config import Config
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _isolated_data_dir(tmp_path, monkeypatch):
|
def _isolated_data_dir(tmp_path, monkeypatch):
|
||||||
"""Keep any media writes on an isolated temp dir rather than the fixed
|
"""Keep any media writes on an isolated temp dir rather than the fixed
|
||||||
/var/thoughtsync. DB-free unit tests never actually hit it (create_app takes its
|
/var/inkwell. DB-free unit tests never actually hit it (create_app takes its
|
||||||
signing key from env-or-random, not a file), but this stays defensive."""
|
signing key from env-or-random, not a file), but this stays defensive."""
|
||||||
monkeypatch.setattr(Config, "DATA_DIR", str(tmp_path / "data"))
|
monkeypatch.setattr(Config, "DATA_DIR", str(tmp_path / "data"))
|
||||||
yield
|
yield
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from thoughtsync.acl import visible_to_user
|
from inkwell.acl import visible_to_user
|
||||||
from thoughtsync.models.user import User
|
from inkwell.models.user import User
|
||||||
|
|
||||||
|
|
||||||
def test_visible_to_user_builds_owner_or_shared_predicate():
|
def test_visible_to_user_builds_owner_or_shared_predicate():
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from thoughtsync.app import create_app
|
from inkwell.app import create_app
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from thoughtsync.app import create_app
|
from inkwell.app import create_app
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -61,7 +61,7 @@ async def test_no_version_in_the_environment_reports_unknown(monkeypatch):
|
|||||||
happens to hold, so bumping the packaging version cannot make this pass for the
|
happens to hold, so bumping the packaging version cannot make this pass for the
|
||||||
wrong reason.
|
wrong reason.
|
||||||
"""
|
"""
|
||||||
from thoughtsync import __version__
|
from inkwell import __version__
|
||||||
|
|
||||||
monkeypatch.delenv("APP_VERSION", raising=False)
|
monkeypatch.delenv("APP_VERSION", raising=False)
|
||||||
reported = await reported_version()
|
reported = await reported_version()
|
||||||
|
|||||||
+14
-10
@@ -3,9 +3,9 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from thoughtsync import client_dist
|
from inkwell import client_dist
|
||||||
from thoughtsync.app import create_app
|
from inkwell.app import create_app
|
||||||
from thoughtsync.client_dist import (
|
from inkwell.client_dist import (
|
||||||
APK_NAME,
|
APK_NAME,
|
||||||
BY_ID,
|
BY_ID,
|
||||||
MANIFEST_NAME,
|
MANIFEST_NAME,
|
||||||
@@ -15,7 +15,7 @@ from thoughtsync.client_dist import (
|
|||||||
release,
|
release,
|
||||||
releases,
|
releases,
|
||||||
)
|
)
|
||||||
from thoughtsync.config import Config
|
from inkwell.config import Config
|
||||||
|
|
||||||
# DB-free, like the rest of this suite — the test lane runs no Postgres. That is
|
# DB-free, like the rest of this suite — the test lane runs no Postgres. That is
|
||||||
# why the advertisement is asserted through `advertisement()` rather than through
|
# why the advertisement is asserted through `advertisement()` rather than through
|
||||||
@@ -61,7 +61,7 @@ def coded(platform_id: str, value: int):
|
|||||||
def _empty_baked_client(tmp_path, monkeypatch):
|
def _empty_baked_client(tmp_path, monkeypatch):
|
||||||
"""Point the baked-in copy at an empty directory.
|
"""Point the baked-in copy at an empty directory.
|
||||||
|
|
||||||
In a source checkout `src/thoughtsync/client/` does not exist, so these tests
|
In a source checkout `src/inkwell/client/` does not exist, so these tests
|
||||||
would pass anyway — but only by accident of where they are run. A built image
|
would pass anyway — but only by accident of where they are run. A built image
|
||||||
has real clients there, and a test that silently depends on which tree it is in
|
has real clients there, and a test that silently depends on which tree it is in
|
||||||
is one that will eventually lie.
|
is one that will eventually lie.
|
||||||
@@ -118,13 +118,17 @@ def test_every_platform_has_its_own_filenames():
|
|||||||
assert not set(artifacts) & set(sidecars)
|
assert not set(artifacts) & set(sidecars)
|
||||||
|
|
||||||
|
|
||||||
def test_the_android_names_are_the_ones_already_published():
|
def test_the_android_names_are_the_ones_the_image_build_writes():
|
||||||
"""Pinned because renaming them is a tidy-up that strands every installed phone.
|
"""Pinned against `packaging/fetch-clients.sh`, which writes the APK and its
|
||||||
|
sidecar under these names when it bakes them into the image. The two are a pair
|
||||||
|
with nothing checking them against each other, so a rename on one side reads as
|
||||||
|
"no Android client" on the other rather than as an error.
|
||||||
|
|
||||||
The Android lane publishes these exact names and CI bakes them in under them.
|
Phones never ask for these names; they poll `/api/client/android`. The names
|
||||||
|
changed once, with the rename to Inkwell (Scribe note 5071).
|
||||||
"""
|
"""
|
||||||
assert APK_NAME == "thoughtsync.apk"
|
assert APK_NAME == "inkwell.apk"
|
||||||
assert MANIFEST_NAME == "thoughtsync-android.json"
|
assert MANIFEST_NAME == "inkwell-android.json"
|
||||||
|
|
||||||
|
|
||||||
# --- absence is an ordinary answer -------------------------------------------
|
# --- absence is an ordinary answer -------------------------------------------
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from thoughtsync.app import create_app
|
from inkwell.app import create_app
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
+17
-17
@@ -22,20 +22,20 @@ import pytest
|
|||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
from sqlalchemy import select, text
|
from sqlalchemy import select, text
|
||||||
|
|
||||||
from thoughtsync import ratelimit
|
from inkwell import ratelimit
|
||||||
from thoughtsync.app import create_app
|
from inkwell.app import create_app
|
||||||
from thoughtsync.db import dispose_engine, session_scope
|
from inkwell.db import dispose_engine, session_scope
|
||||||
from thoughtsync.models.label import NoteLabel
|
from inkwell.models.label import NoteLabel
|
||||||
from thoughtsync.models.note import Note
|
from inkwell.models.note import Note
|
||||||
from thoughtsync.models.user import User
|
from inkwell.models.user import User
|
||||||
from thoughtsync.notes.tags import _lift_and_reconcile_tags
|
from inkwell.notes.tags import _lift_and_reconcile_tags
|
||||||
from thoughtsync.settings import get_setting, live, refresh_live, reset_live, set_settings
|
from inkwell.settings import get_setting, live, refresh_live, reset_live, set_settings
|
||||||
from thoughtsync.notes.checklist import parse_items, set_item_checked
|
from inkwell.notes.checklist import parse_items, set_item_checked
|
||||||
from thoughtsync.notes.helpers import derive_display_title
|
from inkwell.notes.helpers import derive_display_title
|
||||||
from thoughtsync.models.note_link_preview import NoteLinkPreview
|
from inkwell.models.note_link_preview import NoteLinkPreview
|
||||||
from thoughtsync.models.note_revision import NoteRevision
|
from inkwell.models.note_revision import NoteRevision
|
||||||
from thoughtsync.revisions import REVISION_WINDOW_MINUTES, should_snapshot
|
from inkwell.revisions import REVISION_WINDOW_MINUTES, should_snapshot
|
||||||
from thoughtsync.unfurl_queue import _unfurl_new_urls, detect_urls
|
from inkwell.unfurl_queue import _unfurl_new_urls, detect_urls
|
||||||
|
|
||||||
pytestmark = pytest.mark.integration
|
pytestmark = pytest.mark.integration
|
||||||
|
|
||||||
@@ -305,7 +305,7 @@ async def test_auto_unfurl_stores_a_preview_and_skips_what_is_cached(db, owner,
|
|||||||
calls.append(url)
|
calls.append(url)
|
||||||
return {"url": url, "title": f"T {url}", "description": None, "image_url": None, "site_name": "example.com"}
|
return {"url": url, "title": f"T {url}", "description": None, "image_url": None, "site_name": "example.com"}
|
||||||
|
|
||||||
monkeypatch.setattr("thoughtsync.unfurl_queue.unfurl", fake_unfurl)
|
monkeypatch.setattr("inkwell.unfurl_queue.unfurl", fake_unfurl)
|
||||||
|
|
||||||
await _unfurl_new_urls(note.id, note.body)
|
await _unfurl_new_urls(note.id, note.body)
|
||||||
assert sorted(calls) == ["https://example.com/a", "https://example.com/b"]
|
assert sorted(calls) == ["https://example.com/a", "https://example.com/b"]
|
||||||
@@ -331,7 +331,7 @@ async def test_auto_unfurl_drops_a_preview_whose_url_left_the_body(db, owner, mo
|
|||||||
# Simulate the body changing while the request was in the air.
|
# Simulate the body changing while the request was in the air.
|
||||||
return {"url": url, "title": "T", "description": None, "image_url": None, "site_name": None}
|
return {"url": url, "title": "T", "description": None, "image_url": None, "site_name": None}
|
||||||
|
|
||||||
monkeypatch.setattr("thoughtsync.unfurl_queue.unfurl", fake_unfurl)
|
monkeypatch.setattr("inkwell.unfurl_queue.unfurl", fake_unfurl)
|
||||||
note.body = "changed my mind"
|
note.body = "changed my mind"
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@@ -352,7 +352,7 @@ async def test_detection_agrees_with_what_gets_stored(db, owner, monkeypatch):
|
|||||||
async def fake_unfurl(url):
|
async def fake_unfurl(url):
|
||||||
return {"url": url, "title": "T", "description": None, "image_url": None, "site_name": None}
|
return {"url": url, "title": "T", "description": None, "image_url": None, "site_name": None}
|
||||||
|
|
||||||
monkeypatch.setattr("thoughtsync.unfurl_queue.unfurl", fake_unfurl)
|
monkeypatch.setattr("inkwell.unfurl_queue.unfurl", fake_unfurl)
|
||||||
await _unfurl_new_urls(note.id, body)
|
await _unfurl_new_urls(note.id, body)
|
||||||
|
|
||||||
stored = {
|
stored = {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import uuid
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from thoughtsync.app import create_app
|
from inkwell.app import create_app
|
||||||
from thoughtsync.models.label import Label
|
from inkwell.models.label import Label
|
||||||
from thoughtsync.serialize import serialize_label
|
from inkwell.serialize import serialize_label
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
+35
-8
@@ -1,12 +1,15 @@
|
|||||||
|
import io
|
||||||
|
import json
|
||||||
|
import zipfile
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from thoughtsync.app import create_app
|
from inkwell.app import create_app
|
||||||
from thoughtsync.common import coerce_bool, parse_dt
|
from inkwell.common import coerce_bool, parse_dt
|
||||||
from thoughtsync.colors import NOTE_COLORS
|
from inkwell.colors import NOTE_COLORS
|
||||||
from thoughtsync.models.note import Note
|
from inkwell.models.note import Note
|
||||||
from thoughtsync.notes.checklist import (
|
from inkwell.notes.checklist import (
|
||||||
append_item,
|
append_item,
|
||||||
parse_items,
|
parse_items,
|
||||||
remove_item,
|
remove_item,
|
||||||
@@ -14,13 +17,15 @@ from thoughtsync.notes.checklist import (
|
|||||||
set_item_text,
|
set_item_text,
|
||||||
strip_marker,
|
strip_marker,
|
||||||
)
|
)
|
||||||
from thoughtsync.unfurl_queue import detect_urls
|
from inkwell.unfurl_queue import detect_urls
|
||||||
from thoughtsync.notes import (
|
from inkwell.notes import (
|
||||||
|
_ImportBudget,
|
||||||
_attachment_ext,
|
_attachment_ext,
|
||||||
_header_filename,
|
_header_filename,
|
||||||
_keep_spec,
|
_keep_spec,
|
||||||
_native_spec,
|
_native_spec,
|
||||||
_note_markdown,
|
_note_markdown,
|
||||||
|
_read_import_specs,
|
||||||
_safe_filename,
|
_safe_filename,
|
||||||
_slugify,
|
_slugify,
|
||||||
_usec_to_dt,
|
_usec_to_dt,
|
||||||
@@ -32,7 +37,8 @@ from thoughtsync.notes import (
|
|||||||
parse_list_items,
|
parse_list_items,
|
||||||
parse_tags,
|
parse_tags,
|
||||||
)
|
)
|
||||||
from thoughtsync.notes.tags import split_body_tags
|
from inkwell.notes.import_export import NATIVE_APP_MARKERS
|
||||||
|
from inkwell.notes.tags import split_body_tags
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -672,3 +678,24 @@ def test_native_spec_of_a_pre_m304_export_still_carries_its_items():
|
|||||||
# items at all.
|
# items at all.
|
||||||
old = {"body": "shopping", "items": [{"text": "milk", "checked": True}]}
|
old = {"body": "shopping", "items": [{"text": "milk", "checked": True}]}
|
||||||
assert _native_spec(old)["items"] == [{"text": "milk", "checked": True}]
|
assert _native_spec(old)["items"] == [{"text": "milk", "checked": True}]
|
||||||
|
|
||||||
|
|
||||||
|
def _export_zip(app_marker: str) -> zipfile.ZipFile:
|
||||||
|
buf = io.BytesIO()
|
||||||
|
with zipfile.ZipFile(buf, "w") as zf:
|
||||||
|
zf.writestr("notes.json", json.dumps({"app": app_marker, "notes": [{"body": "kept"}]}))
|
||||||
|
return zipfile.ZipFile(io.BytesIO(buf.getvalue()))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("marker", ["inkwell", "thoughtsync"])
|
||||||
|
def test_an_export_imports_under_either_name(marker):
|
||||||
|
# "thoughtsync" is what every export written before the rename to Inkwell says.
|
||||||
|
# Those zips are backups, so they have to keep restoring as native exports rather
|
||||||
|
# than falling through to the Keep importer, which would find nothing in them.
|
||||||
|
specs, source = _read_import_specs(_export_zip(marker), _ImportBudget())
|
||||||
|
assert source == "inkwell"
|
||||||
|
assert [s["body"] for s in specs] == ["kept"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_exports_are_written_as_inkwell():
|
||||||
|
assert NATIVE_APP_MARKERS[0] == "inkwell"
|
||||||
|
|||||||
+2
-2
@@ -4,8 +4,8 @@ The whole security property is "a caller cannot forge their own address", and it
|
|||||||
on counting in from the RIGHT of the header rather than the left. These are the cases
|
on counting in from the RIGHT of the header rather than the left. These are the cases
|
||||||
that tell the two apart — pure functions, no request context, no database.
|
that tell the two apart — pure functions, no request context, no database.
|
||||||
"""
|
"""
|
||||||
from thoughtsync.proxy import forwarded_for, trusted_entry
|
from inkwell.proxy import forwarded_for, trusted_entry
|
||||||
from thoughtsync.settings import live
|
from inkwell.settings import live
|
||||||
|
|
||||||
PEER = "10.0.0.1" # the socket address: our own proxy, or the caller when unproxied
|
PEER = "10.0.0.1" # the socket address: our own proxy, or the caller when unproxied
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import time
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from thoughtsync import ratelimit
|
from inkwell import ratelimit
|
||||||
from thoughtsync.settings import live
|
from inkwell.settings import live
|
||||||
from thoughtsync.app import create_app
|
from inkwell.app import create_app
|
||||||
from thoughtsync.ratelimit import SlidingWindow
|
from inkwell.ratelimit import SlidingWindow
|
||||||
|
|
||||||
|
|
||||||
def window(limit: int, window_s: float) -> SlidingWindow:
|
def window(limit: int, window_s: float) -> SlidingWindow:
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ from datetime import datetime, timedelta, timezone
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from thoughtsync.retention import (
|
from inkwell.retention import (
|
||||||
SWEEP_BATCH,
|
SWEEP_BATCH,
|
||||||
SWEEP_INTERVAL_SECONDS,
|
SWEEP_INTERVAL_SECONDS,
|
||||||
SWEEP_STARTUP_DELAY_SECONDS,
|
SWEEP_STARTUP_DELAY_SECONDS,
|
||||||
expired_before,
|
expired_before,
|
||||||
sweep_expired_trash,
|
sweep_expired_trash,
|
||||||
)
|
)
|
||||||
from thoughtsync.settings import REGISTRY, get_public_config, validate_updates
|
from inkwell.settings import REGISTRY, get_public_config, validate_updates
|
||||||
|
|
||||||
NOW = datetime(2026, 7, 26, 12, 0, tzinfo=timezone.utc)
|
NOW = datetime(2026, 7, 26, 12, 0, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from thoughtsync.app import create_app
|
from inkwell.app import create_app
|
||||||
from thoughtsync.saved_filters import clean_params
|
from inkwell.saved_filters import clean_params
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from thoughtsync.security import generate_token, hash_password, hash_token, verify_password
|
from inkwell.security import generate_token, hash_password, hash_token, verify_password
|
||||||
|
|
||||||
|
|
||||||
def test_password_roundtrip():
|
def test_password_roundtrip():
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ hook, and this suite has no Postgres.
|
|||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from thoughtsync.app import create_app
|
from inkwell.app import create_app
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from thoughtsync.settings import REGISTRY, validate_updates
|
from inkwell.settings import REGISTRY, validate_updates
|
||||||
|
|
||||||
|
|
||||||
def test_registry_has_expected_keys():
|
def test_registry_has_expected_keys():
|
||||||
|
|||||||
+2
-2
@@ -2,8 +2,8 @@ from datetime import datetime, timezone
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from thoughtsync.app import create_app
|
from inkwell.app import create_app
|
||||||
from thoughtsync.sync import (
|
from inkwell.sync import (
|
||||||
DEFAULT_LIMIT,
|
DEFAULT_LIMIT,
|
||||||
MAX_LIMIT,
|
MAX_LIMIT,
|
||||||
MIN_CLIENT_PROTOCOL_VERSION,
|
MIN_CLIENT_PROTOCOL_VERSION,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import ipaddress
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from thoughtsync.app import create_app
|
from inkwell.app import create_app
|
||||||
from thoughtsync.unfurl import UnfurlError, extract_preview, is_public_ip, validate_url
|
from inkwell.unfurl import UnfurlError, extract_preview, is_public_ip, validate_url
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
Reference in New Issue
Block a user