Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
cad05fe
feat(checklists): add setting to insert new items at the end of a che…
dawiddyd Aug 24, 2026
69b7e81
feat(i18n): translate new-item-insertion setting for all locales\n\nA…
dawiddyd Aug 24, 2026
a80b8a5
add mcp server for remote jotty access via REST API
dawiddyd Aug 25, 2026
3ebdb09
fix kanban assign/reminder API endpoints for API-key auth
dawiddyd Aug 25, 2026
87ea9a3
Merge pull request #595 from fccview/feature/new-item-insertion-switch
fccview Sep 27, 2026
975e24f
fix tests
fccview Sep 27, 2026
a5a4282
brain trust ftw
fccview Sep 27, 2026
f25790a
better look for links and better guides overall
fccview Sep 27, 2026
473d91f
slight re-style
fccview Sep 27, 2026
71f764b
rabbit nitpicks and redblood bug
fccview Sep 27, 2026
835099c
Merge pull request #615 from fccview/feature/brain
fccview Sep 27, 2026
4e10550
bugfixes spree, why not
fccview Sep 27, 2026
ddc6d7c
address bunny nitpicks so we can have a bugs free instance
fccview Sep 27, 2026
09058a2
one last fix
fccview Sep 27, 2026
994e207
Merge pull request #616 from fccview/bugfix/bugfixing-spree
fccview Sep 27, 2026
8f7354e
almost good to go!!
fccview Sep 27, 2026
e3f9764
bump the bastard
fccview Sep 27, 2026
3dc8fc4
Add JSDoc to page handlers, root layout, and Brain graph components
coderabbitai[bot] Sep 28, 2026
bdf0362
Fix conflicts and merge develop in
fccview Sep 28, 2026
fb0f66f
bit of a rehaul but the concept is the same
fccview Sep 28, 2026
272ed15
fix a bunch of constraints and issues with the api now that mcp is ha…
fccview Sep 28, 2026
cfedfc5
mcp should feel like magic when using it now
fccview Sep 28, 2026
799e52f
getting there sugarplum
fccview Sep 28, 2026
73cb5f5
fix bunny nitpicks
fccview Sep 28, 2026
bf984c0
Merge pull request #611 from fccview/feature/mcp-server
fccview Sep 28, 2026
296945a
make build for mcp
fccview Sep 28, 2026
13ef814
make it so the mcp only works from the right jotty version onward
fccview Sep 28, 2026
d578561
fix issues with the mcp
fccview Sep 28, 2026
08c9ef7
ok a lot of fixes were needed here, damn
fccview Sep 28, 2026
fc773fb
forgot typechecking ffs
fccview Sep 28, 2026
263e934
fix tags issue
fccview Sep 28, 2026
bf6df1f
little mcp changes
fccview Sep 28, 2026
062284a
some hardening
fccview Sep 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .agents/skills/jotty/references/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ OIDC redirect URI has to match the IdP exactly, scheme included.

## API keys

`authenticateApiKey` in `actions/api`. Header `x-api-key`. Keys are hashed at rest. Generating a new one is an audit event. See [api.md](api.md).
`authenticateApiKey` in `actions/api`. Header `x-api-key`. Keys are stored in plain text in `users.json` on purpose, so users can see theirs again in Profile. Generate them with `crypto.randomBytes` and compare them timing-safe, never with `===`. Never log one. See [api.md](api.md).

## Sanitising

Expand Down
23 changes: 14 additions & 9 deletions .agents/skills/jotty/references/persistence.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# File persistence

There is no database. A missed lock, a half-written file, or a loop that stats 800 notes is a real outage for a real person.
Your files are the database. The only other store is a derived SQLite index of relationships, which can always be rebuilt from them. A missed lock, a half-written file, or a loop that stats 800 notes is a real outage for a real person.

## Layout

