the block knows it is settled before it paints, so it stops flickering
It could not. Each part of Root location fetches its own status, so the block had no way to know whether it was finished until its children had mounted and reported — meaning it painted open and then folded away, on every load. That is not a rendering problem to tune: the information genuinely was not there yet. So one request answers the three bits first, and nothing renders until both it and the path have arrived. The children still own their detail and still report changes upward while the block is open; the summary only seeds the opening state. Two sources for one fact with a clean division — the summary is what is true on ARRIVAL, the callbacks are what changes while somebody is looking. `settledNow` picks between them: the summary while nothing has reported, the live value once anything has. Using the live value throughout would open the block for exactly the gap between the summary landing and the children answering, which is the flicker with extra steps. The summary walks the media tree, so it is deliberately not polled, and it skips the walk entirely when there is no media folder to walk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -267,6 +267,44 @@ router.get('/_config/media/sync', async (ctx) => {
|
||||
* flight, and this one walks the whole tree reading manifests. Answering both from one route would
|
||||
* make every tick of a progress bar re-read the library.
|
||||
*/
|
||||
/**
|
||||
* The three bits Root location needs BEFORE it paints: is it set up?
|
||||
*
|
||||
* ── Why this is not assembled from the children ──
|
||||
*
|
||||
* Each part of that block fetches its own status, which is right — but it means the block cannot know
|
||||
* whether it is finished until its children have mounted and answered, so it must render open and then
|
||||
* fold itself away. That flicker is on every page load, and it is not a rendering problem to be tuned
|
||||
* away: the information genuinely was not there yet.
|
||||
*
|
||||
* One request that answers it first removes the cause. The children still own their own detail and
|
||||
* still report changes upward while the block is open — this only seeds the opening state so the first
|
||||
* paint is already correct.
|
||||
*
|
||||
* It walks the media tree, which is the expensive part, and is therefore deliberately NOT polled.
|
||||
*/
|
||||
router.get('/_config/summary', async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
const settings = await readSettings(user.email);
|
||||
|
||||
const [website, media] = await Promise.all([
|
||||
readWebsiteStatus(user.email),
|
||||
readMediaStatus(user.email),
|
||||
]);
|
||||
|
||||
// Only walked when there is somewhere to walk. Reconciling against a media folder that does not
|
||||
// exist is a guaranteed empty answer bought at the price of a tree walk.
|
||||
const outstanding = media.complete
|
||||
? await reconcile(user.email).then((state) => state.missing.length + state.stale.length)
|
||||
: null;
|
||||
|
||||
return ctx.json({
|
||||
rootSet: settings.isSet,
|
||||
websiteReady: website.isRepo,
|
||||
mediaReady: media.complete && outstanding === 0,
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/_config/media/ledger', async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
const scope = ctx.req.query('path') ?? '';
|
||||
|
||||
Reference in New Issue
Block a user