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
|
||||
#
|
||||
@@ -29,15 +29,15 @@ POSTGRES_PASSWORD=
|
||||
#
|
||||
# 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.
|
||||
#THOUGHTSYNC_TAG=latest
|
||||
#INKWELL_TAG=latest
|
||||
|
||||
# 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
|
||||
# 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.
|
||||
#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 —
|
||||
# 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
|
||||
# accounts, device tokens issued) are logged at INFO and read with
|
||||
# `docker compose logs app`.
|
||||
#THOUGHTSYNC_LOG_LEVEL=INFO
|
||||
#INKWELL_LOG_LEVEL=INFO
|
||||
|
||||
# Database identity. Changing these AFTER the first start does not rename anything
|
||||
# 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_DB=thoughtsync
|
||||
#
|
||||
# Rename any THOUGHTSYNC_* lines already in your .env to INKWELL_* — the old
|
||||
# names are no longer read.
|
||||
|
||||
# --- a note on HTTPS --------------------------------------------------------
|
||||
#
|
||||
|
||||
@@ -54,7 +54,7 @@ permissions:
|
||||
|
||||
env:
|
||||
REGISTRY: git.fabledsword.com
|
||||
IMAGE: git.fabledsword.com/bvandeusen/thoughtsync
|
||||
IMAGE: git.fabledsword.com/bvandeusen/inkwell
|
||||
|
||||
jobs:
|
||||
# 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.
|
||||
image: postgres:16-alpine
|
||||
env:
|
||||
POSTGRES_USER: thoughtsync
|
||||
POSTGRES_USER: inkwell
|
||||
POSTGRES_PASSWORD: ci_integration
|
||||
POSTGRES_DB: thoughtsync_test
|
||||
POSTGRES_DB: inkwell_test
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U thoughtsync"
|
||||
--health-cmd "pg_isready -U inkwell"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
@@ -243,7 +243,7 @@ jobs:
|
||||
test -n "$PG"
|
||||
PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG")
|
||||
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) —
|
||||
# no bash /dev/tcp — so use the Python that is always present here.
|
||||
/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
|
||||
# points at /app/src so the runtime imports this source tree (with static/ present),
|
||||
# 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/ alembic/
|
||||
|
||||
@@ -41,7 +41,7 @@ COPY alembic/ alembic/
|
||||
# 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
|
||||
# rest (client_dist.py).
|
||||
COPY client/ src/thoughtsync/client/
|
||||
COPY client/ src/inkwell/client/
|
||||
|
||||
ENV PYTHONPATH=/app/src
|
||||
|
||||
@@ -52,4 +52,4 @@ EXPOSE 5000
|
||||
# Wait for the database, run migrations, then serve. The DB wait keeps a briefly
|
||||
# slow/unready database from crash-looping the container. Family convention
|
||||
# (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
|
||||
|
||||
```
|
||||
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`)
|
||||
tests/ DB-free unit tests (pytest)
|
||||
frontend/ Vue 3 + Vite + TypeScript + Tailwind SPA
|
||||
@@ -28,7 +28,7 @@ Backend (needs a Postgres reachable at `THOUGHTSYNC_DATABASE_URL`):
|
||||
```sh
|
||||
pip install -e ".[dev]"
|
||||
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`):
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@ script_location = %(here)s/alembic
|
||||
prepend_sys_path = . src
|
||||
path_separator = os
|
||||
|
||||
# Local-dev default; overridden at runtime by THOUGHTSYNC_DATABASE_URL (see env.py).
|
||||
sqlalchemy.url = postgresql+asyncpg://thoughtsync:thoughtsync@localhost:5432/thoughtsync
|
||||
# Local-dev default; overridden at runtime by INKWELL_DATABASE_URL (see env.py).
|
||||
sqlalchemy.url = postgresql+asyncpg://inkwell:inkwell@localhost:5432/inkwell
|
||||
|
||||
|
||||
[loggers]
|
||||
|
||||
+3
-3
@@ -8,8 +8,8 @@ from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
|
||||
from alembic import context
|
||||
|
||||
from thoughtsync.models import Base
|
||||
import thoughtsync.models.all # noqa: F401 — registers every model on Base.metadata
|
||||
from inkwell.models import Base
|
||||
import inkwell.models.all # noqa: F401 — registers every model on Base.metadata
|
||||
|
||||
config = context.config
|
||||
|
||||
@@ -18,7 +18,7 @@ if config.config_file_name is not None:
|
||||
|
||||
config.set_main_option(
|
||||
"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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! 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
|
||||
//! 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
|
||||
|
||||
+11
-11
@@ -14,15 +14,15 @@ services:
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: thoughtsync
|
||||
POSTGRES_PASSWORD: thoughtsync
|
||||
POSTGRES_DB: thoughtsync
|
||||
POSTGRES_USER: inkwell
|
||||
POSTGRES_PASSWORD: inkwell
|
||||
POSTGRES_DB: inkwell
|
||||
volumes:
|
||||
- thoughtsync-dev-db:/var/lib/postgresql/data
|
||||
- inkwell-dev-db:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U thoughtsync"]
|
||||
test: ["CMD-SHELL", "pg_isready -U inkwell"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
@@ -34,22 +34,22 @@ services:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
THOUGHTSYNC_DATABASE_URL: postgresql+asyncpg://thoughtsync:thoughtsync@db:5432/thoughtsync
|
||||
INKWELL_DATABASE_URL: postgresql+asyncpg://inkwell:inkwell@db:5432/inkwell
|
||||
PYTHONPATH: /app/src
|
||||
volumes:
|
||||
- ./pyproject.toml:/app/pyproject.toml
|
||||
- ./alembic.ini:/app/alembic.ini
|
||||
- ./alembic:/app/alembic
|
||||
- ./src:/app/src
|
||||
- thoughtsync-dev-data:/var/thoughtsync
|
||||
- inkwell-dev-data:/var/inkwell
|
||||
ports:
|
||||
- "5000:5000"
|
||||
# Install deps, wait for the DB, run migrations, then serve with live reload.
|
||||
command: >
|
||||
sh -c "pip install --quiet -e . &&
|
||||
python -m thoughtsync.dbwait &&
|
||||
python -m inkwell.dbwait &&
|
||||
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:
|
||||
image: node:22-alpine
|
||||
@@ -67,5 +67,5 @@ services:
|
||||
command: sh -c "npm install && npm run dev -- --host 0.0.0.0"
|
||||
|
||||
volumes:
|
||||
thoughtsync-dev-db:
|
||||
thoughtsync-dev-data:
|
||||
inkwell-dev-db:
|
||||
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
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# 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:
|
||||
#
|
||||
# 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
|
||||
# are gone.
|
||||
|
||||
@@ -27,22 +27,21 @@ services:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-thoughtsync}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-inkwell}
|
||||
# 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
|
||||
# instead, with the message below.
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env — see .env.example}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-thoughtsync}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-inkwell}
|
||||
volumes:
|
||||
# Volume names kept from the previous compose file so an existing deployment
|
||||
# upgrades in place. Renaming them would silently start against an empty
|
||||
# database while the old one sat there, orphaned and looking like data loss.
|
||||
- thoughtsync-db:/var/lib/postgresql/data
|
||||
# Which volume this resolves to is set at the bottom of this file, and an
|
||||
# existing deployment overrides it from .env rather than renaming anything.
|
||||
- inkwell-db:/var/lib/postgresql/data
|
||||
# Deliberately NOT published to the host. The app reaches Postgres over the
|
||||
# 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:
|
||||
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
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
@@ -56,9 +55,9 @@ services:
|
||||
max-file: "3"
|
||||
|
||||
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.
|
||||
image: git.fabledsword.com/bvandeusen/thoughtsync:${THOUGHTSYNC_TAG:-latest}
|
||||
image: git.fabledsword.com/bvandeusen/inkwell:${INKWELL_TAG:-latest}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
@@ -66,11 +65,11 @@ services:
|
||||
environment:
|
||||
# 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).
|
||||
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:
|
||||
# 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.
|
||||
- thoughtsync-data:/var/thoughtsync
|
||||
- inkwell-data:/var/inkwell
|
||||
# WHERE THE APP IS REACHABLE FROM. Three shapes, and the right answer is
|
||||
# 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
|
||||
# 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.
|
||||
#
|
||||
# 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
|
||||
# does.
|
||||
ports:
|
||||
- "${THOUGHTSYNC_BIND:-0.0.0.0}:${THOUGHTSYNC_PORT:-5000}:5000"
|
||||
- "${INKWELL_BIND:-0.0.0.0}:${INKWELL_PORT:-5000}:5000"
|
||||
healthcheck:
|
||||
# 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.
|
||||
@@ -113,5 +112,12 @@ services:
|
||||
logging: *logging
|
||||
|
||||
volumes:
|
||||
thoughtsync-db:
|
||||
thoughtsync-data:
|
||||
# Named explicitly so the name is one a person can type, and overridable so a
|
||||
# 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
|
||||
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` |
|
||||
| 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
|
||||
// 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).
|
||||
//
|
||||
// `-` and `*` only, deliberately, even though the `ul` matcher below also takes `+`.
|
||||
|
||||
+10
-10
@@ -37,7 +37,7 @@ case "$channel" in dev|stable) : ;; *)
|
||||
esac
|
||||
|
||||
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`
|
||||
# (packaging/channel-tag.sh, Scribe #2184).
|
||||
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
|
||||
# a binary AXML manifest, so the values are recorded where they were already known.
|
||||
# Copied verbatim rather than rebuilt here.
|
||||
if fetch "$BASE/thoughtsync.apk" "$dest/thoughtsync.apk" &&
|
||||
fetch "$BASE/thoughtsync-android.json" "$dest/thoughtsync-android.json"; then
|
||||
echo " android $(field "$dest/thoughtsync-android.json" version_name)"
|
||||
if fetch "$BASE/thoughtsync.apk" "$dest/inkwell.apk" &&
|
||||
fetch "$BASE/thoughtsync-android.json" "$dest/inkwell-android.json"; then
|
||||
echo " android $(field "$dest/inkwell-android.json" version_name)"
|
||||
else
|
||||
# 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
|
||||
# version for.
|
||||
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
|
||||
|
||||
# --- desktop -----------------------------------------------------------------
|
||||
@@ -132,10 +132,10 @@ bake_desktop() {
|
||||
#
|
||||
# `<platform id>|<published name>|<name on disk>`
|
||||
for row in \
|
||||
"linux-deb|ThoughtSync_${key}_amd64.deb|thoughtsync.deb" \
|
||||
"linux-pacman|thoughtsync-${key}-1-x86_64.pkg.tar.zst|thoughtsync.pkg.tar.zst" \
|
||||
"linux-appimage|ThoughtSync_${key}_amd64.AppImage|thoughtsync.AppImage" \
|
||||
"windows|ThoughtSync_${key}_x64-setup.exe|thoughtsync-setup.exe"
|
||||
"linux-deb|ThoughtSync_${key}_amd64.deb|inkwell.deb" \
|
||||
"linux-pacman|thoughtsync-${key}-1-x86_64.pkg.tar.zst|inkwell.pkg.tar.zst" \
|
||||
"linux-appimage|ThoughtSync_${key}_amd64.AppImage|inkwell.AppImage" \
|
||||
"windows|ThoughtSync_${key}_x64-setup.exe|inkwell-setup.exe"
|
||||
do
|
||||
id="${row%%|*}"; rest="${row#*|}"
|
||||
remote="${rest%%|*}"; local_name="${rest#*|}"
|
||||
@@ -158,7 +158,7 @@ bake_desktop() {
|
||||
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"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
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>}"
|
||||
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ requires = ["setuptools>=68.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "thoughtsync"
|
||||
name = "inkwell"
|
||||
version = "0.2.0"
|
||||
description = "Self-hosted personal thought-capture web app (FabledSword family)"
|
||||
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.
|
||||
#
|
||||
@@ -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
|
||||
# own setup; LOG_LEVEL lets an operator turn it up without a code change.
|
||||
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",
|
||||
)
|
||||
|
||||
@@ -38,8 +38,8 @@ wins — would mean replacing one client silently retracts the other four.
|
||||
|
||||
Two files, and both must be present:
|
||||
|
||||
- the artifact, at a FIXED name (`thoughtsync.deb`, not
|
||||
`ThoughtSync_2026.08.30.0307_amd64.deb`)
|
||||
- the artifact, at a FIXED name (`inkwell.deb`, not
|
||||
`Inkwell_2026.08.30.0307_amd64.deb`)
|
||||
- its sidecar, `{version_name, version_code, size, sha256}`
|
||||
|
||||
**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
|
||||
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
|
||||
bundle that cannot be verified cannot be offered as an update — so a missing
|
||||
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
|
||||
# `/api/client/android`. Renaming them to match the pattern below would
|
||||
# buy tidiness and strand every installed phone.
|
||||
artifact="thoughtsync.apk",
|
||||
sidecar="thoughtsync-android.json",
|
||||
artifact="inkwell.apk",
|
||||
sidecar="inkwell-android.json",
|
||||
mimetype="application/vnd.android.package-archive",
|
||||
),
|
||||
Platform(
|
||||
id="linux-deb",
|
||||
label="Debian / Ubuntu",
|
||||
artifact="thoughtsync.deb",
|
||||
sidecar="thoughtsync-linux-deb.json",
|
||||
artifact="inkwell.deb",
|
||||
sidecar="inkwell-linux-deb.json",
|
||||
mimetype="application/vnd.debian.binary-package",
|
||||
code_is_int=False,
|
||||
),
|
||||
Platform(
|
||||
id="linux-pacman",
|
||||
label="Arch / CachyOS",
|
||||
artifact="thoughtsync.pkg.tar.zst",
|
||||
sidecar="thoughtsync-linux-pacman.json",
|
||||
artifact="inkwell.pkg.tar.zst",
|
||||
sidecar="inkwell-linux-pacman.json",
|
||||
mimetype="application/zstd",
|
||||
code_is_int=False,
|
||||
),
|
||||
Platform(
|
||||
id="linux-appimage",
|
||||
label="Other Linux (AppImage)",
|
||||
artifact="thoughtsync.AppImage",
|
||||
sidecar="thoughtsync-linux-appimage.json",
|
||||
artifact="inkwell.AppImage",
|
||||
sidecar="inkwell-linux-appimage.json",
|
||||
# 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
|
||||
# cannot be wrong.
|
||||
@@ -167,8 +167,8 @@ PLATFORMS: tuple[Platform, ...] = (
|
||||
Platform(
|
||||
id="windows",
|
||||
label="Windows",
|
||||
artifact="thoughtsync-setup.exe",
|
||||
sidecar="thoughtsync-windows.json",
|
||||
artifact="inkwell-setup.exe",
|
||||
sidecar="inkwell-windows.json",
|
||||
mimetype="application/vnd.microsoft.portable-executable",
|
||||
code_is_int=False,
|
||||
),
|
||||
@@ -7,26 +7,26 @@ from pathlib import Path
|
||||
class Config:
|
||||
"""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
|
||||
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
|
||||
``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.
|
||||
|
||||
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
|
||||
container recreation; a text-notes-only install never writes to it.
|
||||
"""
|
||||
|
||||
DATA_DIR = "/var/thoughtsync"
|
||||
DATA_DIR = "/var/inkwell"
|
||||
|
||||
DATABASE_URL = os.environ.get(
|
||||
"THOUGHTSYNC_DATABASE_URL",
|
||||
"postgresql+asyncpg://thoughtsync:thoughtsync@localhost:5432/thoughtsync",
|
||||
"INKWELL_DATABASE_URL",
|
||||
"postgresql+asyncpg://inkwell:inkwell@localhost:5432/inkwell",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -50,5 +50,5 @@ class Config:
|
||||
@classmethod
|
||||
def secret_key_env(cls) -> str | None:
|
||||
"""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):
|
||||
"""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"
|
||||
|
||||
@@ -10,7 +10,7 @@ from . import Base
|
||||
|
||||
class Setting(Base):
|
||||
"""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)."""
|
||||
|
||||
__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
|
||||
`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."""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -237,7 +237,7 @@ async def export_notes():
|
||||
).all()
|
||||
|
||||
payload: dict = {
|
||||
"app": "thoughtsync",
|
||||
"app": "inkwell",
|
||||
"version": 1,
|
||||
"exported_at": datetime.now(timezone.utc).isoformat(),
|
||||
"labels": [{"name": lb.name, "color": lb.color} for lb in all_labels],
|
||||
@@ -278,7 +278,7 @@ async def export_notes():
|
||||
data,
|
||||
headers={
|
||||
"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:
|
||||
|
||||
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
|
||||
|
||||
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;
|
||||
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
|
||||
exhaust memory/disk."""
|
||||
from __future__ import annotations
|
||||
@@ -58,7 +58,13 @@ def _note_markdown(note: Note, labels: list) -> str:
|
||||
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
|
||||
# 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:
|
||||
"""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."""
|
||||
return {
|
||||
"title": n.get("title"),
|
||||
@@ -186,9 +192,10 @@ class _ImportBudget:
|
||||
|
||||
|
||||
def _read_import_specs(zf: zipfile.ZipFile, budget: _ImportBudget) -> tuple[list[dict], str]:
|
||||
"""Detect the archive format and return (specs, source). A ThoughtSync export
|
||||
is recognized by its notes.json (app == thoughtsync); otherwise each Keep-shaped
|
||||
<note>.json is imported. Returns ([], "") when nothing importable is found."""
|
||||
"""Detect the archive format and return (specs, source). An Inkwell export
|
||||
is recognized by its notes.json (app in NATIVE_APP_MARKERS); otherwise each
|
||||
Keep-shaped <note>.json is imported. Returns ([], "") when nothing importable
|
||||
is found."""
|
||||
names = zf.namelist()
|
||||
for name in names:
|
||||
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))
|
||||
except (ValueError, KeyError):
|
||||
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)]
|
||||
return specs, "thoughtsync"
|
||||
return specs, "inkwell"
|
||||
|
||||
keep_specs: list[dict] = []
|
||||
keep_keys = ("textContent", "listContent", "isPinned", "isArchived", "isTrashed", "userEditedTimestampUsec")
|
||||
@@ -255,7 +262,7 @@ async def _create_imported_note(
|
||||
body = spec.get("body") or ""
|
||||
# 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.
|
||||
# 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.
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
import pytest
|
||||
|
||||
from thoughtsync.config import Config
|
||||
from inkwell.config import Config
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolated_data_dir(tmp_path, monkeypatch):
|
||||
"""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."""
|
||||
monkeypatch.setattr(Config, "DATA_DIR", str(tmp_path / "data"))
|
||||
yield
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import uuid
|
||||
|
||||
from thoughtsync.acl import visible_to_user
|
||||
from thoughtsync.models.user import User
|
||||
from inkwell.acl import visible_to_user
|
||||
from inkwell.models.user import User
|
||||
|
||||
|
||||
def test_visible_to_user_builds_owner_or_shared_predicate():
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from thoughtsync.app import create_app
|
||||
from inkwell.app import create_app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from thoughtsync.app import create_app
|
||||
from inkwell.app import create_app
|
||||
|
||||
|
||||
@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
|
||||
wrong reason.
|
||||
"""
|
||||
from thoughtsync import __version__
|
||||
from inkwell import __version__
|
||||
|
||||
monkeypatch.delenv("APP_VERSION", raising=False)
|
||||
reported = await reported_version()
|
||||
|
||||
+14
-10
@@ -3,9 +3,9 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from thoughtsync import client_dist
|
||||
from thoughtsync.app import create_app
|
||||
from thoughtsync.client_dist import (
|
||||
from inkwell import client_dist
|
||||
from inkwell.app import create_app
|
||||
from inkwell.client_dist import (
|
||||
APK_NAME,
|
||||
BY_ID,
|
||||
MANIFEST_NAME,
|
||||
@@ -15,7 +15,7 @@ from thoughtsync.client_dist import (
|
||||
release,
|
||||
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
|
||||
# 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):
|
||||
"""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
|
||||
has real clients there, and a test that silently depends on which tree it is in
|
||||
is one that will eventually lie.
|
||||
@@ -118,13 +118,17 @@ def test_every_platform_has_its_own_filenames():
|
||||
assert not set(artifacts) & set(sidecars)
|
||||
|
||||
|
||||
def test_the_android_names_are_the_ones_already_published():
|
||||
"""Pinned because renaming them is a tidy-up that strands every installed phone.
|
||||
def test_the_android_names_are_the_ones_the_image_build_writes():
|
||||
"""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 MANIFEST_NAME == "thoughtsync-android.json"
|
||||
assert APK_NAME == "inkwell.apk"
|
||||
assert MANIFEST_NAME == "inkwell-android.json"
|
||||
|
||||
|
||||
# --- absence is an ordinary answer -------------------------------------------
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from thoughtsync.app import create_app
|
||||
from inkwell.app import create_app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
+17
-17
@@ -22,20 +22,20 @@ import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select, text
|
||||
|
||||
from thoughtsync import ratelimit
|
||||
from thoughtsync.app import create_app
|
||||
from thoughtsync.db import dispose_engine, session_scope
|
||||
from thoughtsync.models.label import NoteLabel
|
||||
from thoughtsync.models.note import Note
|
||||
from thoughtsync.models.user import User
|
||||
from thoughtsync.notes.tags import _lift_and_reconcile_tags
|
||||
from thoughtsync.settings import get_setting, live, refresh_live, reset_live, set_settings
|
||||
from thoughtsync.notes.checklist import parse_items, set_item_checked
|
||||
from thoughtsync.notes.helpers import derive_display_title
|
||||
from thoughtsync.models.note_link_preview import NoteLinkPreview
|
||||
from thoughtsync.models.note_revision import NoteRevision
|
||||
from thoughtsync.revisions import REVISION_WINDOW_MINUTES, should_snapshot
|
||||
from thoughtsync.unfurl_queue import _unfurl_new_urls, detect_urls
|
||||
from inkwell import ratelimit
|
||||
from inkwell.app import create_app
|
||||
from inkwell.db import dispose_engine, session_scope
|
||||
from inkwell.models.label import NoteLabel
|
||||
from inkwell.models.note import Note
|
||||
from inkwell.models.user import User
|
||||
from inkwell.notes.tags import _lift_and_reconcile_tags
|
||||
from inkwell.settings import get_setting, live, refresh_live, reset_live, set_settings
|
||||
from inkwell.notes.checklist import parse_items, set_item_checked
|
||||
from inkwell.notes.helpers import derive_display_title
|
||||
from inkwell.models.note_link_preview import NoteLinkPreview
|
||||
from inkwell.models.note_revision import NoteRevision
|
||||
from inkwell.revisions import REVISION_WINDOW_MINUTES, should_snapshot
|
||||
from inkwell.unfurl_queue import _unfurl_new_urls, detect_urls
|
||||
|
||||
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)
|
||||
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)
|
||||
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.
|
||||
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"
|
||||
await db.commit()
|
||||
|
||||
@@ -352,7 +352,7 @@ async def test_detection_agrees_with_what_gets_stored(db, owner, monkeypatch):
|
||||
async def fake_unfurl(url):
|
||||
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)
|
||||
|
||||
stored = {
|
||||
|
||||
@@ -2,9 +2,9 @@ import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from thoughtsync.app import create_app
|
||||
from thoughtsync.models.label import Label
|
||||
from thoughtsync.serialize import serialize_label
|
||||
from inkwell.app import create_app
|
||||
from inkwell.models.label import Label
|
||||
from inkwell.serialize import serialize_label
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
+35
-8
@@ -1,12 +1,15 @@
|
||||
import io
|
||||
import json
|
||||
import zipfile
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from thoughtsync.app import create_app
|
||||
from thoughtsync.common import coerce_bool, parse_dt
|
||||
from thoughtsync.colors import NOTE_COLORS
|
||||
from thoughtsync.models.note import Note
|
||||
from thoughtsync.notes.checklist import (
|
||||
from inkwell.app import create_app
|
||||
from inkwell.common import coerce_bool, parse_dt
|
||||
from inkwell.colors import NOTE_COLORS
|
||||
from inkwell.models.note import Note
|
||||
from inkwell.notes.checklist import (
|
||||
append_item,
|
||||
parse_items,
|
||||
remove_item,
|
||||
@@ -14,13 +17,15 @@ from thoughtsync.notes.checklist import (
|
||||
set_item_text,
|
||||
strip_marker,
|
||||
)
|
||||
from thoughtsync.unfurl_queue import detect_urls
|
||||
from thoughtsync.notes import (
|
||||
from inkwell.unfurl_queue import detect_urls
|
||||
from inkwell.notes import (
|
||||
_ImportBudget,
|
||||
_attachment_ext,
|
||||
_header_filename,
|
||||
_keep_spec,
|
||||
_native_spec,
|
||||
_note_markdown,
|
||||
_read_import_specs,
|
||||
_safe_filename,
|
||||
_slugify,
|
||||
_usec_to_dt,
|
||||
@@ -32,7 +37,8 @@ from thoughtsync.notes import (
|
||||
parse_list_items,
|
||||
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
|
||||
@@ -672,3 +678,24 @@ def test_native_spec_of_a_pre_m304_export_still_carries_its_items():
|
||||
# items at all.
|
||||
old = {"body": "shopping", "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
|
||||
that tell the two apart — pure functions, no request context, no database.
|
||||
"""
|
||||
from thoughtsync.proxy import forwarded_for, trusted_entry
|
||||
from thoughtsync.settings import live
|
||||
from inkwell.proxy import forwarded_for, trusted_entry
|
||||
from inkwell.settings import live
|
||||
|
||||
PEER = "10.0.0.1" # the socket address: our own proxy, or the caller when unproxied
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ import time
|
||||
|
||||
import pytest
|
||||
|
||||
from thoughtsync import ratelimit
|
||||
from thoughtsync.settings import live
|
||||
from thoughtsync.app import create_app
|
||||
from thoughtsync.ratelimit import SlidingWindow
|
||||
from inkwell import ratelimit
|
||||
from inkwell.settings import live
|
||||
from inkwell.app import create_app
|
||||
from inkwell.ratelimit import SlidingWindow
|
||||
|
||||
|
||||
def window(limit: int, window_s: float) -> SlidingWindow:
|
||||
|
||||
@@ -2,14 +2,14 @@ from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from thoughtsync.retention import (
|
||||
from inkwell.retention import (
|
||||
SWEEP_BATCH,
|
||||
SWEEP_INTERVAL_SECONDS,
|
||||
SWEEP_STARTUP_DELAY_SECONDS,
|
||||
expired_before,
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from thoughtsync.app import create_app
|
||||
from thoughtsync.saved_filters import clean_params
|
||||
from inkwell.app import create_app
|
||||
from inkwell.saved_filters import clean_params
|
||||
|
||||
|
||||
@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():
|
||||
|
||||
@@ -6,7 +6,7 @@ hook, and this suite has no Postgres.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from thoughtsync.app import create_app
|
||||
from inkwell.app import create_app
|
||||
|
||||
|
||||
@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():
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@ from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from thoughtsync.app import create_app
|
||||
from thoughtsync.sync import (
|
||||
from inkwell.app import create_app
|
||||
from inkwell.sync import (
|
||||
DEFAULT_LIMIT,
|
||||
MAX_LIMIT,
|
||||
MIN_CLIENT_PROTOCOL_VERSION,
|
||||
|
||||
@@ -2,8 +2,8 @@ import ipaddress
|
||||
|
||||
import pytest
|
||||
|
||||
from thoughtsync.app import create_app
|
||||
from thoughtsync.unfurl import UnfurlError, extract_preview, is_public_ip, validate_url
|
||||
from inkwell.app import create_app
|
||||
from inkwell.unfurl import UnfurlError, extract_preview, is_public_ip, validate_url
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
Reference in New Issue
Block a user