commit 6e88d849e7c0cc1ce393f0ecb7f6a83d59f15e68 Author: André Padez Date: Thu Aug 27 17:28:00 2026 +0100 the finances skeleton — repo, prefix and permission key, before any feature Published empty on purpose: identity is the expensive part to change later, the feature is the cheap part to add. One route, two placeholder panels, no tables and no sidecar. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..552f221 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +*.log diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..68ce727 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,40 @@ +# AGENTS.md + +Guidance for a coding agent working on **this plugin's own repository**. `CLAUDE.md` beside it only +points here — the two harnesses discover different filenames and one set of instructions has to reach +both. + +**This is not what the plugin is.** `README.md` answers that. This file answers "I have to change +something in here — what will I break?" + +## Where you are + +A plugin repository, cloned into `~/officer-dev/plugins/finances`, its own git remote. It is **not** part +of the platform's repository, and `platform/plugins/finances` is a different thing — that is where this +plugin gets INSTALLED, by the installer cloning it from gitea. Never edit there; the next install +overwrites it. + +## What must not change without knowing why + +- **Permission key `finances`** — stored as the subject of every grant. Renaming it is a data change. +- **Panel keys** (`finances-overview`, `finances-detail`) — a user's stored layout names them. +- **`appName`** — the directory name IS the id: route segment, table prefix, sidecar suffix, install key. + +## Adding tables + +There are none yet. When there are, `db/schema.ts`, every table name prefixed `finances_`, and +`bun db:push` from the platform repo. Uninstall never drops a table; deleting this directory and running +`db:push` does. + +## Verifying + +From the platform repo, which is where the toolchain lives: + +```bash +bunx tsgo +bun run plugins:check +bunx prettier --write +``` + +A change in `api/` or `web/` needs `pm2 restart officer`. A manifest change needs it too — manifests are +read once per server process. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8c36885 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,6 @@ +# CLAUDE.md + +**The instructions live in [`AGENTS.md`](AGENTS.md). Read it first — this file only points there.** + +Kept separate so agents that look for `AGENTS.md` by convention find the same guidance as those that +look for `CLAUDE.md`, without the two drifting apart. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d7441e --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Finances + +Money, on your own machine. + +**This is a skeleton.** The repository, the mount prefix (`/finances`, `/api/finances`) and the permission +key (`finances`) are real and settled. What the plugin actually does is not written yet — the two panels +render a placeholder and the one route answers `GET /api/finances/ping`. + +Published in this state on purpose: the identity of a plugin is the part that is expensive to change +later, and the feature is the part that is cheap to add. + +## What it ships + +| Part | Present | Note | +| ------------------ | ------- | ------------------------------------------------------------ | +| `manifest.ts` | yes | identity, one permission | +| `api/router.ts` | yes | one route, mounted at `/api/finances` | +| `web/` | yes | two placeholder panels and a layout | +| `db/schema.ts` | no | no tables until the data model is settled | +| `sidecar/` | no | nothing long-lived yet | +| `assets/icon.png` | no | the dock falls back to the manifest's lucide `Wallet` | + +## Installing + +It is on disk and not installed until you install it from `/plugins`. Being in the tree makes it appear +on that page; it mounts nothing until then. + +## Further reading + +`plugins/example` in the platform repo is the reference plugin and documents every convention this one +follows. diff --git a/api/router.ts b/api/router.ts new file mode 100644 index 0000000..149fc81 --- /dev/null +++ b/api/router.ts @@ -0,0 +1,6 @@ +import { createRouter } from "@@/create-router"; + +/** Mounted at `/api/finances`. The prefix comes from the manifest's `publisher`; nothing here sets it. */ +export const router = createRouter(); + +router.get("/ping", (ctx) => ctx.json({ plugin: "finances", ok: true })); diff --git a/manifest.ts b/manifest.ts new file mode 100644 index 0000000..1692e26 --- /dev/null +++ b/manifest.ts @@ -0,0 +1,40 @@ +import type { PluginManifest } from "@@/plugins/manifest"; + +/** + * Finances. + * + * The skeleton, published before the feature so that the repository, the mount prefix and the permission + * key exist and are stable while the rest is still being decided. + * + * What it does NOT ship yet, and why each is an absence rather than a stub: + * + * db/ no tables until the data model is settled. A table added is easy; a table renamed after + * someone has rows in it is not. + * sidecar/ no process. Nothing here is long-lived yet — an importer or a rates poller would be. + */ +export const manifest: PluginManifest = { + publisher: "officerdev", + version: "2026.08.27.01", + platform: ">=1.0.0", + + label: "Finances", + summary: "Money, on your own machine.", + tags: ["Personal"], + icon: "Wallet", + color: "#059669", + + /** + * One key gating the whole surface. + * + * Finances is the kind of data where "read" is already a strong grant, so a second key splitting + * read from write would say less than it looks like it says. One, until there is a surface that + * genuinely divides. + */ + permissions: [ + { + key: "finances", + label: "Finances", + description: "Accounts, balances and transactions", + }, + ], +}; diff --git a/web/FinancesDetail.tsx b/web/FinancesDetail.tsx new file mode 100644 index 0000000..f957532 --- /dev/null +++ b/web/FinancesDetail.tsx @@ -0,0 +1,15 @@ +import { useParams } from "react-router"; + +// The second panel, reading the section from the URL rather than from the panel beside it. +export const FinancesDetail = () => { + const { section } = useParams(); + + return ( +
+

