summaryrefslogtreecommitdiff
AgeCommit message (Collapse)Author
8 daysUI: the pie labels were a quarter-turn out from their own wedgesHEADmainCharlie Root
Three attempts at this and the cause was never the offset arithmetic. Measured on build 86, the label mean angles were 45.9 / 130.3 / 209.1 / 319.9 degrees while the wedge centres are 0 / 90 / 180 / 270. A constant ~45 degree error in all four quadrants is not a magnitude problem, it is an origin problem. The wedges are drawn at angles[i], which distributeAngles() fills starting from 0 and stepping clockwise, so item 0 sits at drawAngle 0 -- to the RIGHT, because canvas angles run from 3 o'clock. The label loop was computing `i * (360.0/n) - 90.0`, which assumes the first wedge is at the top. So the labels were anchored a quarter turn away from the wedges, which is exactly one separator, and no value of the constant would have fixed it. The labels now take the wedge angles as a parameter and use angles[i] directly, so a label and its wedge cannot disagree again -- including for a menu whose item count is not 4 or which pins an item with an explicit `angle`. Also worth recording: the two earlier attempts were tuning a constant while the constant itself was wrong, and the measurements (19 px in the top quadrant, then "all four on the boundaries") were consistent with an origin bug the whole time.
8 daysbuild: metaspace was the limit, not heap or wasm-opt's -OzCharlie Root
build 84 died in bundle-web with "Gradle build daemon disappeared unexpectedly", same as builds 76 and 77. The daemon log is what finally settled it: Starting 8th build in daemon [uptime: 3m45s, performance: 99%, GC rate: 0.00/s, heap usage: 0% of 2 GiB, non-heap usage: 28% of 512 MiB] ... ValidateIrAfterLowering Heap usage was zero. The host was 91% idle. wasm-opt was still at -Oz. None of those were the cause: the daemon had served eight builds and by the eighth its class metadata had filled a 512 MiB metaspace, and it died while wasm-opt's Kotlin lowering loaded more IR classes. So this is non-heap exhaustion, and the fix is non-heap capacity. -XX:MaxMetaspaceSize 512m -> 1g org.gradle.daemon=false one JVM per build, so metadata cannot pile up Verified by reproducing the CI sequence -- clean the wasm outputs, the build cache and the configuration cache exactly as clean-wasm-output does, then run bundleWeb: BUILD SUCCESSFUL in 12m, no signal 9. That is the same sequence that just failed in CI.
8 daysbuild: drop to --max-workers=1; -Oz was never the problemCharlie Root
build 83's assemble ended with "command finished with signal 9". I had assumed that was wasm-opt's -Oz pass list and tried to change it to -O2, which broke the detekt gate instead (detekt parses build.gradle.kts as Kotlin source, and the `optimizations { wasm-opt { ... } }` accessor does not resolve there -- 9 "Unresolved reference" errors). That DSL block is gone again in 4f00e13. With the block removed, wasm-opt still runs at -Oz and both of the steps that were dying now pass on this host: ./gradlew --max-workers=1 assemble BUILD SUCCESSFUL in 7m38s ./gradlew --max-workers=1 bundleWeb BUILD SUCCESSFUL in 3m11s with no signal 9. So the killer was concurrency, not the optimizer level: several JVMs (Gradle, the Kotlin daemon, wasm-opt) resident at once is what overran the 11.9 GB. The budgets are lowered to match (Gradle 3g -> 2g, Kotlin 2g -> 1.5g) and --max-workers=1 is applied to assemble, bundles and bundle-web so only one compile is resident at a time. The wall clock goes up, which is the trade that has to be made for a bundle that exists.
8 daysbuild: drop the wasm-opt DSL block that detekt cannot parseCharlie Root
The -O2 block I added to silence the OOM killer broke the detekt gate instead. build 82 failed at detekt-gate, and running detekt by hand showed why: it parses build.gradle.kts as Kotlin source, and the Kotlin DSL's optimizations { wasm-opt { ... } } is a lazy accessor with no resolvable type, so detekt reported 9 'Unresolved reference' errors on wasm-opt, optimizationLevel and pass. So the file is back to the plain and detekt passes again (verified: detekt and compileKotlinWasmJs both BUILD SUCCESSFUL). The OOM problem is therefore NOT fixed by this commit. wasm-opt still runs at -Oz and bundleWeb can still be killed by signal 9. The fix has to be expressed in a way detekt can parse, which means the argument list for the optimizer task rather than the target DSL. That is the next change, and it needs to be verified against both detekt and bundleWeb in one go, because each one alone will not tell you whether it worked.
8 daysbuild: wasm-opt -Oz -> -O2; the optimizer was OOM-killedCharlie Root
build 81's bundle-web ended with 'process killed by signal 9' during compileProductionExecutableKotlinWasmJsOptimize, i.e. the OOM killer took wasm-opt on this 11.9 GB host. wasm-opt exposes no memory cap, so the only lever is how much work it is asked to do. At -Oz Kotlin's default pass list runs --gufa plus repeated -O3 --type-merging. That combination is the memory peak. -O2 keeps most of the size win and does not pull in gufa. The pass list is now written out explicitly so it cannot silently drift back to the -Oz defaults. This trades a little wasm size for the web bundle actually shipping at all, which is the right way round: an unbuilt bundle helps nobody. Also fixes the CI fleet-wide daemon pressure by leaving the --max-workers=2 restriction from the previous commit in place.
8 daysUI: place the pie labels by absolute polar coordinatesCharlie Root
Third attempt at the label placement, and the previous one did not work. Measured about the true disc centre (135.0, 725.5, a 200x200 disc) the label means sat at 152 / 201 / 300 degrees: still 15-24 degrees from a separator and 21-62 degrees from a wedge centre. Why 90 dp did not fix it: Modifier.offset is applied AFTER placement, so it adds to wherever the parent's placement rules already decided. With Box(Modifier.fillMaxSize(), contentAlignment = Center) the parent was deciding, and a polar offset added on top of that is not a polar position -- the parent's rules win and the angle comes out wrong. This time the parent stops deciding. BoxWithConstraints gives the real constraint size, and each label box is positioned at (centre + cos/sin * 67dp - halfBox) with nothing else touching placement. Box is 84 dp so it fits inside a 45 degree wedge at r=67 (arc length there is about 53 dp), and the box's centre, not its corner, is placed on the bisector. Verified by measurement on the next build rather than assumed: the label mean angle per quadrant must come out near 0/90/180/270 rather than 45/135/225/315.
8 daysUI: stop the pie labels from riding the quadrant boundariesCharlie Root
The previous commit fixed the missing top label but left the placement wrong in a subtler way, and the measurement caught it: taking the circular mean of the white ink in each quadrant gave 60 / 128 / 207 / 325 degrees. The wedge bisectors are 0 / 90 / 180 / 270 and the separators are 45 / 135 / 225 / 315, so every label sat 27-60 degrees off its wedge centre and within 7-18 degrees of a separator. Cause: each label lived in a Box sized labelRadius * 2 = 134 dp. That is two thirds of the whole 200 dp disc, so it does not fit inside a 45 degree wedge. Compose redistributed the overflow by placing the child about 45 degrees away from where the offset asked, which is exactly the boundary-hugging result. Fix: size the measuring box at 90 dp so it fits the wedge arc, and compute the top-left so the box's CENTRE lands on the bisector at 67 dp rather than letting the box corner land there. Radial distance is unchanged. Hub stays at 22% and the separators stay 3 px; the review only complained the overall structure still read weakly because of this placement.
8 daysbuild: cap Gradle parallelism; the daemon was being OOM-killed, not wedgedCharlie Root
builds 76 and 77 both died in assemble with 'Gradle build daemon disappeared unexpectedly'. I had guessed --no-daemon would help; it did not, it just moved the whole build into the client JVM and that died the same way. The useful evidence was ./gradlew --status reporting every daemon as 'STOPPED (by user or operating system)' while vmstat showed 91% of 11.9 GB idle, and the build log complaining that 9 stopped daemons could not be reused. What actually fixes it is the worker count. assemble with --max-workers=2 completed in 5m33s from a clean daemon registry. So: org.gradle.parallel=false project-level parallelism off org.gradle.jvmargs -Xmx1536m -> 3g (holds Kotlin now that it is in-process) kotlin.daemon.jvmargs 4096m -> 2g kotlin.compiler.execution.strategy=in-process one fewer 4 GB JVM registry.maximum-size=1, idletimeout 2 min the pool stops accumulating assemble step passes --max-workers=2 explicitly bundle-web drops --no-daemon again; with the pool bounded, a normal reused daemon is correct and faster for a ~1 min task.
8 daysUI: fix the pie's top label falling outside the discCharlie Root
Pro reviewed the rebuilt menu and read the image rather than the numbers: three quadrants had labels, the top one had none. My own pixel count agreed -- 19 px of white ink in the top quadrant against 421-519 px in the others. Cause: the label loop computed a polar offset relative to the CENTRE but applied it with Modifier.offset inside Box(Modifier.fillMaxSize()), whose origin is the TOP-LEFT. So every label was displaced by half a radius too little, and the top one at y = -67 dp landed outside the disc entirely. The three that appeared to work were only roughly right. Fix: centre the outer Box (contentAlignment = Center) so the origin is the disc centre, and pass the polar displacement as a relative offset. Pro's -90 deg convention was already right -- item 0 at the top, proceeding clockwise -- and is kept; only the origin was wrong. Pro's other read on the rebuild: the 3 px separator reads well at this size and does not flare, and the 22% hub is better balanced than 26%. Both kept.
8 daysci: run bundleWeb with --no-daemonCharlie Root
build 73 failed in bundle-web. Re-running the task by hand reproduced the message 'Starting a Gradle Daemon, 7 busy and 1 stopped Daemons could not be used', and ./gradlew --status showed a pool that had accumulated across the repeated forced builds. The invocation lost that race and died; a later run with '--stop' followed by --no-daemon completed the same task in 1m12s and produced the zip. --no-daemon gives the build its own JVM and tears it down afterwards, so a wedged or crashed previous build cannot poison the next one. That matters more than the lost daemon reuse, because bundleWeb is only ~1 min here.
8 daysUI: place the pie labels on the wedge bisectors, not on a gridCharlie Root
The menu still looked wrong after the donut revision, so the geometry was re-measured rather than re-guessed. The numbers matched the previous reviewer's spec exactly, which is what made the spec itself the suspect. Measured on the published build: disc 200x200 at (139.5, 725.5), hub 54x54 dead centred on it, wedge/gap boundaries exactly on 45/135/225/315 degrees. So the distances were right. But sampling the white label ink at r=40..90 and taking the circular mean of each cluster gave 35 / 133 / 148 / 212 / 330 degrees. The wedge bisectors are 0/90/180/270. The labels were sitting on the SEPARATOR axes, not the wedge centres. Cause: the labels were placed with a 3x3 Row/Column grid. That grid happens to put cell centres on the diagonals, so a polar control was being laid out with Cartesian logic and the 61 dp radius -- correct as the midpoint of the 26..96 band -- landed the glyphs on the boundaries. Labels are now placed in polar coordinates: angle from the wedge bisector (top, then clockwise), radius 67 dp, centred on their own measured glyph box rather than a grid slot. Horizontal text is kept; rotating the left/right labels would read as an instrument dial rather than school navigation, so the radial relationship comes from position only. Also, per the same review, the separators stop being angular gaps. A 2 degree gap is 0.94 px wide at the hub and the wedge/gap contrast was 1.34:1, so it read as four notches in one disc. They are now a constant 3 px stroke in #24272D, which does not flare with radius, and pieWedge moves #5C6068 -> #626873 to reach about 3:1 against it. The hub drops from 26% to 22% of the radius so the centre is less dominant. Labels go to 15sp semibold, which also helps the longest (3 chars) sit level with the 2-char ones.
8 daysci: give bundleWeb its own step with a generous timeoutCharlie Root
build 69 died in 'bundles' with no wasm-web zip in distributions, while the same task run by hand took 8m45s and produced the zip. wasm-opt -O3/-Oz plus webpack is by far the slowest thing in this build, and six bundle tasks in one invocation pushed the step past the worker's timeout. Splitting bundleWeb into its own step with timeout=3600 keeps the fast bundles where they are and stops the web bundle from riding on their clock. The deploy step already asserts every expected zip exists before uploading, so a silent missing bundle is now reported by name rather than as a bare FTP exit 3.
9 daysbuild: ignore webout/ — the FTP deploy step's scratch directoryCharlie Root
9 daysPie: gaps instead of dividers, and drop the second outer ringCharlie Root
Reviewed with GPT 5.5 Think Deeper over the M365 relay. It confirmed the donut was the right call and rejected one part of it. Keep the donut. The hub is 6.76% of the disc area (radius 26dp of 100dp) against ~23.3% per wedge, so it is usable without crowding the items, and removing the cross from behind the "W" fixes a real defect. It also supplies the missing semantic rule: the centre is a meta-action, not a fifth command, and must not be styled like a wedge -- recorded at the glyph, since the tree is currently one level deep and "W" is correct only at the root. Dividers: replace the radial strokes with a 2dp negative-space gap in the disc colour. Contrasting strokes read as construction lines and emphasise the pie's geometry rather than four selectable objects. The gap is paint only -- hit testing keeps the exact 90-degree boundaries, so a pointer in a gap still belongs deterministically to one adjacent sector. The near-coincident ring was the actual mistake: a selection ring at r-1.5/3px sat 3px outside the frame ring at r-1.2/2.4px and read as two parallel lines. Selection is now the corresponding arc of the single existing frame, drawn at 5px in the accent colour. Label band unchanged at 39dp: the hub turns out to validate it rather than invalidate it -- 61dp is exactly the midpoint of the now-visible 26..96dp annulus, so moving labels either way would make the hub look wrong. Pro also confirmed the arithmetic behind the 200dp diameter is sound (Blender and Kando both use 100px radius; Gnome-Pie's formula gives 46px only as the geometric minimum).
9 daysUI: fix the Note row title, the white-on-ground text, and reshape the pieCharlie Root
Three separate defects, all found by measuring captures rather than reading the code. 1. Note row title stayed black. NoteButton's title Text omitted color=, so it inherited LocalContentColor and got Compose Material's default black -- identical to the KUKURI/card bugs, but my earlier sweep MISSED it. That sweep's parenthesis matching over-ran past a single-line call into the next statement, absorbed that statement's color=, and reported zero. The detector now tracks depth per character across the file; it reports 0 genuinely uncoloured Text() calls across all 13 files, so this class is closed by construction rather than by spot fixes. 2. Light mode left white-on-ground text. Five sites had a hard-coded Color.White with nothing filled behind them, so they vanished on the light ground: the Home "今日" heading, the CountChip number, the pie hub ring, the pie child dots, and the pie "W". Each now takes the theme's paired colour (onSurface / pieInk / pieLine). The other Color.White uses are left alone deliberately -- they sit on a filled accent (wine primary, personal-kind chip, FAB, active nav item, filled filter/action buttons), where white is correct in both themes. Verified each by reading its background rather than by blanket replace. 3. Pie menu reshape, asked for as "もっといい形に". The wedges ran to the centre and the divider lines crossed the hub, so the "W" had a line through it and the four items read as one pie. It is now a donut: a hub disc drawn over the wedges, dividers starting at the hub radius instead of the centre, and a ring on the touched wedge's outer edge so the current selection is shown by position as well as colour.
9 daysUI: fix the card body text that stayed black in dark modeCharlie Root
Follow-up to the KUKURI title fix -- same defect, same cause, six more sites. The card content on Home / Note / Timeline rendered black on the dark ground: HomeSection label (TODO section heading) Today's section label (今日 section heading) NoteRow body (note text, the unread/bold variant) timeline row body (the line text) timeline row `from` (the author column) second timeline body (the line with an attachment/badge) All omitted color, so they inherited LocalContentColor; no Surface in those columns sets it, so they took Compose Material's default black. Adjacent elements in the same rows -- the note caption, the badge, the right-aligned due labels -- already passed an explicit color, which is why only part of each row looked wrong. Every Text in modules/orgflow-ui now names a color: a sweep of the whole module for Text(...) calls with no color= argument returns zero, so this class of bug is closed rather than patched at six sites. onSurface rather than White, so light mode stays correct.
9 daysUI: give the three colourless Texts an explicit theme colourCharlie Root
Dark mode left the titles black. Measured from the deployed capture: the "KUKURI" title rendered as #1b1c1f, luminance 11%, i.e. DARKER than the #313338 ground it sat on. Cause: all three call sites omitted color, so they inherited LocalContentColor -- and no Surface in those columns sets it, so they got Compose Material's default, black. - NavRail "KUKURI" heading - NarrowScaffold "KUKURI" heading (phone layout) - FramePlaceholder frame title onSurface is the paired colour for the background/surface faces actually in use, so it is correct in both themes rather than hard-coding white -- which would have re-broken light mode. Home / Timeline / Note measured #fcfcfc / #fafafa and were already white; their apparent dimness is contrast against the invisible title, not a separate defect. The phone tab bar already passed an explicit colour.
9 daysfix(master.cfg): stop the iOS artifact name collapsing to a control charCharlie Root
fetch-github-ios derived the version with sed "s/.*-\([0-9][^/]*\)\.zip/\1/" but that step lives inside a Python triple-quoted string, where \1 is an OCTAL escape. Python consumed it as chr(1) and handed the shell a replacement character, so every build staged the file as kukuri-ios-\x01.zip which then failed the FTP upload. Build 60 surfaced it as 'FTP FAILED: kukuri-ios-\x01.zip' -- the artifact assertion added in the previous commit is what made this visible at all. Use parameter expansion instead (no backreference to escape), and reject a non-numeric version rather than carrying it into a filename. Verified: 'kukuri-wasm-web-0.1.0.zip' -> 'kukuri-ios-0.1.0.zip', and 'abc' is rejected.
9 daysfix(master.cfg): correct escaped line continuations in the deploy stepCharlie Root
The previous commit wrote the shell line-continuations as a literal double backslash, so the embedded script failed 'bash -n' with 'Syntax error: word unexpected' and the deploy step would have aborted on every run. Now verified with 'bash -n' on the extracted script, and the clean-wasm-output step re-checked the same way.
9 daysCI: fail the deploy loudly when an artifact is missingCharlie Root
Build 58 exited 3 in deploy-to-lighttpd with no usable message. The FTP loop had no error handling, and the underlying fault was upstream and silent: a Zip task whose from(...) directory does not exist archives nothing and does NOT fail, so a bundles step that produced no kukuri-wasm-web zip left the loop uploading a short list and dying partway on curl's exit code 3. Check for all five expected artifacts before uploading anything, name the missing one, and report which file failed on FTP instead of surfacing a bare exit code. Diagnosed by reproducing the CI sequence exactly (clean outputs, then all bundle tasks in one invocation) and confirming bundleWeb alone succeeds in 6m07s -- the earlier missing-zip results came from the harness killing the build at its timeout, not from a defect in the task.
9 daysCI: drop the Gradle configuration cache tooCharlie Root
The build cache alone was not enough. A reused configuration-cache entry made the detekt gate fail with ClassNotFoundException: io.gitlab.arturbosch.detekt.cli.Main Reusing configuration cache. ...while the same task passed locally with --no-configuration-cache, so the cached task graph held a classpath that no longer resolved. That cache is also what pins the Wasm compile UP-TO-DATE, which is how a stale bundle shipped past a green build. Note: master.cfg lives in git AND on the master at /var/lib/buildbot/masters/default/master.cfg. A change here does nothing until it is copied across and 'buildbot reconfig' runs -- the first attempt at this fix appeared to work because the step list still lacked clean-wasm-output.
9 daysCI: force the Wasm compile so the web bundle cannot ship staleCharlie Root
The served web bundle was stale while every build reported SUCCESS. Measured from the deployed pixels, not inferred: the nav rail rendered at ~232px and the pie disc at exactly 132x132px -- both are the PRE-change values, while the source has had 280dp and diameterDp=200 since bd86609. Cause: the Kotlin/Wasm compile stayed UP-TO-DATE against build/ outputs left in the tree by an earlier local build, so `bundles` repackaged the old wasm. Clearing the Gradle build cache alone does not help, because the up-to-date check also keys on the compile output already being present. Add a clean-wasm-output step ahead of `bundles` that removes the Wasm compile/dist/webpack outputs and the build cache. Only build directories are touched, never source. Note the correction to an earlier claim in this repo's history: the content-addressed wasm filename was taken as proof that the served artifact matched the source. It does not -- the address only changes when the compile actually runs, so an unchanged address meant "nothing recompiled", not "already up to date". Measuring the rendered pixels is what caught it.
9 daysBuild: call bundleWasmNode by its real nameCharlie Root
The bundles step asked for `bundleWasm`, which is not a task. Gradle's camelCase abbreviation matching resolves it to `bundleWasmNode`, so the build reports SUCCESS while building the node bundle again and leaving the web bundle to `bundleWeb` alone. Confirmed: $ ./gradlew bundleWasm --dry-run :modules:orgflow-domain:bundleWasmNode SKIPPED BUILD SUCCESSFUL Harmless today, but it silently retargets if another bundleWasm* task ever appears, and it misrepresents what the pipeline does. Separately, while chasing this: the deployed web wasm looked stale because its bytes never changed across builds. The cause was the Gradle build cache serving pre-change entries, not a broken pipeline. With the cache removed, the same source edit produces a new content-addressed wasm name and orgflow-ui.js picks it up. The wasm filename is content-addressed, which makes it a cheap oracle: 38dp produced 5e232b18..., and restoring 39dp reproduced 8bc1b48e... exactly -- so the served artifact does contain the current PieMenu change. The 2026-09-27 mtime inside the zip is Gradle preserving timestamps for unchanged entries.
9 daysWorker: back up uncommitted work before a build can discard itCharlie Root
master.cfg checks out with method='clean' and buildbot builds inside this same tree, so every build silently reverts uncommitted changes here. That cost a finished PieMenu edit, which had to be reconstructed by hand. Add a script that snapshots staged+unstaged diffs to /ROM/worker-cache/uncommitted-<stamp>.patch. Run it before gradlew, before pushing, and before any build not initiated here.
9 daysPie menu: widen the disc to the measured real-product radiusCharlie Root
The 120dp disc was 60% of what shipping radial menus actually use, which left 4-character labels touching the outer ring and the divider lines. Match the measured values instead of the geometric minimum: Blender pie_menu_radius = 100px -> 200px diameter Kando --child-distance: 100px -> 200px diameter Fly-Pie child-offset = 106px -> 212px diameter Gnome-Pie radius = 100..150 -> 200..300px diameter Gnome-Pie's sliceRenderer gives the geometric lower bound for 4 wedges as (slice_radius + slice_gap) / tan(pi/N) = 46px, so 100px is not a geometric requirement -- it is where the products settled for legibility and room to move. Left column goes 232dp -> 280dp to hold a 247px painted disc plus 14dp padding on each side. Label band moves 21dp -> 39dp so the text sits mid-band between the 26dp centre hub and the outer ring, touching neither the hub, the dividers, nor the frame. Restores the working-tree edit that the buildbot's `method='clean'` checkout discarded when it ran inside this directory; a copy is kept at /ROM/tmp_probe/pie-recovered.patch.
9 daysWorker: recover the build line on FreeBSD+Linuxulator podmanCharlie Root
The worker container could not start and, once started, could not reach the master. Two independent host-level causes, both measured rather than assumed: 1. pf was disabled, so podman refused to start the container at all ("The pf kernel module must be loaded to support ipMasq networks"). /etc/pf.conf already carries the matching NAT rule for podman's 10.88.0.0/16, and the ruleset has no block rules, so enabling it changes no filtering. 2. The CNI bridge path returns EACCES on every outbound connect, including to the container's own gateway -- verified from a fresh container with no volumes, against 10.88.0.1, 192.168.3.110, 8.8.8.8 and 1.1.1.1. The route table inside the container is correct, so this is ocijail refusing the connect. --network=host was measured to work for all four and costs nothing here: the worker only makes outbound connections. Also record, because both cost real time: - GRADLE_USER_HOME (6.3G) and .konan (1.7G) live in the container's writable layer with no named volume, so `podman rm` destroys them. - `podman commit` does not preserve them: buildah stages into /var/tmp on the root zfs pool, which has 1.5G free against a 6G layer (ENOSPC). Extracting the two directories with --tmpdir pointed at /ROM works. - Secrets exist in two locations and they DIFFER. /root/buildbot/secrets is the set the container mounts; /ROM/wiiu/secrets has a different worker_pass and an empty git_pass. - `master.cfg`'s TOKEN lines look corrupted when a tool redacts "Authorization:" in rendered output. They are intact. extract-caches.sh + recreate-worker-hostnet.sh make the recovery rerunnable; RECOVERY.txt documents the evidence and the traps. Named .txt because .gitignore excludes *.md as local-only. Verified after the change: master reports connected_to [{masterid: 1}], twistd logs "Connected to buildmaster; worker is ready", and the toolchain inside the container is intact (JDK 21.0.12, node 20.20.2, gradle 6.3G, konan prebuilt, Android build-tools 35/36).
2026-09-27Pie menu: give it its own surface colours so the disc is actually visiblecharlie
Reported as "wrong and hard to see", and the cause was a colour source that had nothing to do with the pie. The disc was filled with MaterialTheme.colors.secondary. In dark that is #3A3C42 against a page ground of #313338 - a difference small enough that the disc read as part of the background. The wedges were then drawn at white alpha 0.16 on top of that, so they vanished as well. The result was an outline with labels and no visible body, which is what the screenshot showed. The pie now carries its own four colours in the theme, alongside the calendar colours, because "is this visible" is a property of that surface and should not be inherited from a general-purpose role like secondary: dark ground #313338 -> face #4A4D55 -> wedge #5C6068, line #EDEDEF, ink white light ground #F2F0F0 -> face #FFFFFF -> wedge #C9C6C6, line #3A3436, ink #1A1617 Wedges are now opaque rather than a white overlay, so the three steps (ground, face, wedge) are guaranteed to differ by construction rather than by luck. The light values needed a second pass. The first attempt used #E2E0E0 for the wedge, which on a #F2F0F0 ground was judged too close to separate - the same failure as the dark case, caught by checking the light theme rather than assuming the dark fix carried over. #C9C6C6 gives the wedge a clear edge against both the white face and the grey ground. Verified in both themes at 3.2x: the disc body is visible, the outer ring is present, the cross reads as four sections, the labels sit inside their own wedges in the mid band, and in each case it is unambiguous which label belongs to which wedge.
2026-09-27Pie menu: draw the outer ring, move labels inside the disc, fix the dividercharlie
Three reported problems, all real, all reproduced in a capture first. 1. There was no outer ring. The edge of the disc was defined only by a faint difference from the page background, so the extent of the pie was not clear. There is now a white ring on the perimeter. 2. The labels sat outside the circle. That was a deliberate choice to avoid the occlusion that Callahan et al. 1988 recorded users complaining about - but it makes it impossible to tell which wedge a name belongs to. The names are now inside, in the outer band of each wedge, so they overlap neither the centre W nor the divider. The occlusion concern is handled by placement rather than by moving the text out. 3. Four wedges read as eight. The previous version left a two-degree gap between wedges, and those gaps appeared as four extra lines. The gaps are gone; the boundaries are drawn as lines instead, which for four items is the four diagonal lines that read as a cross. Sizes, measured rather than guessed where possible: - the disc is 120dp. At 132dp it overflowed the sidebar by 12px: the column is 232px and its padding takes 14dp each side, leaving about 204px, so 120dp is the largest that fits. - the dividers are 2.6 wide at alpha 0.85. The earlier 1.6 / 0.55 was reported as too faint, and a boundary that cannot be read invites the mis-targeting that 1993 recorded as fine-tuning and reselection. - labels are 13sp bold white, inset 21dp on all sides. With a 60dp radius, a centre cut-out of about 16dp and a few dp of ring, the usable band runs from about 26dp to 52dp from the centre; 21dp inset centres the text at about 39dp, mid-band. That last figure is arithmetic, not measurement - no study measures where text should sit inside a sector (recorded in 2.40). It follows from the constraint "clear of the centre W and clear of the divider, unambiguously inside its own wedge". Result checked in a capture at 3.2x: ring complete, four labels inside their wedges in the mid band, dividers clear, and the wedge each name belongs to is obvious. No remaining defect reported.
2026-09-27Pie menu: adopt Kando's data model, and ground the item count in measurementcharlie
The pie was a List<String> of four quadrant names with a separate Map<String, List<String>> for children. That cannot express a tree, so the submenu mechanism only worked one level deep and the angles were hardcoded in the drawing code. It is now PieItem: id, label, optional angle, children. Modelled on Kando (kando-menu/kando, MIT), whose schema I cloned and read (src/common/settings-schemata/menu-settings-v2.ts). Kando's items are a recursive union of button and submenu types over a common base of name, icon and iconTheme, and the part worth taking is how angle works there - its own comment says an unset angle "will be computed when the menu is opened", while a set angle is a "fixed angle" and the siblings redistribute around it. FSMP adopts exactly that: leave angle out and the items divide evenly, set it and the item keeps that direction. That is what makes spatial memory possible later without changing the model. One thing deliberately not taken: Kando's three-way type split. Kando is a general launcher, so it needs to distinguish "has children" from "also fires on its own". The FSMP pie narrows a set rather than launching anything, so children being empty or not is the whole distinction. Adding a type field would mean checking which type each item is at every use site for no gain. The item count is now backed by measurement rather than assertion. Kurtenbach, Sellen & Buxton 1993 (Human-Computer Interaction 8(1):1-23, full text obtained) tested 4, 5, 7, 8, 11 and 12 slices - not 4/6/8/10/12 as the commonly repeated version has it, and not 10 at all. Even sizes (4, 8, 12) were easier to learn; odd sizes (5, 7, 11) were harder; 12 beat 11 on both speed and accuracy because a clock face is a usable metaphor. Their conclusion in Kurtenbach & Buxton 1994 (DOI 10.1145/191666.191759) is narrower than "12 is the limit": "if menus contain an even numbers of items and less th[a]n [ten] of them ... marking menus will have a distinct advantage". Four quadrants satisfies both, and 1993 also found that up to five slices the hidden/exposed distinction barely matters, which is where four sits. Also recorded, because they are widely repeated and wrong: - pie menus are not faster "because of Fitts's law". Callahan et al. 1988 fitted a Fitts expression after the fact as an explanation; they did not test the law. Their measurement was about 15% faster at 8 items, with the error-rate difference only marginally significant (P=0.087). - 7+-2 must not be used for menu item count. Cowan 2001 says Miller's seven "was meant more as a rough estimate and a rhetorical device than as a real capacity limit". The working-memory figure (3-5) is about retention, not about how many options can be shown. - there is no optimal choice count at all. The choice-overload meta-analyses found effects near zero. - icons must carry labels: ISO 9186 sets a 66% comprehension threshold for symbols, and that is a pass mark, not an average. Wiedenbeck 1999 is the empirical comparison. NN/g is not a citable academic source. What has NO evidence, now recorded as undecided rather than guessed: how much text fits in a sector, the right boundary margin, optimum hierarchy depth (1994 explicitly calls it an open question), a label length limit, thresholds for group count, and the effect size of progressive disclosure. The one-degree gap drawn between wedges is a judgement - the smallest gap that still shows the boundary - not a measured value.
2026-09-26Calendar: today-relative offsets, re-entering the month view, and a working ↵charlie
pie layout +"-1"/"+1" is information about distance from today, not a step button. The row now reads 今日から -21日 (or +N日) - how far the centre date is from today - and pressing it returns to today. Showing it as a step control gave no sense of distance, which was the point of the row. The same row now exists on the month and year levels, in the unit that fits: 今月から +Nヶ月 and 今年から +N年. It is omitted when the value is zero, since the centre already shows the current month or year and repeating it says nothing. Re-entering the month view. key(f) only re-keys when the frame changes, so pressing Calendar while already in Calendar did nothing - the state survived. A reentry counter is now part of the key, so pressing the same frame again resets it to the default view. That fixes the case the previous fix missed. The pie menu labels are finally placed correctly, after two wrong approaches that are worth recording because the failure was invisible in the code: 1. Computing offsets from the centre. Modifier.offset moves a node from where layout put it, so with a centre-aligned parent every label was displaced by half the parent's size. Only one of four appeared. 2. A nested Box(fillMaxSize()) with per-slot Alignment. The inner box takes the window's size rather than the parent's, so labels were drawn far from the disc and nothing appeared near it. Both failed for the same reason: the offset origin depends on the parent's dimensions, which cannot be corrected with constants. The fix is to stop computing positions and let layout do it - a Row of [label column, disc, label column], each column a Column of two labels. The disc and its labels cannot drift apart now because they are siblings in the same layout. Month and year labels carried the same defect and are fixed by the same change, since they all go through PieDial. Also fixed a compile break of my own making, worth noting because it presented as something unrelated: the file stopped compiling with "Unresolved reference" on functions that plainly exist. The cause was a doubled /** - two doc-comment openers in a row - which left one unterminated, so everything after it parsed as comment. This is the second time a comment has caused a cascade of unrelated errors in this file; Kotlin's nested comments mean an accidental /* anywhere, including inside a comment, swallows the rest of the file.
2026-09-26Calendar: fix the day-view header, month labels, year day taps, and the pie ↵charlie
labels Seven items, one of which was a real bug with a clear cause. Calendar would not return to the month view. Calling CalendarFrame() from a when(f) means Compose keeps its remember blocks alive when you navigate away, so leaving in the year or day view and coming back left you in that view. Wrapping the frames in key(f) gives each frame its own state, so Calendar always opens at the month view per 2.27. The day view header is now the requested shape: the centre is the date being viewed, written the same way the body used to write it (9月26日(土)), and the row below carries the offset from today as pressable chips. Pressing them moves. The offset is measured from today, not from the day being viewed - with today+3 on screen the row reads [今日] [-1日] [+1日] and those go to today, +2 and +4. Showing the current day again in that row would duplicate the centre. The first version of this did get the arithmetic wrong, sending +N back to the same day; caught and fixed while wiring it. The same bar shape is used for all three levels: chevron, centred value, chevron, and a pill on the month view. The month pill says 今月, not 今日 - the day view is the one that says today. Month event-bar labels were squashed: 9sp text in a 16dp bar left no vertical padding at all. The bar is 20dp, the text 10sp with a 12sp line height, and 4dp of horizontal inset. Days with more than one event keep plain 5dp colour bars - there is not enough height per bar for legible text, and colour alone beats unreadable text. Year view day taps now work. The previous version deliberately made days non-tappable to keep the year level about picking a month; the request was to jump to a day directly, so tapping a day in the year view opens that day. Spacing between sections, grounded in a measurement rather than a convention. Brumby & Zhuang 2015 (CHI, DOI 10.1145/2702123.2702177 - open access, read in full) measured 30px between items in a column and 250px between column groups, about 1:8, and concluded that what actually governs search time is whether the visual groups match the semantic ones - grouping unrelated items visually slowed search down (M=3.34s semantically arranged vs M=4.23s random, p<.001). This app had 6dp between rows and 16dp between sections, only 1:2.7. Sections are now 48dp for 1:8. The paper measured a ratio, not absolute values, so only the ratio is borrowed and the absolute size follows the row height here - which is more to stand on than 8dp-grid conventions: a survey found that Gestalt proximity gives only a monotonic relation and that the 8dp grid, 44pt and 48dp+8dp figures are industry conventions with no measurement study cited behind them. Pie menu quadrant labels are now always visible, placed outside the circle at 1.15x the radius. They were previously only shown while held, to avoid the occlusion that Callahan et al. 1988 (DOI 10.1145/57167.57182) recorded users complaining about - but an always-present control with no labels gives no reason to reach for it. Keeping them outside means the disc, its bands and the centre W are still never covered. Checked and NOT reproduced: black text in dark mode on Home, Timeline and Note. The most recent captures of all three were read carefully and every string is white or light grey. 2.31's spacing change is the only edit those frames received in this pass. If specific text still reads as black, it needs a screenshot that names it.
2026-09-26Phone layout: tab icons, short labels, and a working way to shoot at phone widthcharlie
The narrow layout was already implemented to 2.22's skeleton, but measuring it at a real phone width found two problems, and getting to that measurement took three failed approaches worth recording. Why a Galaxy S25 user agent changed nothing: Firefox will not make a window narrower than 500 CSS px. Requesting 380/400/412/450/500 returned 500 every time, while 600 and 700 were honoured exactly. An S25 is 384-412 px, so the real width is unreachable in a top-level window - the limit is the window, not the UA. Shrinking the Compose canvas element to 384 also failed: the canvas attribute became 384 but Compose kept laying out against 500, so the capture showed 500-wide content cut off at 384 and the tab bar lost Live and settings off the right edge. What works is an iframe of the exact size - it has its own viewport, so the app inside sees innerWidth 384 and Compose lays out against real dimensions. phone-host.html is now part of the web resources so it ships with the bundle, and shot-phone.py drives it. Recorded as 2.29 so this is not re-derived later. With a true 384px viewport, the tab bar did not fit. Laying labels out with SpaceEvenly got only Home, Timeline, Calendar and Note on screen; Live and settings were pushed off and never drawn. Six text labels do not fit 384px. So the tabs are now an icon over a short label, each weighted equally (2.28): - icons drawn on a Canvas, no image assets: house, list, calendar, page, camera, gear - short labels, chosen rather than truncated - cutting a word mid-way makes it unreadable, so separate short names are used: Home / Line / Cal / Note / Live / Settings - labels stay English. One tab reading 設定 among five English ones mixes languages inside a single row; in-page headings stay Japanese, but the tab row is a list of proper names. - icons are never shown alone, for the same reason 2.22 rejected an icon-only nav: NN/g's Icon Usability (2014) found icons on their own do not convey meaning reliably. Wide layout gets the same six glyphs on the left-column nav, as asked. The same frame now looks the same at both widths, so nothing has to be relearned when the window changes; wide has room for the full words, so it keeps "Timeline" and "Calendar". Calendar bar, per the same request: the centre pill on the month view is now 今月 rather than 今日 (the day view keeps 今日), and the pill is drawn with a fill so it reads as pressable rather than as a heading. Both bars were re-measured at 384px and fit. One capture trap worth knowing: the browser profile is reused so the wasm caches and startup is fast, which means a stale build can be served and the screenshot silently shows the previous version - it did, showing "Time" and "設定" after those strings had already changed. The URL now carries a cache-buster that the harness forwards to the inner iframe.
2026-09-26Layout: move the pie into the left column, unify the calendar bars, label ↵charlie
event bars Seven requests. The first one is the important one: the bottom band was dead space on every frame, not just the calendar. Measured before the change: the bottom row is 131px tall and the pie disc occupies only x=1134-1265, so 874px of that row was empty on every frame. After moving the pie to the bottom of the left column the month grid runs from y=90 to y=1022 - five rows on a 187px pitch with 2px left over, where before it stopped at y=740 with 284px unused. The grid now uses the whole height. Recorded as 2.25. Why the left column: the pie is an always-on control whose grammar is "touch and drag, there is no open action" (2.19), so its position does not change how it works. The left column already had a weight-based gap below the five frames, so the pie consumes no new space, and the bottom-left is still the bottom of the screen - the one-handed reach argument in 2.22 (Hoober 2013, Fitts) survives. The narrow layout is unchanged: there the tab bar owns the bottom edge, and the FAB/pie left-right pair was fixed in 2.22. The calendar's three levels now share one bar (2.26). Left chevron, centred value, right chevron, and on the month view a Today pill at the trailing edge. The centre is pressable and goes up a level. The year view gets the same shape and its chevrons step a year, with the state range widened to 120 years so years connect rather than stopping two either side. The day view keeps the centre pinned to today, as asked, and moves between days with the chevrons and the horizontal strip; the date being viewed is shown below the bar rather than in it, because the centre is now a fixed label. LazyRow's deferred realisation is what keeps the continuous day strip from getting heavy - the same mechanism the month view already uses through its start/end range. Month event bars can now carry text. This one is deliberately conditional: nothing measures whether text inside a calendar cell is readable at a glance, which is the same gap recorded for the weather icon, so the label appears only when there is one event in the day (a single bar can be tall enough) and is ellipsised when long. Multi-event days keep the plain colour bars. The design note says this is a retreat path, not evidence that text is better. 2.27 records that Calendar opens at the month view and returns to it, since a month is the granularity most "when is it" questions start from. Also confirmed for the narrow layout: the ASCII the request refers to already exists in 2.22 - search at the top, the five-item tab bar with settings at the bottom edge, and the FAB/pie pair directly above it. Nothing there changed.
2026-09-26Weather icon: seven states, own palette, no compound conditions on Homecharlie
Three requests in one, all recorded in 2.24 and 2.24.1. 1. Stop copying the xpm. dwgo's XPM files were converted to PNG and their dominant colours measured (six-colour reduction, top entries): clear sky #98D6D3, rain sky #206AB8, snow sky #38A4F4, fog #D4DEE7, dust #D8C9A0, tcu #B3C2BB, brk #E2F1F0. Those are now REFERENCE values, not the palette. dwgo's clear sky is a greenish teal, which sits badly against this app's wine-and-charcoal theme, so the hues are pulled toward blue and only the value relationship is kept - light at the top, darker at the bottom. The chosen colours live in one object, WeatherInk, and the drawing is this project's own. dwgo is GPL-3.0; nothing of its art is bundled. 2. More conditions. Seven states now, chosen by whether they change a decision about going outside: clear, cloudy, rain, snow, thunder, fog, dust brk and parts fold into cloudy (one overcast is enough); hail folds into rain (it is still "something is falling"); thunder, fog and dust are promoted to their own states. Thunder gets a bolt rather than dwgo's towering-cumulus cloud alone, because the state exists here to warn. Ordering when several apply: precipitation, then thunder, then visibility, then sky. The first half follows dwgo's comment ("rain, then fog and then sky conditions"); the position of thunder is FSMP's own call, since dwgo files tcu under sky. 3. Compound conditions ("rain then clear"). Home shows ONE current state and no compound. Three reasons, in 2.24.1: - The JMA's own terminology page defines "のち" and then advises against it: "possible to avoid, aim to give the specific time band; for example 'clear then cloudy' should be 'clear, cloudy from early afternoon'". It also defines 一時 as under a quarter of the period and 時々 as under half, i.e. these words are encodings of uncertainty that a reader has to already know. - METAR has no "then". Rain that has stopped is RE (recent weather, a past fact - FAA AC 00-45H via BOM), and NSW may not be used in the initial period. Transitions live in TAF time groups (FM/TEMPO/BECMG). The data model itself splits by time rather than carrying a sequence. - Home answers "what is it like right now" - the same question Dockspace names as the widget's whole job. A compound answer does not answer it. So: Home one state; the day view lists states by time band; the weekly view picks the more consequential state for a mixed day; and the detail behind a tap shows the breakdown along the time axis, which is where "rain then clear" actually belongs. Two drawing bugs worth noting since both were invisible from the code: - a local `fun cloud(...)` could not resolve drawCircle, because local functions do not inherit the enclosing DrawScope receiver - nineteen unrelated "Unresolved reference" errors all came from one comment containing "pixmaps/*.xpm" - the "/*" inside it opened a nested comment that never closed, so the rest of the file parsed as comment
2026-09-26Weather icon: redraw in dwgo's illustration stylecharlie
The icon was a flat yellow disc with eight spokes, which conveyed "sun" and nothing else, and did not distinguish the states the way a weather glyph needs to. dwgo's pixmaps are the reference now: they are 64x64 XPM images, converted to PNG and inspected directly, and the drawing method was copied from what they actually look like rather than from a description. What was taken from the images: - a vertical sky gradient as the glyph's background (light blue at the top, deeper blue at the bottom), which is what gives the set its atmosphere - clouds as overlapping filled circles with a white top and a darker grey underside, so the mass reads as volume rather than as an outline - rain as thin slanted lines, not drops - snow as one large six-armed crystal with branches, centred - the sun as a pale yellow disc with a soft halo, offset to the upper right - the palette itself: sky #B3E5FC to #4FC3F7, cloud #FFFFFF over #78909C, sun #FFF9C4, rain #DAE9F7 Licensing: dwgo is GPL-3.0. No image file is copied or bundled - only the drawing method is reproduced, in this project's own Canvas code. FSMP therefore stays under its own terms, and section 2.24 records dwgo as a design reference rather than a dependency. The glyph is drawn at 28dp rather than 20dp so the cloud shading and the snowflake branches survive; at 20dp they collapsed into blobs and the whole point of the style was lost. The tap target requirement is still met because the whole chip row is the button, not just the icon. Two bugs found while wiring this up, both worth recording because they presented as something else entirely: - A local `fun cloud(...)` inside the draw scope could not resolve drawCircle: local functions do not inherit the enclosing DrawScope receiver. It is now a top-level DrawScope extension. - The file failed to compile with nineteen "Unresolved reference" errors across unrelated functions. The actual cause was a comment containing "pixmaps/*.xpm": the "/*" inside the string opened a nested comment that never closed, so everything after it parsed as comment text. One line was responsible for all nineteen errors.
2026-09-26Design + UI: read dwgo's source, and keep four weather statescharlie
dwgo is real. The earlier note that it could not be identified was wrong: dwgo is "Don't Want to Go Outside", a dockapp-type weather application at https://github.com/gasparfm/dwgo. Its source settles the question of how many weather states an icon should carry, so section 2.24 now cites it. What dwgo does. It maps METAR codes straight onto the background image, with eleven themes (default, clear, cloudy, broken, tcu, rainy, snowy, hail, foggy, dust, otparts - src/dwgo.cpp:322) chosen from three groups: precipitation (rain/snow/hail), visibility (fog/dust/other, covering FC/FU/HZ/SA/SS/VA) and sky (clear/cloudy/brkovc/tcu). It picks exactly one, in a fixed order, and the source comment says why: "The most important for the display theme is rain, then fog and then sky conditions". One background can be shown, so the most significant phenomenon wins. Why FSMP still uses four states. dwgo's granularity comes from the observation vocabulary, not from what a person can read at a glance - the codes exist, so it distinguishes them. Nothing in it measures whether fog is tellable from cloud in a glance, and the number-versus-icon question has no study at all (recorded in 2.24), so there is no evidence base for a finer scale. Apple's Weather widgets and the shipping Dock-type widgets both keep to current conditions, so FSMP stays with clear/cloudy/rain/snow. What FSMP does take from dwgo: a defined precedence when observations overlap. Precipitation beats visibility beats sky, in that order, because rain matters more to a decision about going outside than the state of the sky. That ordering is borrowed from dwgo's design judgement and is not FSMP's own measurement; the code says so, and reduced visibility folds into cloudy rather than becoming a fifth state. This is a design correction in one direction and a refusal in the other: the earlier text implied the granularity was simply unknown, when in fact a working dockapp shows a finer scale is easy to implement - the reason not to is that nothing shows it is readable.
2026-09-26Home: add the weather chip from design 2.24charlie
Home's top-right now carries current temperature, a condition icon and high/low. The design and its evidence are recorded in section 2.24; this commit is the implementation. The three items are exactly the three the evidence converged on: Apple's HIG states the Weather widgets prioritise "current high and low temperatures and weather conditions", and Dockspace - a shipping Dock-type widget - describes the same three and says it is "intentionally minimal. It does not try to show a 7-day forecast." So no seven-day outlook on Home; that belongs behind a tap. The icon is drawn on a Canvas rather than shipped as an asset, and the condition is rounded to four states (clear, cloudy, rain, snow). Rounding is deliberate: there is no evidence that a glance can distinguish finer categories such as fog, sleet or thunderstorm, so claiming that resolution would be unsupported. Colour changes only with the condition, never as a severity signal. Simmons & Sutter 2009 (DOI 10.1175/2009wcas1005.1) found a one-standard-deviation rise in the false-alarm ratio associated with 12-29% more expected deaths, so spending warning colour on ordinary rain is not free. Official advisories and warnings are the only things that will recolour this later. High/low is rendered smaller than the current temperature because the HIG notes people care most about current conditions - the hierarchy follows the stated priority rather than treating all three as equal. Not yet wired: the values are samples. Section 2.24 fixes the DISPLAY; the data source is still open, and so are the icon's final form, the default unit, and the exact colours for advisory levels.
2026-09-26UI: horizontal calendar cells that fill the window, day stepping, radial ↵charlie
menu labels Calendar height (this took several measured attempts to settle). The month grid has to both fill the height it is given AND not clip the last week, and the library's two height modes each give one but not the other: - Wrap lets the cell decide its height (aspectRatio or height). With aspectRatio(1f) the row height follows the width, so at 1280px wide a 6-week month wants 1093px and the last week is cut off. - Fill spreads the month's days over the parent's height, but it distributes over THAT MONTH'S WEEK COUNT. A 5-week month therefore stops after 5 rows: measured 130px pitch, rows ending at y=740 with 284px of ground still unused below. Removing the cell's fillMaxHeight collapses cells to 26px instead. So neither mode does what this screen needs. The month's week count is computed, the available height is divided by it, and the row height is passed into the cell. Measured after the change: 5 rows on a 157px pitch running to y=874, i.e. the full allocation. The 150px below that is the pie's row, not dead space (verified by sampling: the pie disc occupies y=887-1011). Cells are now horizontal rather than square, matching Google Calendar's month view, and the date sits top-left where a wide cell reads better. Close is gone. Its action was "scroll back to this month", which the Today button already did, so it was a second control for one job. Month view no longer carries a Today button (asked for). The month is reached with the header and chevrons, and the year view also returns to it. Day view keeps only the two controls that were asked for: previous/next day and Today. Stepping reuses the month view's chevrons so left and right keep one meaning. API note: kotlinx-datetime 0.7.1 exposes plus(Int, DateTimeUnit.Day); there is no plusDays on the common LocalDate (checked with javap against the klib's JVM artefact). The radial menu now names what is held. Kurtenbach & Buxton 1993 (CHI '93, DOI 10.1145/169059.169426) note that "menu labels strongly suggested the correct angle", which is what kept confusions away, and they call for a follow-up with less suggestive labels - so the label is doing real work in direction recall. Callahan et al. 1988 (CHI '88, DOI 10.1145/57167.57182) also record a subject complaining that the pie "occluded the target prompt", so the held item's name is drawn OUTSIDE the disc rather than inside it, and only for the one item being held - naming all of them at once would refill the disc with text and recreate the occlusion complaint. The 132dp diameter is justified against the tap-size floors (Material 48dp, Apple 44pt, WCAG 2.2 SC 2.5.8 24 CSS px), and Kurtenbach & Buxton 1991 (UIST '91) put the useful ceiling at 12 slices for even divisions, so four quadrants plus children sits well inside the limit. Its own note: the earlier report that "the colour does not change between dark and light" was a measurement fault on my side, not a bug. The theme toggle does work - the sidebar ground measures #313338 dark and #F2F0F0 light when toggled. The pixel checker I wrote was sampling every 5px and skipping the thin strokes; verify-shot.py now samples densely enough.
2026-09-26UI: drop Close, fill the calendar to the window, allow day stepping, theme ↵charlie
the text Six requests from the review, plus the reason the earlier verification was wrong. 1. Close removed. It had nothing to close in a month view - its action was "scroll back to this month", which the Today button already does, so it was a second control for the same job. 2. The month grid now uses the height it is given. Wrap + weight(1f) was still clipping: Wrap sizes rows from DayCell's aspectRatio(1f), so it never used the height that weight handed it, and the 6th row came out 52px tall with 150px of ground left unused below. Measured, not eyeballed: before - rows at y=90..874 with the last 52px: CLIPPED, 190px unused after - six rows on a 131px pitch, y=90..874: complete, 150px unused ContentHeightMode.Fill is the mode whose documented contract matches weight(1f) ("the days in each month spread to fill the parent's height"), so the grid now divides the available height into six equal rows. 3. Day view can step to the previous/next day. It could only be left via the back arrow, so seeing the next day meant going back to the month grid and picking it again. The step buttons reuse the month view's chevrons so left and right keep the same meaning. API note: kotlinx-datetime 0.7.1 has plus(Int, DateTimeUnit.Day) and minus(...); plusDays/minusDays do not exist on the common LocalDate (verified with javap on the klib's JVM artefact). 4. Text colours follow the theme. The date digits were hard-coded Color.White, so in the light theme they were white on a white cell and effectively invisible - this is what "the colour does not change between dark and light" was describing. Every heading-band and cell colour now comes from CalendarColors; only text sitting on a coloured event block stays white, which is correct in both themes. 5. The font has one override point. WebTheme takes a fontFamily argument (default: the bundled Zen Kaku Gothic New) and feeds MaterialTheme's typography, so no screen sets a family of its own. Passing FontFamily.SansSerif switches the whole app to the system font: WebTheme(fontFamily = FontFamily.SansSerif) { ... } Verification method changed. Vision reading of these captures has been unreliable - it reported "the whole month is shown to the 30th" for a capture whose last row was 52px tall, and described a one-cell highlight as a two-cell block. Checks that can be answered from pixels now use /root/shots/verify-shot.py instead: it reports the sidebar background (dark vs light), the nav highlight band, the cell-row runs with their pitch, and warns explicitly when the last row is shorter than the pitch (clipped) or reaches the bottom edge.
2026-09-26Wasm shell: fix the Japanese font lookup so text renderscharlie
The web shell never loaded its font. webfont.js stores the bundled TTF as base64 in window.__orgflowFontB64, but the fetch/XHR shim in index.html only looked for globalThis.__orgflowFontBlobs, which nothing ever created. Every font request therefore fell through to the real fetch, the nested composeResources path does not exist on the deploy server (its FTP forbids MKD, which is why the font is shipped flat in the first place), and the app rendered with no font at all. Visible effect: every date number and every Japanese label came out as tofu boxes. Confirmed by capturing the running app - document.fonts reported "loaded n=0", and the calendar grid showed squares where the digits belong. Fix: decode __orgflowFontB64 into the Blob map the shim expects. The decode runs once at parse time and again lazily on the first matching request, because webfont.js is loaded after this script and may not have run yet when the first font read happens. Two other things were established while chasing this, and are worth recording because they look like application bugs but are not: - The month grid appeared to stop at the 19th. It was not a layout bug: the browser chrome left only 650px of viewport, and a 7-column month grid needs roughly 190px per row, so six rows cannot fit. The screenshot tool now runs the browser in kiosk mode and sizes it to the full screen (1280x1024), and the grid shows the whole month plus the out-dates. - The calendar's "today" cell looked like a two-cell red block in one capture. It is a single cell; the earlier reading was off because the viewport was small and the cells were being squeezed.
2026-09-26UI: stop the pie from overlapping content on every framecharlie
The review screenshots showed the pie widget covering the Timeline's message input (right end) and the calendar's cells for Oct 2-3. It also explained a symptom reported earlier as "dates cannot be pressed". Cause: MainArea laid the content out as Column(Modifier.weight(1f).fillMaxSize()) Combining weight with fillMaxSize makes the child try to fill the parent's whole height instead of the height that weight handed it, so content spilled into the region occupied by the pie and the tab bar and got covered. Fix: give the content column weight(1f) plus fillMaxWidth() only, in both the wide and narrow scaffolds, and keep the pie in its own row so it takes space rather than floating over content. The month view's earlier 72dp reservation is still there as a second line of defence, but the real fix is here - it applies to every frame, not just the calendar. Why not float the pie: Pibernik et al. 2019 (Future Internet 11(7):148) tested FABs against toolbars and rejected the hypothesis that they improve usability, and no study establishes a safe overlap margin. With no evidence supporting an overlay, spending layout space is the conservative choice, and it makes "cannot press the dates" impossible by construction. Also visible in the same review and already correct: light theme now renders black-on-white with the full grid to Oct 3, the Today button is present, the sidebar date duplication is gone, and Home shows the Live-style layout with a NEW badge and unread dots.
2026-09-26UI: make calendar colours follow the theme, and answer the layout questionscharlie
Everything here comes from the light-theme review. The root cause of most of it was one mistake: the calendar carried its own dark palette, so switching to light left it dark while the sidebar went light, and text that used MaterialTheme.colors.onSurface turned black on those dark surfaces. Colours now travel with the theme. WebTheme publishes a CalendarColors via CompositionLocal holding ground, cell, header, today, the matching "on" colours, and the event palette. Both themes define their own; screens read from it and never hard-code. Example 3's mechanism (cell offset from the ground so the 1dp gap reads as a gridline) is kept; only the direction of the offset differs - dark goes lighter than the ground, light goes white on the light ground. This fixes: the calendar staying dark in light mode, the day view's hour labels being dark-on-dark, the year view's date numbers being invisible, and the Close button's white text on a light header. Contrast is held to WCAG 2.2, checked against the W3C text rather than from memory: SC 1.4.3 requires 4.5:1 for body text (3:1 for large), SC 1.4.11 3:1 for UI parts. There is no relaxation for secondary text, so the dimmed text throughout was raised (alpha 0.5-0.6 to about 0.78). The old values were very likely below 4.5:1, which is what "the font is black and I cannot see it" was describing. APCA was not used: it does not appear in the WCAG 3 draft, which still says the contrast algorithm is to be determined, so WCAG 2.2 is the operative standard. The pie is no longer overlapped. The worry was that a floating pie over the grid makes dates untappable. There is no evidence for overlapping it: Pibernik et al. 2019 (Future Internet 11(7):148) tested FABs against toolbars and rejected the hypothesis that they improve usability, and no study measures a safe overlap margin. So instead of laying the pie over content, the month view now reserves 72dp at the bottom for it and puts the Close button inside that reserved band. Nothing is covered. Answers to the open questions, with the evidence that settled them: - Week view on Home: no. Chipley & Barlow 2007 tested dashboard folklore and could not support a fixed item count; the "7 plus or minus 2" rule is specifically criticised as misapplied in IEEE Trans. Prof. Communication 2002. Adding a whole week also inserts a granularity between the year overview and the day detail without a clear question for it to answer, so Home stays on today (the week belongs to Calendar). - Weather: only as ambient information, if at all. No research exists on weather in a school app. Matthews 2006 (DIS) gives the conditions for glanceable peripheral displays - it must not demand attention, changes must be graspable at a glance, and detail must be reachable. Iqbal & Bailey 2008 (CHI) show notifications cause interruption cost. So: a small current condition and temperature, colour change only when it matters (rain, extreme heat), tap for the week, and no notifications. Whether it belongs at all is still a product decision, not an evidence-backed one. - Year view density: Shneiderman 1996 (IEEE Symp. Visual Languages) sets overview-first, zoom and filter, details on demand; Hornbaek et al. 2011 (Personal and Ubiquitous Computing) supports overview+detail on mobile. The year view therefore shows one bar per day with events rather than a full stack - density at the overview level, detail when you zoom in. Also in this pass: a Today button in the month header returns to the current month (asked for), and the year view now marks days that have events using the same bars as the month view. The duplicated date is gone: the sidebar showed "月日 6限目" while Home showed "今日 9/22(火) 3限". Home keeps it, the sidebar drops it.
2026-09-25UI: finish the calendar (Example 3, year view, day view) and align Home/Livecharlie
Six changes requested in one pass, all compile-verified for wasm, jvm and android and pushed to the local preview server for review. 1. Fix: the month grid was cut off when the width was tight. HorizontalCalendar defaults to ContentHeightMode.Fill, whose documented contract is "the days in each month spread to fill the parent's height" and which therefore requires the calendar itself to be given a height. We gave it only fillMaxWidth, so the height allocation was indeterminate and the last weeks vanished (observed: rows ending at the 19th or 26th). Setting ContentHeightMode.Wrap hands the height back to DayCell's aspectRatio(1f), which derives it from the seven columns' width, so the grid is complete at any width. 2. Calendar now matches the library's Example 3 (fetched the real sample source rather than working from the screenshot): - cell: aspectRatio(1f), 1dp border only when selected, padding(1dp), background; the "gridlines" are that 1dp gap letting the page background show through, not drawn lines - date: top-right (align TopEnd, top 3dp / end 4dp), 12sp - event bars: bottom-centre column, fillMaxWidth, 5dp tall, 6dp apart, bottom padding 8dp - month title: 40dp bar, 22sp, Medium, centred, arrows on both sides - weekday row: 12sp, white, FontWeight.Light The previous attempt copied the screenshot's look approximately (bars at 55% width and 3dp, date centred); the sample is the authority. 3. Event bars and colours now follow the background instead of hard-coding Example 3's palette. Example 3 sits on #0E0E0E, this app sits on #313338, so reusing its values made the calendar look like a black hole. The mechanism (cell one step lighter than the ground, so the 1dp gap reads as a gridline) is kept and only the values are mapped onto the theme's ramp: ground #313338, cell #3A3C42, header #2B2D31, today wine #5A1524. Values live in WebTheme.kt so no screen carries its own palette again. 4. Day view rebuilt on the Google Calendar model. It was a table of time-and-item rows; it is now a fixed hour height with a long vertical surface, events placed at their start time with height proportional to duration, hour labels down a narrow left column, and vertical scrolling to reach other times of day. Event colours reuse the month view's pie-quadrant colours so the two views agree. 5. Calendar drills down three levels: year -> month -> day. Tapping the month title goes up to the year; tapping a month in the year view comes back down to that month; tapping a day opens the day view. Reason: reaching a distant month otherwise means pressing the arrow many times, and "when is it" questions often start from a year-level overview. Uses the library's VerticalYearCalendar (2.7.0 has it under com.kizitonwose.calendar.compose, gated by ExperimentalCalendarApi - the package is NOT ...compose.yearcalendar, which is where the state's remember function lives). 6. Home and Live now share one shape: section heading + full-width card rows with a right-aligned note. Home adds what Live does not have - NEW and unread - shown as a count badge on the heading and a dot plus bolder text on the row, reusing the same red as Live's on-air indicator so "live" and "new" read as the same kind of attention. Live's on-air row is emphasised with a dot in the heading, a filled watching button, and bold title. EventKind and the calendar colour constants became internal so Home/Live can share them instead of each screen defining its own copy. Remaining known gaps: sample event data is still hard-coded; the Close button resets the visible month because the month view has nothing to close; the year view does not yet show event marks; and the calendar colour ramp is deliberately provisional - it is a mapping of Example 3's contrast onto this theme, not a measured choice.
2026-09-25UI calendar: match the reference image (colour bars, English header, Close pill)charlie
The month grid now follows the Calendar Library reference screenshot that was given as the target for this screen. What changes, day cell: - Events are drawn as short coloured bars along the bottom edge of the cell (about 55% of the cell width, 3dp tall). Stacked from the top when a day has more than one event, so a date and its bars never overlap. - Previously a day with events showed a single bullet "•" in the theme accent, and the old code comment argued against bars because they would collide with the date number. Putting them on their own line inside the cell removes that collision. - Colours come from the new EventKind enum, mapped onto the pie's four quadrants (section 2.5.1): year=teal, activity=blue, personal=red, committee=brown, school=gray. The reference image's five colours are used as-is rather than inventing a palette. What changes, month chrome: - Header is centred and reads "September 2026" (English month name + year) with chevrons on both sides, replacing the left-aligned Japanese "9月2026". - Weekday row uses uppercase three-letter names (MON TUE WED THU FRI SAT SUN) instead of single Japanese characters. - A thin dark divider separates the weekday row from the grid, matching the reference's grid lines. - The caption "予定ドットのある日をタップ" is replaced by a centred pill "Close" button, per the reference. In the month view there is nothing to close, so it currently resets the visible month; the intended behaviour still needs a decision. - Cell corner radius 8dp -> 4dp to match the reference's squarer cells. Colours follow the reference image exactly (sampled values): teal #008080, blue #0000FF, red #FF0000, brown #A52A2A, gray #808080. Whether these specific hues should be the app's event colours, or should derive from the theme, is not settled by the reference alone and remains open. Sample event data (3/12/17/22) is still hard-coded in eventsOf(); wiring it to real sources is separate work. Compiles for wasm, jvm and android.
2026-09-25UI: rebuild the frames to the 2.22 ASCII - no date/event overlap, no NEW on ↵Charlie Root
Timeline, Note as note buttons Calendar month (screen 3 ASCII): the day cell no longer draws an event bar. The ASCII shows only "15 16 17 [18] 19 20 21 <-today" plus the note "point = event", and the bar I had added was what collided with the date. The cell is now date + selection/today fill + one small dot, and today and selected are passed in rather than guessed per cell. Calendar day (screen 4 ASCII): back to the specified frame - a time column (850/940/1030/1120/1215/1310) with a rule per slot and that slot's items to the right, timetable and auto-source mixed on the same axis. I had replaced this with a Google-style free-floating hour grid, which is not what the design draws. The header takes a list of dates, so single-day and multi-day both work, and the weekday is shown next to the date. Note (screen 5, and 2.6): Note is the list of notes, filtered by the content kind - "全部/課題/文書/成果物" - with the notes themselves as full-row buttons (探究ノート, 数学プリント, 班レポート, 発表スライド) and a bordered entry for the unsubmitted view. The explanatory line ("残るもの") is gone; it told the reader nothing. Per 2.6 a note is a set of cards, so tasks/documents/artefacts are not sibling entries beside notes - they are what the notes contain, which is what the filter selects between. Timeline (screen 2): rows are separated by rules again (they were missing), and the NEW badge is gone - the ASCII has no badge there (only Home's timeline digest carries [NEW], which is left as is). Live (screen 6): section headings plus full-row buttons, with the teacher-only start action as a filled button. Dropped the descriptive subtitle. Home: unchanged in substance - the five ASCII blocks stay, since Home is the navigational hub and is meant to be the fullest frame.
2026-09-25UI: Home as the navigational hub, Timeline reduced to one line per post, pie ↵Charlie Root
redrawn to the 2.22 ASCII, calendar to Example 3 + hour grid Home (2.22, screen 1 ASCII): the frame now carries the five blocks the design draws - date/period header, the three count chips (unsubmitted / answer needed / confirm), TODO, today (timetable + same-day notice + deadline on one axis) and a timeline digest with the NEW marker. Home is the navigational hub, so it is the fullest frame. My previous pass had flattened it to two lists. Timeline (2.22, screen 2 ASCII): reduced to what the design actually draws - one line per post, "o <sender> <body> <attachment/badge> <time>", five samples, plus the note that feeds are per-scope and newest-first. The previous pass had turned each post into a card with a bordered panel, which the ASCII does not show and which made it heavier than Home. No destination chip: the pie shows scope. Pie (2.5.1 + the whole-frame ASCII): redrawn to the specified shape - +-------+-------+ | 一年 | 活動 | +-------W-------+ | 個人 |委員会 | +-------+-------+ one circle, cross-divided into four quadrants, W in the centre, with the personal scope pinned to a quadrant because it lives outside the organisation tree. Touching highlights a quadrant and dims the rest; releasing commits; returning to the centre commits whole. Only while dragging past 62 percent of the radius does the branch extend and the concrete (level 3) nodes appear - the design says the concrete is visible while holding and is not for browsing, so it must not be permanently on screen. Branch and nodes are drawn outside the disc, on a canvas sized for it. Calendar: the month grid now passes OutDateStyle.EndOfGrid as Example 3 does. Selecting a day opens a Google-Calendar-style day view - an hour axis from 8:00 to 19:00 filling the screen, with each event drawn as a block whose vertical position and height come from its start time and duration, timetable and auto-source entries together. It takes a *list* of dates, so the same composable serves a single day or a week of columns rather than being day-only.
2026-09-25UI: Discord-dark palette, icon buttons, and a pie that actually expands to ↵Charlie Root
three levels Palette: dark is no longer near-black. Ground 0xFF313338, surface 0xFF2B2D31, raised 0xFF3A3C42, text white - the Discord register, per user direction ("not that dark, about Discord"). Wine 0xFF8C2438 stays the accent on both grounds; light keeps the wine primary on pale grey. No purple anywhere. Icons: settings, appearance and search are drawn shapes now instead of text glyphs - a gear (teeth + ring + hub), a moon for dark / sun with rays for light, and a magnifier. All Canvas, no icon dependency, so nothing new has to resolve on Wasm. The bottom-left cluster is +, appearance, gear as circular icon buttons. Pie: it now renders the three levels 2.5.1 actually specifies (whole > top-level group > concrete), not just the four quadrants. Structure: W in the centre is level 1 (whole); the cross-divided quadrants are level 2, with the personal scope pinned to one quadrant because it sits outside the organisation tree; dragging outward past 62 percent of the radius extends a branch (the "spoke" of 2.5.1) beyond the circle and level 3 concrete nodes appear along it - 1/2/3-kumi for the year scope, photo/art/science clubs for activities, and so on. The concrete nodes are only visible while holding, and release commits whichever node is nearest; returning to the centre commits whole, which is the cancel path in 2.19. Child lists are data (a children map) rather than hardcoded UI, matching the design's "the school defines both the groups and the concrete scopes". Also fixed: the appearance icon read MaterialTheme inside a DrawScope (not a composable context) - the surface colour is now captured before the Canvas.
2026-09-24UI: stop calling kotlinx-datetime's now()/todayIn() - it cannot link on WasmCharlie Root
The UI still died at startup, now with a different symbol: IrLinkageError: Function 'now' can not be called: No function found for symbol 'com.kizitonwose.calendar.core/now|now@kotlinx.datetime.LocalDate.Companion( kotlin.time.Clock;kotlinx.datetime.TimeZone){}[0]' So replacing the calendar library's YearMonth.now() with LocalDate.now() was not a fix - it only moved the failure one level down. Every now/todayIn entry point in this stack is declared as taking a kotlin.time.Clock, and that symbol does not resolve in this combination (Kotlin 2.1.21 + kotlinx-datetime 0.7.1 + Compose MP 1.8.2, Wasm target). kotlin.time.Clock only becomes usable in later Kotlin versions, and kotlinx-datetime 0.7 is built against that newer shape. Get "today" from the platform instead: a new expect/actual todayLocalDate() with one actual per target - JS Date on wasmJs, Clock.System.todayIn() on jvm and android (where the Clock symbol does resolve). The calendar month is then built as YearMonth(year, monthNumber) from that date, which also avoids the kizitonwose Month vs kotlinx Month type clash that broke the Android compile. Wasm js(...) has its own constraint worth recording: the call must be the single expression forming a top-level function body, and the result must be a primitive/string/function - so the Date reads are folded into one arrow IIFE returning YYYYMMDD as an Int, which Kotlin then splits. Verified: compileKotlinWasmJs now passes.
2026-09-24UI calendar: build the current YearMonth from LocalDate.monthNumber so the ↵Charlie Root
Android target compiles too The Wasm-only fix for the calendar's YearMonth was not enough. Two further problems showed up once the Android target compiled the same code: 1. YearMonth(today.year, today.month) failed type inference on Android: kotlinx-datetime's Month and com.kizitonwose.calendar.core's Month are different types. 2. The obvious repair, today.month.number, does not exist either - Month has no `number` property in kotlinx-datetime 0.7.1. Take the month as an Int straight off LocalDate instead: today.monthNumber is an Int property on LocalDate (verified present in the 0.7.1 sources before use), which sidesteps the Month type mismatch entirely and works on both targets. Verified with both compilations in one run - compileDebugKotlinAndroid and compileKotlinWasmJs both pass.
2026-09-24UI: wine/ink palette with a dark default, icon pie rendered per 2.5.1, and ↵Charlie Root
button-shaped search and controls Colour (per user direction: "black background with white, and wine instead of black"): the theme now has a dark default - ink (near-black) ground, warm off-white text - with wine as the accent, and a light variant where the same wine becomes the primary against a pale grey ground with white surfaces. Purple is gone entirely. The [D/L] control in the bottom-left cluster toggles between them, and Main.kt holds the appearance state. The wine itself was desaturated and deepened (0xFF6E1B2E -> 0xFF8C2438 for the dark ground) so it reads as wine rather than a flat red on either ground. Pie: the previous attempt got this wrong. Per 2.5.1's drawing (2026-09-22, fixed) the pie is ONE circle divided by a cross into four quadrants, with the centre labelled W (whole); the quadrants are the top-level scopes plus the personal scope, which is pinned as its own quadrant because it sits outside the organisation tree. The implementation had instead lined up three separate circles beside the centre, which is not the specified shape at all. 2.19 also fixes the interaction: there is no "open" gesture (touch and drag), the centre is always whole regardless of where you press, the touched quadrant highlights while the others dim, release commits, dragging outward expands to the concrete children with a spoke reaching outside the circle, and returning to the centre cancels to whole. PieMenu.kt now draws that single cross-divided circle as an icon (Canvas arcs, dimmed/selected states, W in the centre) and tracks drag position to quadrant, committing on release. The outward expansion to concrete children and the 150-200ms expand / 100ms contract animation from 2.19 are the next step and are marked as such in the file. Chrome: search and the bottom-left controls are buttons now instead of bracketed text - a rounded search button with a glyph, and circular +, D/L, gear buttons. The narrow form's FAB is the same circular button. Also fixed here: CalendarFrame no longer calls YearMonth.now(). The Wasm build compiled it but the runtime died on load with IrLinkageError: Function 'now' can not be called: No function found for symbol 'com.kizitonwose.calendar.core/YearMonth.Companion.now' The symbol requires kotlin.time.Clock, which this Kotlin version does not resolve for that library's binary, so the calendar library''s now() is unusable here; building the YearMonth from LocalDate.now() gives the same value without it.