T4.2 - S3: the app's state has an address; X1 is unblocked
Wave 0 counted pushState across html/ and found 0. Every page read its query
string once at boot and never wrote one again, so you could not send anyone a
link to WP07, a refresh dropped you back at the default view, and Back left the
app entirely because the app had never added a history entry.
CR-011 and CR-014 both promise an email carrying a direct link to a work package.
That is X1, and it was blocked on this. It is not blocked now.
html/wp-url.js is the whole mechanism, and it is deliberately NOT a router.
Nothing in it intercepts navigation or renders anything; it is the query string
treated as state that can be read, merged, written and subscribed to. Pages keep
their own rendering. Query parameters rather than a hash, because the server
already serves these paths and a hash is never sent to the server - which matters
the day a link has to be resolved before the page boots.
The merge behaviour is the part that earns its place: WPUrl.push({wp:id}) keeps
the active project, and WPUrl.push({wp:''}) clears one key without needing to know
what else is in the URL. Hand-built URLs losing ?project= is the usual way this
goes wrong.
WIRED: the creator (open package, dashboard view), the SOP wizard (tool, step),
the launcher (project). Each records a history entry only when the user chose the
change - restoring from the URL uses replace, or Back would immediately add an
entry and appear to do nothing.
WPUrl.absolute() is what CR-011/CR-014 will paste into an email in wave 8.
TWO BUGS THIS TASK CREATED AND FIXED, both found by the probe rather than by
reading:
- bootSOP() calls newPackage() during boot, and newPackage() cleared ?wp=. A
deep link therefore worked and then erased its own parameter, leaving Back
with nothing to return to. Now guarded on wpCreatorReady.
- goToStep() runs validateStep(), which ends in alert() when a required field
is empty - always true on a freshly loaded page. So restoring ?step=3 from a
shared link opened a modal dialog mid-boot, and hung the browser under CDP.
Restoring a view is not a forward navigation and no longer runs the
forward-navigation guard.
The second one is worth keeping in mind for the rest of wave 4: this app has 79
native dialogs, and any of them firing during a restore path will hang a headless
browser rather than fail visibly.
VERIFICATION. tests/url_state_check.py, 23 checks, all passing, covering every
done-when on the task:
- a URL identifying a work package opens that package
- the same URL for a SIGNED-OUT user goes to login, carries the target through
?next=, and lands on the work package itself after signing in
- refresh preserves project, package, tab and view
- Back and Forward move through states, verified as still-initialised rather
than reloaded, and with the dashboard actually rendered rather than only the
URL changed
- a different user opening the same URL reaches the same view
- nothing credential-shaped appears in the query string
Metric 8, pushState: was 0 at wave 0, now 2 in html/ (one pushState and one
replaceState, both in wp-url.js) behind 6 call sites across 4 files. The raw
count stays low by design - one place writes history, which is the same reason
the token work put one place in charge of colour.
browser_check 71/71, f_items 5 FIXED / F6 REPRODUCES, aggregates 16/16.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,9 @@
|
||||
<!-- Date/number formatting. Must parse BEFORE the app scripts: they format
|
||||
timestamps during their own boot. -->
|
||||
<script src="wp-format.js"></script>
|
||||
<!-- Addressable state (S3). Parses before the app scripts, which read the URL
|
||||
during their own boot. -->
|
||||
<script src="wp-url.js"></script>
|
||||
<link rel="icon" href="favicon.ico" sizes="any">
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
<meta name="theme-color" content="#161616">
|
||||
@@ -440,6 +443,13 @@
|
||||
}
|
||||
// Reconcile the active project against the list; clear if it's gone.
|
||||
const active = ProjectData.getActive();
|
||||
// S3: if a project is active but the URL does not say so, make the URL say
|
||||
// so — with replace, not push, because the user did not navigate here. This
|
||||
// is what makes "copy the address bar" produce a link that lands somebody
|
||||
// else on the same project rather than on whatever they last had open.
|
||||
if(active && typeof WPUrl !== 'undefined' && WPUrl.get('project') !== active.id){
|
||||
WPUrl.replace({ project: active.id });
|
||||
}
|
||||
const dropped = (active && !_projects.some(p => p.id === active.id)) ? active : null;
|
||||
if(dropped) ProjectData.setActive(null);
|
||||
renderProjectPicker();
|
||||
@@ -527,10 +537,17 @@
|
||||
ProjectData.save(Object.assign({}, ProjectData.SAMPLE)).then(saved => { afterProjectChosen(saved); });
|
||||
}
|
||||
|
||||
// S3: which project you are in is addressable, so a launcher link carries it
|
||||
// and Back returns to the project you were looking at before.
|
||||
function urlSyncProject(id, replace){
|
||||
if(typeof WPUrl === 'undefined') return;
|
||||
(replace ? WPUrl.replace : WPUrl.push).call(WPUrl, { project: id || '' });
|
||||
}
|
||||
|
||||
function selectProject(id){
|
||||
if(!id){ ProjectData.setActive(null); applyActiveProject(); return; }
|
||||
if(!id){ ProjectData.setActive(null); urlSyncProject('', false); applyActiveProject(); return; }
|
||||
const p = _projects.find(x => x.id === id);
|
||||
if(p){ ProjectData.setActive(p); applyActiveProject(); }
|
||||
if(p){ ProjectData.setActive(p); urlSyncProject(p.id, false); applyActiveProject(); }
|
||||
}
|
||||
|
||||
function afterProjectChosen(p){
|
||||
@@ -577,7 +594,22 @@
|
||||
reflectSOPStatus(active);
|
||||
}
|
||||
|
||||
function clearActiveProject(){ ProjectData.setActive(null); renderProjectPicker(); applyActiveProject(); }
|
||||
function clearActiveProject(){ ProjectData.setActive(null); urlSyncProject('', false); renderProjectPicker(); applyActiveProject(); }
|
||||
|
||||
// Back / Forward between projects.
|
||||
if(typeof WPUrl !== 'undefined'){
|
||||
WPUrl.onChange(function(state, viaPop){
|
||||
if(!viaPop) return;
|
||||
const want = state.project || '';
|
||||
if(want === (ProjectData.getActiveId() || '')) return;
|
||||
if(!want){ ProjectData.setActive(null); }
|
||||
else {
|
||||
const p = (_projects||[]).find(x=>x.id===want);
|
||||
ProjectData.setActive(p || { id: want });
|
||||
}
|
||||
renderProjectPicker(); applyActiveProject();
|
||||
});
|
||||
}
|
||||
|
||||
// Reflect SOP completion on the tool cards (B4).
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user