Detail

+

+ Section from the URL: {section ?? "(none)"} +

+
+ ); +}; diff --git a/web/FinancesOverview.tsx b/web/FinancesOverview.tsx new file mode 100644 index 0000000..1a20194 --- /dev/null +++ b/web/FinancesOverview.tsx @@ -0,0 +1,29 @@ +import { useClient } from "hooks/useClient"; +import { useQuery } from "@tanstack/react-query"; + +// Placeholder. It proves the route mounts and the panel renders, and nothing else. +export const FinancesOverview = () => { + const client = useClient(); + const { data, isLoading } = useQuery({ + queryKey: ["finances", "ping"], + queryFn: () => + client.get<{ plugin: string; ok: boolean }>("/finances/ping"), + }); + + return ( +
+

Finances

+

+ Nothing here yet — the skeleton is up and mounted. +

+
+
GET /api/finances/ping
+ {isLoading ? ( + … + ) : ( + {JSON.stringify(data)} + )} +
+
+ ); +}; diff --git a/web/layout.ts b/web/layout.ts new file mode 100644 index 0000000..526544c --- /dev/null +++ b/web/layout.ts @@ -0,0 +1,29 @@ +import type { LayoutNode } from "officerdev"; + +// The starting arrangement. The shell persists whatever the user makes of it, per plugin. +// +// Every `appType` must be a key from `panels.ts` — a mismatch falls back silently instead of rendering +// what was meant. +export const defaultLayout: LayoutNode = { + type: "group", + id: "finances-root", + direction: "horizontal", + children: [ + { + node: { + type: "panel", + id: "finances-overview", + appType: "finances-overview", + }, + size: 40, + }, + { + node: { + type: "panel", + id: "finances-detail", + appType: "finances-detail", + }, + size: 60, + }, + ], +}; diff --git a/web/panels.ts b/web/panels.ts new file mode 100644 index 0000000..b5feb59 --- /dev/null +++ b/web/panels.ts @@ -0,0 +1,24 @@ +import { Wallet, ListTree } from "lucide-react"; +import type { AppRegistryMeta } from "officerdev"; +import { FinancesOverview } from "./FinancesOverview"; +import { FinancesDetail } from "./FinancesDetail"; + +// The panels this plugin contributes. The shell renders the screen around them; this plugin never does. +// +// These keys are stored in a user's saved layout, so renaming one is a data change rather than a rename. +export const appRegistryMetas: AppRegistryMeta[] = [ + { + key: "finances-overview", + name: "Overview", + icon: Wallet, + component: FinancesOverview, + availableOnPanel: false, + }, + { + key: "finances-detail", + name: "Detail", + icon: ListTree, + component: FinancesDetail, + availableOnPanel: false, + }, +];