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.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
*.log
|
||||
@@ -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 <the files you touched>
|
||||
```
|
||||
|
||||
A change in `api/` or `web/` needs `pm2 restart officer`. A manifest change needs it too — manifests are
|
||||
read once per server process.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 }));
|
||||
+40
@@ -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",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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 (
|
||||
<div className="h-full overflow-auto p-6">
|
||||
<h2 className="text-lg font-semibold text-duck-dark">Detail</h2>
|
||||
<p className="mt-1 text-sm text-duck-dark/60">
|
||||
Section from the URL: <code>{section ?? "(none)"}</code>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<div className="h-full overflow-auto p-6">
|
||||
<h2 className="text-lg font-semibold text-duck-dark">Finances</h2>
|
||||
<p className="mt-1 text-sm text-duck-dark/60">
|
||||
Nothing here yet — the skeleton is up and mounted.
|
||||
</p>
|
||||
<div className="mt-4 rounded-md border border-duck-dark/10 bg-duck-dark/[0.02] p-3 font-mono text-xs">
|
||||
<div className="mb-1 text-duck-dark/50">GET /api/finances/ping</div>
|
||||
{isLoading ? (
|
||||
<span className="text-duck-dark/40">…</span>
|
||||
) : (
|
||||
<span>{JSON.stringify(data)}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user