6 Commits

Author SHA1 Message Date
cproudlock
a9289ee670 printedparts 0003 was an unguarded ALTER, and no test could see it
Unlike its twenty siblings it ran a bare add_column plus create_index, so
`flask plugin upgrade-all` failed with "duplicate column name: gagelabtag" on
any database whose tables were built by create_all and stamped nowhere - and a
chain stops at its first error, so 0004 never applied either.

The index is guarded by COLUMN SET rather than by name: create_all names indexes
by SQLAlchemy's convention and the migration by this repo's, so a name-keyed
guard would have added a second index over the same column instead of skipping.

The test that should have caught it was restricted to plugins whose whole chain
is a single baseline, on the reasoning that a longer chain meets a second
problem - an ALTER whose column is already present - that guarding a create_table
does not fix. True, but the conclusion was wrong: the answer is to guard the
ALTERs too, not to stop testing the chains that have them. It now covers every
post-cutover chain, and was confirmed to fail against the unguarded migration
before the guard went in.
2026-08-26 15:06:11 -04:00
cproudlock
b96637e1a1 Let a baseline meet a table that is already there
Some checks failed
CI / backend (push) Failing after 11s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 11s
CI / migrations-mysql (push) Failing after 7s
Reported: `flask plugin upgrade-all` failed the tools plugin with "table
securesendsecrets already exists".

Every post-cutover baseline emitted a plain op.create_table. That is correct
only while a table's existence and its plugin's Alembic stamp can never
disagree, and they can. Anything that builds tables from the models -
db.create_all(), which `flask db-utils create-all` calls - creates them and
stamps nothing. A run that creates a table and then fails before its stamp
commits leaves the same split, and re-running is the obvious thing to try. In
both cases upgrade-all meets existing tables with an unstamped chain and the
create is fatal.

Nothing caught it because the migration test DROPS the post-cutover tables
db.create_all() built before running upgrade-all. That arranges the one state
where an unguarded create works, and never the state the report came from. The
new test is the same sequence with the drop removed for the plugin under test.

create_table_if_missing and create_index_if_missing are in alembic_template.py
next to create_plugin_tables, which had the idempotency rule right and turns out
to be called by nobody - its docstring claimed every 0001 used it. Fixed the
docstring rather than rewiring the baselines onto it, because the reason they
hand-write their DDL still stands: the helper's per-plugin MetaData cannot
resolve a foreign key to a core table at CreateTable-compile time.

INDEXES ARE MATCHED ON COLUMNS, NOT NAME, and that distinction matters. The two
creators disagree about names - db.create_all() takes SQLAlchemy's default
convention (ix_securesendsecrets_token) and a baseline spells it the way this
repo's naming rules require (ixsecuresendsecretstoken). A name-keyed guard would
have called the index missing and added a second one over the same column, which
MySQL permits with a warning, so every affected site would have quietly grown
duplicates. The test asserts no table ends up with two indexes over the same
columns.

Applied to all nine table-creating plugin migrations, not just the tools
baseline that was reported: the four later migrations that create tables have
the same exposure, and geenforce, measuringtools, printedparts and backups are
the same defect waiting for a different site.

WHAT THIS DOES NOT FIX. It makes a baseline idempotent, not a whole chain. A
longer chain can still meet an ALTER whose column is already present -
printedparts adding gagelabtag to a table create_all built at head is the case
that showed up while testing. Guarding every op one at a time is the wrong
answer to that; a database built from the models is already AT head and its
chains want stamping rather than replaying, and there is no `flask plugin stamp`
today. Left alone deliberately rather than half-built, and the new test is
scoped to single-baseline plugins so it does not pretend otherwise.
2026-08-24 09:28:37 -04:00
cproudlock
baf6862151 printedparts: QR label with gage tag + revision; record taken rev
Label switches from CODE128 to a QR encoding 'TAG|rev' (gage lab tag + latest
print-file revision), so a physical part carries which revision it was printed
from - short payload stays low-version + reliable at 0.5in (margin quiet zone,
EC M, no logo). Item exposes latestrevision; kiosk strips the |rev to resolve
and records the scanned revision on the take (migration 0004 adds
printeditemtransactions.revision) for traceability of which rev was consumed.
Manual entry records a null revision.
2026-07-22 07:52:17 -04:00
cproudlock
3a3dff285e printedparts stage 17: gage-lab asset tag + print-files redesign
Some checks failed
CI / backend (push) Successful in 1m45s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 8s
The gage lab assigns real WJRP asset numbers, so identity splits: the
internal itemcode stays auto-minted and a new optional unique
gagelabtag (migration 0003) carries the lab's number - settable on
create/edit, searchable, and resolved by the kiosk for scans and bare
keypad digits against the numeric tail of either identifier
(unique-match only). The print-files table becomes stacked revision
cards - filename with rev/current badges, one meta line, delete pinned
right - ending the horizontal scroll in that column.
2026-07-17 14:01:09 -04:00
cproudlock
aa4bfcd41c printedparts stage 15: print-file revision history + role-based alerts
Some checks failed
CI / backend (push) Successful in 1m44s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 7s
printeditemfiles lands as the plugin's first incremental migration
(0002 on the plugin chain - the ADR-008 payoff). Revisions are
append-only per item: upload assigns the next number, records the
uploader from the JWT, enforces an extension allowlist and a 100 MB
cap; download serves the original filename; a permission-gated delete
covers wrong-file mistakes. The detail page gains the revision table
with a current badge. Unique storedfilename is sized 191 so the index
fits MySQL's 767-byte prefix - the per-plugin chain does not apply the
core env's ROW_FORMAT hook.

Alert recipients gain roles: Role joins the 0.13.0 surface, a role
picker on the settings page, and every active member of the selected
roles is folded into the deduped recipient list.
2026-07-17 08:47:41 -04:00
cproudlock
f5cfac33b4 printedparts stage 2: models, real 0001 baseline, tables live
Some checks failed
CI / backend (push) Successful in 1m39s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 8s
PrintedItem (catalog: code, name, image, cached quantityonhand,
per-item threshold, bin) and PrintedItemTransaction (the ledger:
signed quantity change attributed to a badge-resolved employee).
Both registered in PLUGIN_TABLE_OWNERS; 0001 is a post-cutover real
baseline. The migration-guard test learns the new expected head.
Routes are a placeholder ping until the next stage - the scaffold's
list route imported the deleted scaffold model, which surfaces as an
empty 'Migration error' because the alembic env imports the models
package.
2026-07-16 16:57:21 -04:00