Files
platform/docs/marketplace-todo.md
pastilhasandClaude Opus 5 19bbbab907 publishing a plugin is pushing a tag
Three mechanisms where there should be one. The catalogue was filled by hand, and each host separately
polled twenty-one repositories with git to work out whether a release existed — a second source of
truth for a fact one place already knew. Shipping photos 2026.09.03.02 correctly and watching alpha
not notice for ten minutes is what started this.

The timer. scripts/marketplace-cron.sh, every minute, on the machine that publishes. Safe now only
because the previous commit made the sync read tags instead of working trees; over the old one this
would have published somebody's afternoon on a schedule. Nothing is written unless the release commit
differs from the one already published, so a tick with nothing to do is one query, one fetch per repo
and no writes — about 2.7s. One timestamped summary line per tick regardless, because a fresh timestamp
is the only thing that separates "nothing to publish" from "the timer died"; a named line only for a
plugin that moved.

The badge. `updateable` now compares the tag at HEAD with the tag the catalogue says is newest —
difference, not ordering, since parsing 2026.08.27.01 works right up until somebody tags v2 and then
fails silently. refreshRemote() and its two-minute window are gone with the polling they existed to
paper over. The git rule stays as the fallback for a plugin the store cannot speak for: an unregistered
checkout, or an unreachable store. That case now reads whatever refs the last pull left, which is the
accepted cost of not making every machine poll every repository forever.

fetchCatalogue()'s cache went from five minutes to one. It only drew tiles before; now the wait for a
release to appear would have been the sync's minute plus its five.

Also: the Update button names its destination whenever a release is known, not only on `new-release`.
An untagged checkout reports `untagged` and used to lose the tag with it, so the button said "Update"
and the owner had to guess where it landed — the marketplace knows, even for a checkout that has never
fetched it.

Verified: forced a stale row, the timer republished it inside 60s and logged one line. Badge decisions
checked against the live catalogue for seven plugins, including the two shapes that matter — at a tag
the store agrees with, and untagged with a release known only to the store. 9 release tests pass, 100%
of release.ts covered, four of them new and all against real repositories.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 13:13:43 +01:00

75 lines
4.2 KiB
Markdown

# Marketplace — deferred work
What `marketplace.officer.dev` and the plugin install path do not do yet, as of 2026-08-16. Design and
rationale live in `docs/marketplace.md`; this is the list.
Ranked by what actually blocks something.
## Blocks other people using it
- [ ] **Third-party submissions cannot use the sync.** `scripts/marketplace-sync.ts` reads a manifest by
IMPORTING it, which is code execution. Fine for our own repositories, not a mechanism a stranger's
plugin can go through. Needs either a sandbox or a published `manifest.json` alongside the
TypeScript one. Until this exists the catalogue is ours only, and everything below is cosmetic.
- [ ] **No write API and no submission flow.** Registering a plugin means running the sync by hand against
the database. Deliberate while nobody can submit; the first outside contributor makes it the gap.
## Blocks a plugin evolving
- [ ] **Plugin installs CREATE, they do not MIGRATE.** `pushPluginSchema` generates the SQL that builds a
schema from nothing and applies it, skipping objects that already exist. A new version that adds a
column will not get it — the CREATE is skipped as "already exists" and the column is silently
absent. Plugin upgrades need real migrations. The old global `db:push` appeared to handle this, at
the price of being able to drop half the database, so this is a known trade rather than a
regression.
- [ ] **No versions table.** `plugin_metadata` is one row per plugin — a snapshot, not a history. A store
that can offer "install 1.2 instead of 1.3", or show a changelog, needs a different shape.
## Staleness
- [x] **Nothing refreshes the catalogue automatically.** Done 2026-09-03: `scripts/marketplace-cron.sh`
every minute. A Gitea webhook would make it a second instead, at the price of a public write route
on the one process here with no login — not worth it for a minute.
- [x] **The sync reads local clones, not the remotes.** Done 2026-09-03. Bare mirrors, read at the newest
tag, driven by the registry rather than by a directory listing. The catalogue no longer describes
any machine's working copies.
- [ ] **`rawUrl()` assumes Gitea.** A GitHub-hosted plugin gets a 404 for its icon. The forge belongs in
the registry rather than inferred from a URL shape. (The `main`-vs-`master` half of this is gone —
URLs pin to the release tag now.)
- [ ] **Four plugins have never been tagged**, so the store still shows rows the old sync took from a
working tree: av-club, kanban, openseo, fofos-admin. They are frozen until somebody tags them, and
the sync names them on every run somebody asks for.
## Scale
- [ ] **`fetchCatalogue()` fetches everything, every time.** ~737 bytes per entry, so 100 plugins ≈ 72KB
and 1,000 ≈ 720KB. Correct at three; the fix at that size is search rather than pagination, and
search is a server-side query that changes the shape anyway — so this is deliberately not built
early. The seam is `fetchCatalogue()` itself, which already returns a promise with three fallback
paths behind it.
- [ ] **The platform fetches the whole catalogue to answer "what can I install", then discards everything
already on disk.** Fine at three, wasteful at 300. Moving that filter server-side means the platform
telling the store what it has, which would be the first data sent TO the marketplace — worth doing
deliberately rather than by accident.
- [ ] **No search, no categories, no install counts.** Three plugins do not need them.
## Smaller
- [ ] **A plugin's own icon is invisible before install.** `assets/` is published to
`public/plugins/<app>/` on mount, which only happens once it is installed; the catalogue's `iconUrl`
works around it by pointing at the repository, and a plugin author has to know to add one.
- [ ] **`git`, `pm2` and `sudo` are still PATH lookups** in the install path. They work on both machines we
have, and `bunx` did not — see `dc420ba5`. Anything new should follow `pushPluginSchema`'s pattern
of resolving a path rather than trusting `$PATH`.
- [ ] **Uninstall leaves the ecosystem file's PM2 entry removed but the process's logs behind.** Harmless,
unexamined.