T5.4 - CR-005: a per-project location taxonomy, stored as codes
CLAUDE.md lists CR-005 among the change requests that get "silently half-built if
you treat them as frontend-only". This is the server half and the wizard half
together: a new table, four routes, an Alembic revision, and step 11.
CODES, NOT DISPLAY STRINGS, because CR-018 rolls cost up by these values and a
rollup keyed on a label breaks the day somebody fixes a typo in it. Two columns
carry that: `code` is a node's own slug, derived once at import and never
recomputed; `path` is the full slug path, unique per project, and is what a work
package will store. Renaming a value changes `name` alone - the probe renames a
floor and demands its path comes back byte-identical, with its children's paths
intact.
DEACTIVATE, NEVER DELETE. There is no DELETE route, and the probe checks for its
absence (405) rather than trusting that nobody added one. Deactivating hides a
value from new work packages and cascades DOWN, because a floor nobody can pick
must not keep offering its sectors. Reactivating walks UP only - a sector may
have been switched off for its own reasons, and silently resurrecting it would
undo a decision nobody made twice. That asymmetry is deliberate and is pinned by
a named check so it does not get "fixed" into a surprise.
Import reports rather than merges. Rejected rows come back with the SOURCE line
number and a reason; duplicates are listed as duplicates, separated into "already
in this project" and "already on line N of this import". Reusing a parent is not
a duplicate - B1/L2/1P and B1/L2/2P share a building and a floor by design, and
only the full path repeating counts. Re-importing a deactivated value brings the
same row back rather than creating a second one; the probe checks the id.
One parser, on the server. A CSV is read in the browser and posted as text
exactly as a paste is, so "what does a blank column mean" has one answer.
Comma, semicolon and tab all work - a paste out of a spreadsheet is tab
separated and a saved CSV is not, and which one somebody has is a question the
machine can answer.
No guessed floor names. IMPLEMENTATION.md section 8 says the B100 list has not
been supplied. The seeded sample has "Sample" inside every string, and the probe
greps html/ and server/ for a location-shaped assignment containing any of the
review's real names.
server/models.py LocationNode
server/alembic/versions/e2a4c7d91b30_location_taxonomy.py
server/app.py GET/POST/PATCH + import, parser, slug
html/work-package-suite.html step 11, an 11th rail button
html/work-package-suite-app.js the step's logic; LAST_STEP replaces 10
html/work-package-suite-styles.css the list, the report
html/theme-light.css .field-error, now declared once
tests/locations_check.py new - 58 checks
tests/stepper_check.py STEP_COUNT 10 -> 11
Done when
[x] CSV upload and paste both work and report rejected rows with reasons
[x] duplicates are detected and reported rather than silently merged
[x] values are editable after import - rename, add, deactivate
[x] deactivating hides it from new work packages; an existing package
referencing it still resolves, because the row is retained
[x] values are stored as codes suitable for grouping
[x] no guessed real-world floor names exist anywhere in the code
Two decisions worth disagreeing with
Step 11, appended, not step 2, inserted. Locations belong beside Project by
subject. Renumbering 2-10 would touch every sop-step-N id, every
collectStepData case, every gate key and the analytics history - a large
silent-mismatch surface for an ordering change. The count now lives in one
place (LAST_STEP), so reordering later is cheap.
Any project member may edit the list, not only a Project Admin. It matches how
the SOP baseline itself is authored: the Project Admin gate is on CHANGING a
completed SOP, not on writing one. If the location list should be tighter than
the SOP it belongs to, that is a product call.
Verified one at a time
locations_check 58/58 new
stepper_check 70/70 (11 steps)
browser_check 71/71
a11y 22/22 sop now rings 38 focusable elements
url_state 23/23
autosave 34/34
aggregates 16/16
pipeline 43/43
launcher 58/58
f_items F1-F5 FIXED, F6 REPRODUCES (T7.2)
alembic upgrade / downgrade / upgrade all clean on a throwaway SQLite
file, and the migrated schema matches Base.metadata.create_all
column for column - dev auto-creates and production migrates,
so a divergence between the two is invisible until it ships
.field-error was declared in two page sheets by the end of T5.2 and would have
been three by T5.8, so it moved to theme-light.css. No colour literal added
anywhere: still 0 across all page sheets and inline blocks.
Question for the PR, per CLAUDE.md: the levels are fixed at building / floor /
sector. Micron's floors behave like buildings, which this handles by letting a
project use whichever levels it needs - but a job that wants a fourth level, or
different names for the three, cannot say so. Whether that is worth a
per-project level vocabulary is a product question; the schema would take it
without a migration.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -84,7 +84,7 @@
|
||||
where-you-are and opens on demand. -->
|
||||
<button type="button" class="step-rail-toggle" id="step-rail-toggle"
|
||||
aria-expanded="false" aria-controls="step-rail-list">
|
||||
<span class="step-rail-toggle-text">Step <span id="step-rail-pos">1</span> of 10 · <span id="step-rail-here">Project</span></span>
|
||||
<span class="step-rail-toggle-text">Step <span id="step-rail-pos">1</span> of <span id="step-rail-total">11</span> · <span id="step-rail-here">Project</span></span>
|
||||
<span class="step-rail-toggle-chev" aria-hidden="true">▾</span>
|
||||
</button>
|
||||
<ol class="step-rail-list" id="step-rail-list">
|
||||
@@ -98,6 +98,13 @@
|
||||
<li class="step-rail-item"><button type="button" class="step-btn" data-step="8"><span class="step-btn-marker" aria-hidden="true">8</span><span class="step-btn-body"><span class="step-btn-label">Sequence</span><span class="step-btn-state"></span></span></button></li>
|
||||
<li class="step-rail-item"><button type="button" class="step-btn" data-step="9"><span class="step-btn-marker" aria-hidden="true">9</span><span class="step-btn-body"><span class="step-btn-label">Constraints</span><span class="step-btn-state"></span></span></button></li>
|
||||
<li class="step-rail-item"><button type="button" class="step-btn" data-step="10"><span class="step-btn-marker" aria-hidden="true">10</span><span class="step-btn-body"><span class="step-btn-label">Sources</span><span class="step-btn-state"></span></span></button></li>
|
||||
<!-- CR-005. Appended rather than inserted next to Project, which is where
|
||||
it belongs by subject: renumbering steps 2-10 would touch every
|
||||
sop-step-N id, every collectStepData case, every gate key and the
|
||||
analytics history, for an ordering change. Numbers are cheap to
|
||||
reorder once nothing depends on them; a renumbering buried in a
|
||||
feature diff is not. -->
|
||||
<li class="step-rail-item"><button type="button" class="step-btn" data-step="11"><span class="step-btn-marker" aria-hidden="true">11</span><span class="step-btn-body"><span class="step-btn-label">Locations</span><span class="step-btn-state"></span></span></button></li>
|
||||
</ol>
|
||||
<!-- Why a step you asked for did not open. role="alert" because a refused
|
||||
navigation is an error and waiting for a pause to say so is too late
|
||||
@@ -385,6 +392,61 @@
|
||||
<button class="add-btn" onclick="addSource()">+ Add source</button>
|
||||
</div>
|
||||
|
||||
<!-- STEP 11: LOCATIONS (CR-005)
|
||||
Server-backed, not localStorage: CR-018 rolls cost up by these
|
||||
values, and a taxonomy each browser keeps its own copy of cannot
|
||||
be rolled up by anything. See CLAUDE.md, "Frontend and backend
|
||||
boundary". -->
|
||||
<div class="step" id="sop-step-11" style="display: none;">
|
||||
<h2>11. Location List</h2>
|
||||
<div class="notice">Building, floor and sector for this project. The shape of a
|
||||
location differs per job — on some, floors within one building behave like
|
||||
separate buildings and are the unit of both execution and cost tracking — so it
|
||||
is configured here rather than assumed. Work packages pick from this list, and
|
||||
cost rolls up by it.</div>
|
||||
|
||||
<div id="loc-noproject" class="notice" style="display:none; background:var(--warning-bg); color:var(--warning);"></div>
|
||||
|
||||
<div id="loc-tool">
|
||||
<div class="field-grid col1">
|
||||
<div class="field">
|
||||
<label for="loc-paste">Paste rows, or upload a CSV</label>
|
||||
<textarea id="loc-paste" rows="6" aria-describedby="loc-paste-hint"
|
||||
placeholder="One row per sector, e.g. Building, Floor, Sector"></textarea>
|
||||
<small id="loc-paste-hint">One row per value, deepest level last:
|
||||
<strong>building, floor, sector</strong>. Two columns describe a floor,
|
||||
one describes a building. Comma, semicolon or tab separated — a paste
|
||||
straight out of a spreadsheet works. A header row is ignored.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loc-actions">
|
||||
<input type="file" id="loc-file" accept=".csv,.txt,text/csv,text/plain" hidden>
|
||||
<button type="button" class="add-btn" id="loc-file-btn">Choose a CSV file…</button>
|
||||
<button type="button" class="add-btn" id="loc-check-btn">Check without importing</button>
|
||||
<button type="button" class="add-btn" id="loc-import-btn">Import</button>
|
||||
<button type="button" class="add-btn" id="loc-sample-btn">Load sample values</button>
|
||||
</div>
|
||||
<!-- Rejected rows and duplicates land here, with line numbers. An
|
||||
import that says "42 rows" over a file with 50 in it has lost
|
||||
eight and told nobody, which is what this exists to prevent. -->
|
||||
<div class="loc-report" id="loc-report" role="status"></div>
|
||||
|
||||
<div style="margin-top: 2rem; border-top: 1px solid var(--border); padding-top: 1.5rem;">
|
||||
<div class="sub-heading">Current list</div>
|
||||
<p class="field-hint" id="loc-count"></p>
|
||||
<div id="loc-list"></div>
|
||||
<div class="loc-addrow">
|
||||
<label class="loc-addlabel" for="loc-add-parent">Add under</label>
|
||||
<select id="loc-add-parent"></select>
|
||||
<label class="loc-addlabel" for="loc-add-name">Name</label>
|
||||
<input type="text" id="loc-add-name" placeholder="e.g. a new sector">
|
||||
<button type="button" class="add-btn" id="loc-add-btn">+ Add</button>
|
||||
</div>
|
||||
<div class="field-error" id="loc-add-err" role="alert"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- SOP NAVIGATION -->
|
||||
|
||||
Reference in New Issue
Block a user