Expand All @@ -11,12 +11,12 @@ data/
users/session-data.json
notes/<username>/ markdown notes in category folders
checklists/<username>/ markdown lists in category folders
notes/<username>/.index.json link index, keyed by uuid
<any folder>/.category-info.json folder uuid, sharing, order
notifications/<username>.json
encryption/<username>/ PGP key files, never the passphrase
logs/
.schema-version
.relations.db derived links index (SQLite, WAL). Disposable.
```

`.sharing.json` and `.order.json` are leftovers. Current code reads them during migration. New writes go to `.category-info.json`.
Expand All @@ -27,18 +27,19 @@ Schema version is `DATA_SCHEMA_VERSION` in `app/_consts/files.ts`. Old shapes st

Notes and checklists are both `.md` with YAML frontmatter. Checklists are not JSON. `noteToMarkdown` / `listToMarkdown` write them. Readers parse them.

Frontmatter carries `uuid`, `title`, timestamps, tags, encryption flags, checklist type. Body is markdown. Encrypted body is opaque ciphertext. Do not index it.
Frontmatter carries `uuid`, `title`, `createdAt`, tags, encryption flags, checklist type. Body is markdown. Encrypted body is opaque ciphertext. Do not index it.

## Helpers

`app/_server/actions/file/`:

- `ensureDir`
- `readJsonFile` / `writeJsonFile` (atomic temp + rename)
- `serverReadFile` / `serverWriteFile` / `serverDeleteFile`
- `serverReadFile` / `serverWriteFile` / `serverDeleteFile` / `serverDeleteDir` / `serverRenamePath`
- `getUserModeDir`
- `stampUuid` in `lib/stamp-uuid.ts` for giving an item file its uuid

`writeCatInfo` is also atomic. `serverWriteFile` is a straight write. Prefer the atomic helpers for JSON. Do not invent a third writer.
`serverWriteFile` is atomic (temp file + rename), stamps `createdAt` on item files that lack it, invalidates the metadata cache and updates the relations index. Delete, rename and move go through the helpers above for the same reasons. A raw `fs.writeFile` or `fs.rename` on an item file skips all of that. Do not invent another writer.

Paths: `path.join(process.cwd(), ...)`. Constants in `app/_consts/files.ts`. Never a relative `"data/..."` you hope is cwd.

Expand Down Expand Up @@ -67,11 +68,15 @@ Jotty is one Node process. Those in-process maps are enough until someone cluste

## Indexes

`.index.json` under notes is the **link** index, keyed by item uuid. Create, update, delete, and move must call `updateIndexForItem` / `removeItemFromIndex`. It does not rebuild itself.
`data/.relations.db` is the **relations** index, in `app/_server/actions/relations/`. It holds items, links between them, wikilink bindings and plain note text for "Mentioned in". It is derived and disposable: a missing, corrupt or old-schema file is discarded and rebuilt from the markdown on start, with the UI showing "Indexing relationships...". Bump `RELATIONS_SCHEMA_VERSION` when its shape changes. No migration, it just rebuilds.

Folder order and sharing live in `.category-info.json` (`order.items` is a uuid list).
- Writes keep it current through the file helpers (`trackItemWrite` and friends in `relations/tracking.ts`). Use the helpers and you get it for free.
- Changes made outside Jotty are caught by a recursive `fs.watch` on the notes and checklists roots, throttled to one pass a minute, which only stats the paths that changed. If the watcher can't start, reads fall back to a full mtime reconcile at most once a minute.
- Encrypted notes are indexed by uuid and title only. Their body is never parsed.
- `bindings` remembers which uuid each `[[wikilink]]` text first resolved to, per source note. That memory survives edits and rebuilds, and is lost only if the file is deleted.
- Queries are permission scoped through `visibleItems(username)`. Never return rows the viewer can't see.

If you add a write path and forget the index, search and the graph go stale with nothing in the logs.
Folder order and sharing live in `.category-info.json` (`order.items` is a uuid list).

## Path containment

Expand All @@ -83,4 +88,4 @@ Username is not a path segment you trust from the client either. Session usernam

## Data on this machine

The `data/` directory in a running instance holds real notes. Tests use mocks and temp dirs. `yarn mock:data:notes` / `yarn mock:data:lists` fill a named user for local poking. Do not empty, reshape, or "fix" `data/` to make a test pass.
The `data/` directory in a running instance holds real notes. Tests use mocks and temp dirs. `yarn mock:data:notes` / `yarn mock:data:lists` fill a named user for local poking. `yarn mock:data:brain --user=<name>` writes a linked set under a `Brain Seed` category, and `--remove` deletes only that category. Do not empty, reshape, or "fix" `data/` to make a test pass.
51 changes: 51 additions & 0 deletions .github/workflows/mcp-docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Build and Publish MCP

on:
push:
branches: ["main", "develop"]
tags: ["*"]

jobs:
build-mcp-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: The MCP is called jotty-mcp so we keep it jotty-mcp
id: repo
run: echo "name=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')-mcp" >> $GITHUB_OUTPUT

- name: Buildx baby
uses: docker/setup-buildx-action@v3

- name: Boring github login
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Same tags as jotty, so the versions line up
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ steps.repo.outputs.name }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}

- name: One small image, both arches, no emulation
uses: docker/build-push-action@v5
with:
context: mcp-server
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=mcp
cache-to: type=gha,mode=max,scope=mcp
30 changes: 29 additions & 1 deletion .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
- name: Setup the best engine ever
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "24"
cache: "yarn"

- name: Install all dependencies
Expand All @@ -58,3 +58,31 @@ jobs:

- name: Server actions also need to be tested
run: yarn test:run tests/server-actions --reporter=verbose

mcp:
name: The MCP server gets poked too
runs-on: ubuntu-latest
needs: validate-branch
defaults:
run:
working-directory: mcp-server
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Bun, because the MCP is fancy
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.14"

- name: Install MCP dependencies
run: bun install --frozen-lockfile

- name: Types or it didn't happen
run: bun run typecheck

- name: Fake Jotty, real HTTP
run: bun test

- name: Make sure it still bundles
run: bun run build
58 changes: 56 additions & 2 deletions .github/workflows/prebuild-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Setup the best engine ever
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '24'
cache: 'yarn'

- name: Install dependencies
Expand Down Expand Up @@ -52,9 +52,63 @@ jobs:
sha256sum jotty_${{ steps.version.outputs.version }}_prebuild.tar.gz > jotty_${{ steps.version.outputs.version }}_prebuild.tar.gz.sha256

- name: Attach to Release - pray it works
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
files: |
prebuild-release/jotty_*_prebuild.tar.gz
prebuild-release/jotty_*_prebuild.tar.gz.sha256
tag_name: ${{ steps.version.outputs.version }}

build-mcp:
runs-on: ubuntu-latest
permissions:
contents: write
defaults:
run:
working-directory: mcp-server
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup the other best engine ever
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Make sure it still works
run: |
bun run typecheck
bun test

- name: Bake the MCP
run: bun run build

- name: Get version from tag
id: version
run: |
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT

- name: Structure the prebuild stuff
run: |
mkdir -p prebuild-release/jotty-mcp
cp dist/main.js prebuild-release/jotty-mcp/main.js
cp README.md prebuild-release/jotty-mcp/README.md
cp ../LICENSE prebuild-release/jotty-mcp/LICENSE

- name: Create tarball - a smaller funny name
run: |
cd prebuild-release
tar -czf jotty-mcp_${{ steps.version.outputs.version }}_prebuild.tar.gz jotty-mcp
sha256sum jotty-mcp_${{ steps.version.outputs.version }}_prebuild.tar.gz > jotty-mcp_${{ steps.version.outputs.version }}_prebuild.tar.gz.sha256

- name: Attach to Release - pray it works twice
uses: softprops/action-gh-release@v2
with:
files: |
mcp-server/prebuild-release/jotty-mcp_*_prebuild.tar.gz
mcp-server/prebuild-release/jotty-mcp_*_prebuild.tar.gz.sha256
tag_name: ${{ steps.version.outputs.version }}
16 changes: 13 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Jotty

Jotty is a self-hosted app for notes, checklists and Kanban boards. There is no database. Every note, checklist, user and share is a file on a disk that the person running it owns.
Jotty is a self-hosted app for notes, checklists and Kanban boards. Every note, checklist, user and share is a file on a disk that the person running it owns. The one database, `data/.relations.db`, is a derived index of links. Deleting it loses nothing anybody wrote: it is rebuilt from those files. Never make it the only home of user data.

If we corrupt one of those files there is no restore button and nobody to email. There is a person and whatever backup they happened to take.

Expand All @@ -12,7 +12,7 @@ Read, change, write back on a shared file without a lock loses data. It has happ

A write that fails halfway leaves a truncated note where a note used to be. Use the existing file helpers, they write atomically.

Indexes do not rebuild themselves. Add a write path, miss the rebuild, and search goes stale with nothing in the logs to say so.
Indexes go stale quietly. The file helpers keep the caches and the relations index in step, so write through them. A raw `fs` write on an item file leaves search, backlinks and the brain wrong with nothing in the logs to say so.

Loops are expensive. One helper reading one item is fine. That same helper running once per item for a user with 800 notes is a stat storm. Caches and indexes already exist for this, look for them before you walk a directory yourself.

Expand Down Expand Up @@ -92,4 +92,14 @@ Same words for the same things, please.

Smallest thing that proves the change works. Type check, lint what you touched, run the tests covering the area, and the whole suite if you changed something shared.

The security tests cover auth, path containment and data leakage. If your change makes one fail, the change is wrong until proven otherwise. If you changed behaviour the tests cover, update them and tell me you did.
The security tests cover auth, path containment and data leakage. If your change makes one fail, the change is wrong until proven otherwise. If you changed behaviour the tests cover, update them and tell me you did.

<!-- BEGIN:nextjs-agent-rules -->

# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.

This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.

<!-- END:nextjs-agent-rules -->
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:20-alpine AS base
FROM node:24-alpine AS base

FROM base AS deps
RUN apk add --no-cache libc6-compat
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ My recommended way to run `jotty·page` is with Docker. You can also use:
- The [Proxmox community script](https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/jotty.sh) for Proxmox VE
- The [Unraid template](howto/UNRAID.md) for Unraid Community Applications

Running it without Docker, from source or from the prebuilt tarball, needs Node.js 22.15 or later in the 22.x series, or Node.js 23.11 or later.

<a id="docker-compose"></a>

### Docker Compose (Recommended)
Expand Down Expand Up @@ -186,6 +188,8 @@ I will always detail these migrations in the release notes. I _highly recommend_

📖 **For the complete MARKDOWN documentation, see [howto/MARKDOWN.md](howto/MARKDOWN.md)**

📖 **For links, wikilinks and the brain view, see [howto/BRAIN.md](howto/BRAIN.md)**

<a id="encryption"></a>

## ENCRYPTION
Expand Down
22 changes: 16 additions & 6 deletions app/(loggedInRoutes)/admin/checklist/[uuid]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { redirect } from "next/navigation";
import {
getListById,
getUserChecklists,
} from "@/app/_server/actions/checklist";
} from "@/app/_server/actions/checklist/queries";
import { getCategories } from "@/app/_server/actions/category";
import { getCurrentUser, canAccessAllContent } from "@/app/_server/actions/users";
import { ChecklistClient } from "@/app/_components/FeatureComponents/Checklists/Parts/ChecklistClient";
Expand All @@ -11,6 +11,8 @@ import type { Metadata } from "next";
import { getMedatadaTitle } from "@/app/_server/actions/config";
import { PermissionsProvider } from "@/app/_providers/PermissionsProvider";
import { MetadataProvider } from "@/app/_providers/MetadataProvider";
import { RelationsProvider } from "@/app/_providers/RelationsProvider";
import { getItemRelations } from "@/app/_server/actions/relations";

interface AdminChecklistPageProps {
params: Promise<{
Expand All @@ -26,6 +28,10 @@ export async function generateMetadata(props: AdminChecklistPageProps): Promise<
return getMedatadaTitle(Modes.CHECKLISTS, uuid);
}

/**
* Render a checklist with metadata and relations for a user with access to all content.
* Redirect home when access is denied or the checklist cannot be loaded.
*/
export default async function AdminChecklistPage(props: AdminChecklistPageProps) {
const params = await props.params;
const { uuid } = params;
Expand Down Expand Up @@ -67,14 +73,18 @@ export default async function AdminChecklistPage(props: AdminChecklistPageProps)
type: "checklist" as const,
};

const relations = await getItemRelations(checklist.uuid || "");

return (
<MetadataProvider metadata={metadata}>
<PermissionsProvider item={checklist}>
<ChecklistClient
checklist={checklist}
categories={categories}
user={user}
/>
<RelationsProvider relations={relations}>
<ChecklistClient
checklist={checklist}
categories={categories}
user={user}
/>
</RelationsProvider>
</PermissionsProvider>
</MetadataProvider>
);
Expand Down
Loading
Loading