Publishing a local row to community never linked the row back to the
resulting post, so: no community pill, no update-available tracking,
and the re-publish version-bump flow was unreachable (origin slug was
always null -> publishMode could only ever be 'create').
Adds the writeback path:
- ModuleRepository / BundleRepository.set_community_origin(id, slug,
version): stamps community_post_slug + community_version_number
WITHOUT a version bump or fingerprint change -- origin metadata is
not runtime-affecting, so a published row stays byte-identical to
what consumers installed.
- wp_api: POST /wp/api/modules/{id}/community-origin + the bundles
twin. Validates {post_slug: str, version_number: int}.
- api client: modules.setCommunityOrigin + bundles.setCommunityOrigin.
- main.ts host bridge: new optional `markPublished(localId, kind,
slug, version)` method. Calls the right setCommunityOrigin, then
refreshes both catalogs so the pill appears without a reload.
Optional + try/catch-returns-false so a writeback miss never breaks
the embed's publish-success path. No ABI bump -- adding a method is
non-breaking; the embed guards on its presence.
Tests: set_community_origin stamps slug+version, does NOT bump
version/fingerprint, raises on missing row.
Pairs with the community embed commit that calls markPublished after
createPost / createVersion.
Adds `content_rating TEXT NOT NULL DEFAULT 'safe'` to modules + bundles.
Defaults all pre-015 rows to 'safe'. Repository create/update accept a
new content_rating kwarg with validation against {'safe', 'nsfw'}.
Importer _insert_module / _insert_bundle pass entity.content_rating
through; _update_module / _update_bundle use COALESCE(?, content_rating)
so an install path that omits the key preserves the existing flag.
Why: user wants two NSFW behaviors:
1. Edit page toggle to mark a module/bundle as NSFW locally.
2. Community posts with content_rating='nsfw' auto-stamp NSFW on
the local row at install time.
This is the engine half of that work -- the schema, the data
plumbing, and validation. The SPA half (IdentityCard toggle + pill
render on ModuleListView + install.ts plumbing) lands next.
Tests cover: migration head bump to 15, column existence on both
tables, safe default backfill, accepts nsfw, rejects invalid, update
toggle round-trip, update without content_rating preserves prior.
Implements spec §4 Flow 4: bulk-at-boot is NOT a parallel forward-chain
implementation; it's eager iteration of lazy_migrate_row over every
stale row. Same original-first logic, same re-projection, zero
divergence. Eliminates the transient-wrong window where bulk could
write fabricated defaults over recoverable data in original_payload_json.
run_bulk_lazy returns the failure count; caller surfaces a dashboard
banner when > 0. STALE_PREDICATE catches both 'sv < current' AND
'tolerantly-installed row whose original now outranks the projection'.
Migration 014 invokes run_bulk_lazy at the end of up() — no-op pass
at v1, but validates the wiring is correct. Future bump migrations
(015 = v2, etc) will reuse the same helper.
Implements spec §4 Flow 5 in full:
- No-op when row is at CURRENT with no future original
- NULL-drop optimization when row.sv == original.sv == CURRENT
- Recovery exact catch-up: promote original into payload (the §4 bug fix —
write-back gate is 'base_v < CURRENT OR base IS original', not just
'base_v < CURRENT'; otherwise exact catch-up silently no-ops)
- Still-drifted re-project: tolerantly strip original down to CURRENT
- Deferred strict-validate failure → tolerant_drift_status='broken'
- Standard forward chain otherwise
original_payload_json is inert storage: two writers only (initial
tolerant install + this NULL-drop). No other path rewrites it.
schema_migrated_at is separate from updated_at (structural migration
is not a user edit).
Adds the four payload-versioning columns to modules + bundles per
the community-web spec sections 4-5:
- schema_version (NOT NULL DEFAULT 1)
- original_payload_json (NULL - only set during tolerant install)
- tolerant_drift_status ('none' | 'pending' | 'broken')
- schema_migrated_at (separate from updated_at - structural migration
is not a user edit)
Backfill is implicit via the DEFAULT on the NOT NULL columns.
Library rows installed from the community embed now carry the source
post slug + installed version_number in the engine sqlite. The SPA
uses both surfaces: a "community" pill next to the row name (so users
can tell at a glance which entries were authored locally vs pulled
from the embed), and a warn-coloured "v{N}" pill when the post has
shipped a newer version since install.
Engine:
- Migration 013 adds community_post_slug + community_version_number
to modules + bundles. Both nullable; locally-authored rows stay
NULL forever. Implemented as a .py migration with PRAGMA
table_info guards so rerunning against an already-migrated DB
(test fixtures rewind migrations to exercise older paths) is a
clean no-op rather than "duplicate column name".
- _insert_module / _insert_bundle (importer.py) persist the fields
when the entity dict carries them.
- _row_to_module / _row_to_bundle (repositories.py) surface them on
every read path. Try/except guard for pre-013 DB shape so a
connection opened mid-rollout does not crash.
Manager:
- installEnvelope grows optional `origin: { post_slug, version_number }`
in InstallOptions. When set, stamps every module + bundle in the
selection before the commit roundtrip.
- Host bridge in main.ts threads origin from `install(envelope, opts)`
through to installEnvelope. ABI stays at 1 (additive).
- New communityUpdateStore walks the library snapshot, batch-fetches
each unique community post (deduped, 8-wide concurrency), surfaces
rows whose installed version is behind the post's
latest_version_number. Network failures clear the store silently
so an offline session does not render misleading "v2 available"
pills.
- AppLayout kicks one check after the initial library fetch
resolves. No background polling.
- ModuleListView renders two distinct pills per row: a static
"community" indicator on origin-stamped rows, and a separate
warn-coloured "v{N}" pill when the update store has an entry.
Migration tests updated for the new schema head + bundle/module
column sets.
Introduces a persisted prompt-template library: migration 012 creates the
templates table (name/description/category_id FK/tags/is_favorite/
template_string), TemplateRepository provides CRUD with partial-update
sentinel semantics, and wp_api/templates.py exposes list/get/create/update/
delete/favorite with FK violations mapped to 400. Bumps the migration-head
version assertions to 12.
Migration 006 added the snapshot_fingerprint column but only populated
it on subsequent writes — every pre-006 row stayed NULL. The collision
detector correctly handles NULL as `exists-unknown` (amber EXISTING
badge), but a freshly exported-then-reimported untouched row still
showed EXISTING when content was byte-identical. Backfilling on first
open turns those into silent-skip (no badge), removing the visual noise.
Pure read+UPDATE — no schema change, no version bump on rows.
Adds engine/importer.py exposing commit_import / get_undo_entry /
undo_import. The committer applies a classified adds/replaces/renames
payload inside a single sqlite transaction, dispatching by `kind` to
the modules table (5 module types), bundles, or module_categories.
Module writes recompute snapshot_fingerprint; bundle writes recompute
payload_hash; categories use case-insensitive name-based merge
(matching the legacy import_bundle handler).
Every successful commit persists an import_undo row with enough
metadata (imported_records, replaced_snapshots, rename_map) to
reverse the commit. undo_import restores replaced rows bit-for-bit
(preserving original version + payload_hash + timestamps) and
deletes inserted rows, then evicts the undo entry.
Migration 007 creates the import_undo metadata table. Adds
17 importer tests covering adds across all 7 buckets, replace
snapshotting, rename with old→new mapping, collision rollback,
and undo restore/delete paths.
Pipelines never paid off as a separate module kind, so peel out:
- Engine: drop pipeline_handler, remove "pipeline" from valid module
types in repositories + schema parity.
- Manager: delete Pipelines/PipelineEditor views, PipelineSteps +
PipelineStepPicker components and their tests. Router, sidebar,
topbar, dashboard, TestRunner, ImportExport, resolver and
bundleSelection repointed off the removed routes/types.
- wp_api: drop pipeline branch from modules.py + import_export.py
(and its test).
In its place, add a unified library view:
- src/manager/views/AllItems.vue — modules + bundles in one filter
panel with kind multi-select (OR across the 6 kinds). Reachable
via the Dashboard "View all" link.
- ModuleListView grew the slots/props AllItems needs; BundleAddChild
+ Dashboard + ImportExport updated alongside.
Misc polish along the way: kind-icons / theme tokens / display-prefs
touch-ups, ContextWidget + DebugViewer + AssemblerHelper small fixes,
MockModuleCard + ModulePickerModal tweaks, _shared widget cleanup.
Resolution order: WP_DB_PATH env > COMFYUI_USER_DIR env >
folder_paths.get_user_directory() > parents[4]/user path-traversal >
legacy ~/.comfyui. When the new ComfyUI-user location has no DB but
the legacy file exists, return legacy so existing installs keep data.
Tests short-circuit the chain via WP_DB_PATH.
Dashboard count cards displayed "1" for every kind regardless of how
many rows the user actually had in the library. Root cause: the list
endpoint returned `total = len(items)` AFTER pagination, so any client
that passed `limit=1` for cheap polling (Dashboard does exactly this)
got `total=1` no matter what.
Adds `ModuleRepository.count(...)` that runs the same WHERE-clause
filters as `list()` but issues `SELECT COUNT(*)` with no LIMIT/OFFSET,
and reroutes both calls through a shared `_build_filter_clause` helper
so filter shape can never drift between the two paths.
Pinned by `test_list_total_ignores_limit` — seeds 3 wildcards, queries
`?type=wildcard&limit=1`, asserts items has 1 row but total is 3.
- Bump _gen_id suffix from token_hex(2) to token_hex(4) for 32-bit collision resistance
- Escape LIKE wildcards (%, _, \) in list(query=...) with ESCAPE clause
- Switch _now() and migrations.py to millisecond-precision ISO 8601 timestamps
- Add _Unset typed sentinel replacing object-typed ... default on update()
- Support offset without limit via LIMIT -1 OFFSET ? in list()
- Remove time.sleep() calls from module repository tests (ms precision makes them unnecessary)
- Add 3 new tests: percent/underscore literal search, offset-without-limit