Release planning

CadCraft 1.0 impact map

Every fix and feature lined up for 1.0, and what each one touches: which apps, and which layers of the stack.

ProjectCadCraft
Sheet0.10 impact
Items110
Date2026-11-09
51Fix
22Feature
0Spike
28Platform
0Site ops
8Sanity check
1Decision
≈ 319days in the written 0.10 plans
  • Planned 101 on the plan for the release it is filed under
  • Built 0 built and working in /lab (the dev builds). Whether the public has it is its reach: "in the lab", "Live" (the last gate) or "switched off"; see docs/REACH-LIVE.md
  • Partly built 1 part of it is built in /lab; the rest waits (see "Waits on")
  • Promised 0 the live site already says it is coming
  • Moved 0 moved here from an earlier release or wishlist
  • Found 0 bug found after a release shipped; fixed in a point release or the next one
  • Candidate 3 recommended in a spike or session log; owner to choose
  • Community please test 0 built and in /use; a Discord post gives the steps and the expected result and asks people to try it on their own devices, files or know-how
  • Decision Time 5 a decision, check, file, key, flag or go only you can give; folds into the list once answered

Built means built and working in the lab. Live means the public can use it, and is the last gate: 0 Built · Live, 0 Built · in the lab.

Sheet 1 · impact areas

Where 0.10 lands

How many items touch each app and each layer, by kind. One item can touch several areas, so the rows add up to more than the item count. Pick a row to filter the schedule below.

Impact areaFixFeatureSpikePlatformSite opsSanity checkDecisionAll
Apps
CadCraft 20 17 – 10 – 3 1 51
BimCraft 18 20 – 9 – 4 1 52
ModelCraft 19 20 – 10 – 3 1 53
MarkupCraft 12 16 – 9 – 3 1 41
TakeoffCraft 14 16 – 9 – 3 1 43
CivilCraft 21 18 – 10 – 4 1 54
DetailCraft 15 17 – 9 – 4 1 46
PartCraft 15 17 – 9 – 3 1 45
Studio – 1 – – – – – 1
SuperCad Studio – 6 – 6 – 1 1 14
Layers
Site and services 13 3 – 8 – 1 – 25
Desktop host – 1 – 1 – – – 2
App shell 5 15 – 8 – 2 1 31
App code 23 4 – 3 – – – 30
Our engines 8 – – 1 – 1 – 10
Vendor engines – – – – – – – 0
Drawing surface 3 – – – – – – 3
Help and words 2 1 – – – 1 – 4
Tests and tooling 2 1 – 2 – 3 – 8

Sheet 2 · schedule

Every fix and feature

Grouped by track. App chips carry each app’s layer colour; Studio is in ink. "Waits on" names what has to happen first; the small lines under it say who asked and where the item is written up.

Add an item Fork map (Rust and C++ forks) The roadmap

Issues sync every 10 minutes; last sync Nov 9, 2026 7:00 pm.

ItemWhatAppsLayersStatusDaysWaits onIssues
Library bounties 1
FeatureMB.1 Link to MB.1Comment on MB.1 Member offered bounties

Members offer and fund bounties for Library items (today only staff post bounties, and staff pay by hand or by a Stripe Transfer from our own balance, test mode). A member-funded bounty needs escrow: (1) the poster pays first through Stripe Checkout and the bounty goes live only once the money is received; the funds sit in our Stripe balance tagged to the bounty. (2) The winner needs a Stripe Connect Express account (the maker payout setup); awarding a claim prompts them to set one up. (3) On accept, a Transfer (separate charges and transfers, the winner is unknown at payment time) sends the reward minus our fee, after a short hold against chargebacks. (4) Refund to the poster if the deadline passes with no accepted claim, or if they withdraw before any award. (5) The item still goes into the public Library under CC0 or CC BY; the poster gets no exclusive rights. Open owner decisions: who accepts (poster, staff only for disputes: recommended), our fee, who absorbs Stripe's fee on a refund. Reuses Makers, AddonMoney (test-mode guard), LibraryBounties::payWithStripe and the bounty tax-form threshold. Builds on LIB-BOUNTY (0.9).

– Site and services Planned – Owner: who accepts, our fee, Stripe-fee refunds; lawyer sign-off on holding member money (payout, tax and refund rules) From: Owner
Performance roadmap — wave 3 60
FixPERF.41 Link to PERF.41Comment on PERF.41 Website: Library card lists run nine queries per card: /library/author 914 queries (897 ms), /library/browse 444 (587 ms) (extends PERF.12)

Measured with bin/admin-perf.php: LibraryItems::display() (LibraryItems.php:254-266) calls LibraryVersions::byId and LibraryAuthors::publicAuthor (Auth::byId plus LibraryAuthors::uuidFor, two statements) for every row browse() already selected; item-card.inc:16 then calls LibraryPreview::forVersion, which runs LibraryVersions::fileAccess (LibraryItems::byId again, LibraryVersions.php:524), LibraryItems::byId (LibraryPreview.php:55) and LibraryFiles::byId. None of the 115 lookup methods in the service layer except one has a per-request memo. Fix (a) S: a static per-request memo keyed by id in Auth::byId, LibraryVersions::byId, LibraryItems::byId, LibraryFiles::byId and LibraryAuthors::uuidFor, cleared by the write methods: removes about 600 of the 914 statements on the author page. (b) M: browse() selects the card columns in one statement (item, current version id, license, preview file, published_at, author name, maker flag, uuid) and display() takes the joined row; fileAccess() takes the item row it already has. Target: browse under 25 statements, author under 20.

– Site and services Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.42 Link to PERF.42Comment on PERF.42 Website: Library browse sorts every published item on a computed key and counts them again on every page; at 100x it is seconds per page

LibraryItems.php:152-157 orders by COALESCE(v.published_at, i.updated_at) DESC after the join, so LIMIT cannot help: EXPLAIN shows all 13,846 rows read through idx_browse, joined, then a filesort (64 ms) plus SELECT COUNT(*) (30 ms) plus categoryCounts (:171, 30 ms) per request; OFFSET 9600 takes 98 ms. At 1 M items the same plan is 5 to 7 s per browse page. Fix: a stored library_items.sort_at kept by LibraryItems::refresh() and on publish (the current version's published_at, else updated_at); indexes idx_browse_sort (visibility, status, sort_at, id), idx_browse_kind (visibility, status, kind, sort_at), idx_browse_cat (visibility, status, category, sort_at); ORDER BY i.sort_at DESC, i.id DESC reads 24 rows; keyset paging (?after=sort_at|id) past page 20; the count and categoryCounts cached 60 s per filter key or kept in a counts table on publish. Note: one account owns 13,844 of 13,846 items today, so the author filter plans say nothing yet about a real multi-author Library.

– Site and services Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.43 Link to PERF.43Comment on PERF.43 Website: Library search is LIKE '%q%' on four columns while an unused FULLTEXT index sits on exactly those columns

LibraryItems.php:200 browseWhere runs (i.name LIKE ? OR i.maker LIKE ? OR i.model LIKE ? OR i.description LIKE ?) with %q%; the schema has ft_search (name, maker, model, description) and sys.schema_unused_indexes lists it. EXPLAIN: 52 ms scanning 13,846 rows today versus 16 ms with MATCH … AGAINST ('door' IN BOOLEAN MODE) reading 1,324 rows from the index; at 1 M items about 3.7 s versus tens of ms. Fix: for queries of three or more characters use MATCH(i.name, i.maker, i.model, i.description) AGAINST (? IN BOOLEAN MODE) with +word* terms, keep LIKE for one or two character queries and as OR i.model LIKE ? for part numbers (innodb_ft_min_token_size is 3); the ngram parser later for CJK names. The same path serves /api/v1/library/items?q=.

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
PlatformPERF.44 Link to PERF.44Comment on PERF.44 Website: session_activity and analytics_visitors hold 1.6 M rows and 1,015 MB (83 % of the database), 42 % bots, no retention, one table never read

Measured: session_activity 1.02 M rows / 646 MB growing 51,086 rows a day (21,675 of them is_bot = 1), analytics_visitors 597k rows / 369 MB growing 38,644 a day because a cookie-less client gets a fresh visitor id on every request; the table is written on every page view (Analytics.php:75) and read nowhere. Nothing prunes either (no DELETE in bin/ or the classes except the smoke test's own rows). The report queries full-scan the table (EXPLAIN ANALYZE: summary 1.8 s, topPages 2.4 s, each audience group 1.7 s, tryOpens 1.3 s), so the nightly warm job is roughly 60 to 80 s of scans that push the whole table through a 1 GB server-wide buffer pool these two tables alone fill. CadCraft analytics is about 1.1 inserts a second, over half of all INSERTs on this MySQL, each an fsync (two autocommits per page view). At 10x the warm job takes ten minutes and evicts every other database on the server. Fix: (1) do not store bot page views as rows (count per day per bot name in a small table) and give a visitor with no cc_vid cookie no analytics_visitors row until the second request; (2) retention for humans (owner's number; 30 days for any bot rows kept) as monthly partitions by TO_DAYS(created_at) dropped with ALTER … DROP PARTITION, or a nightly batched DELETE on idx_created; (3) a daily rollup table (day, url, country, region, city, device, os, browser, referrer host, campaign, lang, is_bot, views, people) filled for yesterday only, so the dashboard and reports read at most 365 rows per group; (4) stop writing analytics_visitors or read it somewhere; drop the unused idx_session. Keep the privacy page true. Combines the SQL and server tracks (sql-audit.md 4, server-api.md 9).

– Site and services Planned 4 Owner: how long to keep page-view rows (400 days keeps the year-over-year comparison the analytics page offers). docs/0.9-sessions/perf-wave3/
FixPERF.46 Link to PERF.46Comment on PERF.46 Website: staff pages with uncached GROUP BY scans: /admin/library/metrics 887 ms, /admin/library 422 ms, /admin/hardware 265 ms

LibraryMetrics::report (LibraryMetrics.php:129, :151, :156) is 430 ms of the metrics page, one statement 222 ms (a 41k-row range on uq_once_a_day, a sort and COUNT(DISTINCT who)), linear in rows (2,157 new rows a day, 4 s at 10x). LibraryVersions::possibleDuplicates (LibraryVersions.php:587-604) is 383 ms per /admin/library view, a self-join over 27k versions that finds nothing today. HardwareReports::summary (HardwareReports.php:93) runs 13 GROUP BY scans of all 25,914 rows (240 ms; idx_day cannot help because every row is inside 90 days). MailLog::countsByStatus (MailLog.php:180) runs twice on /admin/mail (53 ms); RoadmapItems::groups runs seven times on /roadmap-issues with the same arguments. Fix: Cache::remember(…, 600) around report, summary and possibleDuplicates (the dashboard already works this way); better for duplicates, set duplicate_of_version_id at publish time in LibraryVersions::create (one indexed lookup on idx_item_sha / idx_source_sha) and list that; for metrics a nightly library_metrics_daily rollup or at least a covering index (day, kind, ref, action, who) and drop the redundant idx_day; call countsByStatus and groups once per request and pass the value down.

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.47 Link to PERF.47Comment on PERF.47 Website: missing, redundant and dead indexes (DDL in the report)

Missing: library_versions.submission_id (LibrarySubmissions::outcome, LibrarySubmissions.php:353, full-scans 27,185 rows, 40 ms per call; 26,537 versions carry a submission id) → ADD KEY idx_submission (submission_id). audit_log.action (Audit::search/searchCount :157-184 with a.action LIKE ?, StripeApprovals::siteRecords :71, DiscordConversations::itemsForMatching :321, Broadcasts::version :463; 52,791 rows growing 1,820 a day) → ADD KEY idx_action_time (action, created_at). roadmap_items.item_key alone (RoadmapSuggestions::findItem :28, ReplyDrafts::usedItems :99 query without release, so uq_release_key cannot serve them; every /suggest review runs it) → ADD KEY idx_item_key (item_key). discord_messages author_id and posted_at (DiscordInbox.php:431, :569, :54, :58; low, reader paused) when the reader resumes. Redundant: library_metrics.idx_day (prefix of uq_once_a_day), broadcasts.idx_status (prefix of idx_due): drop both. Dead weight: session_activity.idx_session (nothing selects by session_id) and the three analytics_visitors indexes (table unread); library_items.ft_search is unused only because the code does not use it (the search row): keep. Each ALTER is an online operation on these sizes; run bin/install.php with a migration.

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.48 Link to PERF.48Comment on PERF.48 Website: ordinary Library page views read every published item (publicStats, LanguageNames::inUse, makers, forUser without LIMIT, a 652 KB cache per page)

LibraryItems::publicStats (LibraryItems.php:278-295) pulls every published row with its author and version into PHP to count authors and categories: 42 ms per view of /library and /library/author. LanguageNames::inUse (LanguageNames.php:77-83) joins all items to their versions (a UNION with addon_listings) on every Library page to learn that two languages exist: 28 to 44 ms. LibraryItems::makers (:268) SELECT DISTINCT maker over all published rows per browse page (fast today, linear later). LibraryItems::forUser (:221) has no LIMIT: the system account's /library/mine loads 13,844 rows. LibraryMetrics::totals is cached, but the cache file is 652 KB of JSON decoded on every card page (that is the 367 ms "PHP" of /library/browse). Fix: publicStats as one GROUP BY or Cache::remember 600 s with the key dropped in LibraryItems::refresh; cache inUse and makers the same way (or compute inUse in the seed); forUser paged by 50 like LibrarySubmissions::forUser; metrics labels read the per-ref rows (idx_ref) for the 48 refs on the page instead of the whole totals file. At 1 M items each of these is seconds; with the fix they are constant.

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.49 Link to PERF.49Comment on PERF.49 Website: every signed-in page view runs the account-menu queries (5 to 6 statements; 12 Licenses::forOrg calls on /account)

navbar.inc:36 → Organizations::listedFor → forUser (Organizations.php:181, a correlated member COUNT), then per workspace Licenses::summary → forOrg (two statements), Licenses::seatFor and StoragePlans::inForce: 5 to 6 statements and about 1.5 ms on each of the 61 pages profiled, 12 forOrg calls on /account. With one workspace per account this is small; with teams it is three statements per workspace per page. Fix: compute listedFor once per request (static memo) and keep the menu's result in $_SESSION with a 60 s stamp, invalidated by Licenses::assign/unassign/grant and Organizations::addMember/removeMember (or the touched() hook the dashboard already uses).

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
PlatformPERF.50 Link to PERF.50Comment on PERF.50 Website: the Library at 1000x (1 M items, 2.8 M versions, 10 M files): what breaks and the design that does not

Scaling the measured plans linearly from 13,846 items / 27,940 versions / 108k files / 41k metric rows: browse sort plus COUNT plus categoryCounts 64+30+30 ms become 4.6+2.2+2.2 s per page (breaks at about 100x); LIKE search 52 ms becomes 3.7 s while FULLTEXT stays tens of ms (index about 1 GB at 1 M items with descriptions: budget it); the per-card N+1 stays about 450 statements per page but 450 round trips is the floor of every page; publicStats / inUse / makers / forUser full reads 42 ms become 3 s each; possibleDuplicates 159 ms becomes 16 s per /admin/library view; library_metrics at 2 M rows a day needs the daily rollup and a 90-day raw retention; /sitemap (PERF.11) must become a sitemap index of 50k-URL files written nightly, never rendered per request; hot data 5 to 8 GB against a 1 GB server-wide buffer pool (the MySQL settings row). Fine as is: PK and unique lookups (uq_slug, uq_uuid, uq_sha256, uq_item_version), int unsigned ids, the content-addressed file store, the events feed and the notification mailer (idx_feed, idx_mail keep them O(result)). This row is the design pass that ties the Library rows together and sets the data budget before the data arrives (docs/0.9-sessions/perf-wave3/sql-audit.md 12).

– Site and services Planned 5 The Library browse/search/index rows above first. docs/0.9-sessions/perf-wave3/
PlatformPERF.53 Link to PERF.53Comment on PERF.53 Apps: 29.6 MB of byte-identical files under different URLs across the eight releases, and every new release re-downloads 98 % unchanged files

tools/perf/addons-shared.mjs hashed every file of the eight current releases: 665 distinct contents under 2,163 URLs, 29.6 MB extra on the wire for a person who opens more than one app (cabinetcraft add-on in four apps 5.8 MB, dwg/ 3.0 MB, ifc schemas in four apps 2.3 MB, regs in four 2.3 MB, m3d 1.3 MB, crs-db.json 914 KB three times, host.js / addons.js / libitem.js eight times). Across releases: CadCraft 0.8.1 → 0.8.2 kept 2,084 of 2,127 files byte-identical (158 MB) and every one is re-downloaded under the new version URL; the same for every app and every "Save for offline" re-save (PERF.15). Vendor libraries are fine (one URL each, pinCurrent works). Change: a shared /assets/lib/<name>/<content-hash>/ URL for lib/* copies and shared add-ons (release-app.php pins it like vendor paths; the import map aliases ./dwg/x.js to it), or content-hashed file names so an unchanged file keeps its URL across releases. Both shrink the import map, which lists every file because ?v= busting needs an entry per module (499 entries / 48 KB in CadCraft, 772 / 78 KB in BimCraft per page view; AppBuilds.php:391-397).

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft Site and servicesApp shell Planned 8 – docs/0.9-sessions/perf-wave3/
FixPERF.54 Link to PERF.54Comment on PERF.54 Website: /library/kits takes 1.3 to 1.45 s and 496 queries; /library/addons 113 queries

FieldPacks::byId() iterates all(), which re-reads and re-checks 74 pack.json files (949 KB) per call, once per kit row (FieldPacks.php:116-137, library/section.php:162): about 5,500 json_decodes per request; coverage() asks AddonLibrary::byListingId per member (405 queries, FieldPacks.php:154) though AddonLibrary::all() is already cached. /library/addons: AddonLibrary::media() once per card (section.php:24, 93 queries). Fix: memoize FieldPacks::all() per request (a static cache like AddonLibrary::$cache), byId from that map, coverage() from AddonLibrary::all()[$mid]; one SELECT … WHERE listing_pk IN (…) for media (mediaCounts() already exists for counts). Expected: kits under 150 ms and about 20 queries. Note for bin/admin-perf.php: rewritten routes need the real file path (/library/section?section=kits).

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
PlatformPERF.58 Link to PERF.58Comment on PERF.58 Apps: an installed add-on package is relinked from blob modules on every start (hvac 334 ms, plant 756 ms) and ships as base64 JSON

addons.js linkPackage turns each verified package into blob: module URLs every start: hvac 0.9.0 (193 files, 1.8 MB; 2.4 MB as base64 JSON) costs 334 ms per start (link 115, import 55, verify 6); plant 756 ms (import 447). The Ed25519 plus sha256 check is cheap (6 to 11 ms for 100 to 193 files); blob modules get no HTTP cache and no V8 code cache, so the cost repeats on every visit and grows with file count. Change: keep the signature check but serve verified packages from the service worker under a stable /addons/pkg/<sha>/<path> scope (or cache the linked texts and import() a persistent URL) so modules get code caching; stop base64 in the package JSON (binary zip, about 34 % smaller). AddonPackages.php, addons.js, sw.js.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraft App shellSite and services Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.63 Link to PERF.63Comment on PERF.63 Website: a distributed crawl of /roadmap-issues filter combinations is about half of all PHP page requests (485,944 in 8 days, 172 ms and 1.4 MB of HTML each)

Access logs: 485,944 requests to /roadmap-issues in 8 days (184,651 on Nov 6); in the last 5 hours 24,548 requests from 17,356 distinct IPs with browser user agents and fake referers, every permutation of ?app=&channel=&kind=&layer=&release=; 23,831 of the last 49,776 dynamic requests. Each costs 172 ms of PHP, 23 queries and 1,416 KB of HTML (112 KB gzipped): about 23 CPU-hours and 54 GB of egress in 8 days, invisible on /admin/analytics because Analytics::SKIP_PREFIXES has /roadmap-issues. The crawl ignores robots, so per-IP throttling cannot help. Change: render the item list once per release into Cache (60 s) and filter from it (172 to about 20 ms); rel="nofollow" on every filter link and a canonical URL per release; if the page stays reachable by link, filter client-side on the cached list. Removes about 45 % of PHP CPU and most HTML egress today; the page gets 8x cheaper for staff.

– Site and services Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.64 Link to PERF.64Comment on PERF.64 Website: a session file is written for every anonymous page view (312,607 files, 1.3 GB, 92,632 a day, GC 38 s)

sessions/ holds 312,607 files, 98.8 % under 120 bytes (a CSRF token or nothing), 92,632 written in 24 h and still 7,574 an hour after today's crawler change; the directory inode is 142 MB (a directory never shrinks); session-gc takes 38 s every 6 h (38,589 CSRF-only files removed per run). Every page view is open, flock and write of a file; the per-session lock also serializes one visitor's parallel requests. (FX.136 shortened the lifetime; this removes the cause.) Change: start the session lazily, only when a cc_session cookie arrives or something is written (csrf_token(), flash(), sign-in); a signed double-submit CSRF token needs no session for visitors; then recreate sessions/ once. About 90k fewer file writes a day and GC in a second.

– Site and services Planned 3 – docs/0.9-sessions/perf-wave3/
FixPERF.65 Link to PERF.65Comment on PERF.65 Website: Vary: Referer on every response, assets included, so the browser caches each shared module per referring page and no shared cache can hold them

public_html/.htaccess:28 wraps the lab no-store rule in <If "%{REQUEST_URI} =~ … || %{HTTP_REFERER} =~ …">, and Apache adds Vary: Referer,Accept-Encoding to every response that passes it, cadcraft.js included. A browser's cache entry for a shared file is then keyed by the referring page: the same assets/js/*.js fetched under /use/bimcraft after /use/cadcraft, or /library, misses; a CDN or shared cache cannot store the assets. With 400 requests per app start this counts. Change: keep <If "%{REQUEST_URI} =~ m#^/assets/apps/[^/]+/a Discord member/#"> for a Discord member builds and send Cache-Control: no-store for lab pages from PHP (header.inc when is_a Discord member) instead of the Referer match.

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
PlatformPERF.68 Link to PERF.68Comment on PERF.68 Server: disk at 75 % with 90 GB reclaimable (a 56 GB Rust build tree, tmp/ build trees, both 0.8 downloads, a lang-overlay cache); logs rotated monthly

Measured: / is 290 of 388 GB. desktop-shell src-tauri target 56 GB (the forks' target moved to W: on 2026-11-08; this one did not; memory rule: never rebuild forks on the VPS), stores/build 18 GB, tmp build trees about 13 GB (editions, public-repos, studio-suite-0.9-a Discord member, devshell, live-apps-www*), downloads 0.8 and 0.8.1 both on disk (2.1 GB each; 0.7 and older moved to the CDN), three byte-identical frozen copies per app (hardlinking would save about 1.3 GB), tmp/cache/lang-overlay 981 MB with no TTL; access.log 345 MB a day uncompressed for a month (2.76 GB now). Noise: mec-broker logs two lines a minute when idle, a Discord member-sandbox-sync logs the same schema drift (team_join_codes.used_at) every 10 minutes, mail-poll logs a PHP array_merge(stdClass) error every 5 minutes (a ContactMail bug). Changes: move or delete the Rust target, clean tmp/ after each release, TTL on lang-overlay, hardlink identical files across frozen versions in bin/release-app.php, publish 0.8 downloads to the CDN like 0.7, weekly (or daily access) logrotate, quiet the idle broker line, fix the sandbox drift and the mail-poll error. About 90 GB back (75 % to about 52 %).

– Site and servicesTests and tooling Planned 1 Owner: /etc/logrotate.d is server-wide; tmp/downloads-replaced (4.7 GB) is the "never deleted" policy and stays. docs/0.9-sessions/perf-wave3/
FixPERF.69 Link to PERF.69Comment on PERF.69 Website: the developer API makes three writes per keyed call (last_used_at, throttle, request log), health polls included

DevKeys::inspect runs UPDATE a Discord member_api_keys SET last_used_at, Throttle::hit an INSERT and DevApi::log an INSERT on every call, 200s on /v1/health and 404s too; each autocommit is an fsync with innodb_flush_log_at_trx_commit 1. Load test: api host /v1/health 679 req/s, /v1/endpoints 763, /v1/status 364 at c=10, zero failures (the floors are fine: health 3.6 ms). At 1000x (an integrator polling once a second) the writes are the cost. Change: update last_used_at at most once a minute per key, skip the request log for health 200s, keep it for everything else; a Discord member_api_requests is pruned at 30 days already.

– Site and services Planned 1 – docs/0.9-sessions/perf-wave3/
Sanity checkPERF.70 Link to PERF.70Comment on PERF.70 Server: operations checks: two cron jobs without a lock, status-check's serial probes, no Apache scoreboard, static files gzipped per request

bin/mail-poll.php (IMAP, every 5 min) and bin/a Discord member-sandbox-sync.php (every 10 min) have no flock while every other job has one; status-check runs 7 probes serially with 12 to 40 s timeouts plus 5 s retry sleeps (about 2 min worst case inside a 5-min slot; MeCentral timed out once on 2026-11-07). mod_status is on (Require local) but /server-status is rewritten to the 404 page by the catch-all .htaccess, so there is no scoreboard for bin/admin-perf.php --http or future measuring. Static JS is gzipped on every request: cadcraft.js 198 to 59.5 KB costs 9.0 ms p50 against 0.7 ms for an uncompressed file through the same .htaccess, 147 req/s at c=10 and p99 1,029 ms at c=20 (PERF.16 carries the fix: pre-compressed .br/.gz siblings and an immutable one-year cache). Changes: add the flock, run the probes with curl_multi or cap the total at 60 s, a <Location /server-status> with RewriteEngine Off in this site's vhost. Load floors for the record (c=10, keep-alive, zero failures): health 775 req/s, catalog 303, /status 189, / 80, /apps/cadcraft 58, /use/cadcraft 17.4 req/s at p50 586 ms (its 830 KB page is the throughput ceiling: PERF.2, PERF.3).

– Site and servicesTests and tooling Planned 1 – docs/0.9-sessions/perf-wave3/
FixPERF.77 Link to PERF.77Comment on PERF.77 Code: PHP service layer: 1,457 prepare() with no shared helper, LibraryResult unused, 35 classes with their own ok()/fail(), components re-running one query

tools/perf/quality-php.php: the $st = db()->prepare(…); $st->execute([…]); $st->fetch(…) idiom appears 666 times across 204 classes, six classes define a private row() helper of their own, 626 hand-written ['ok' => …] results while LibraryResult exists and is used by zero classes. Pages where the controller and its components call the same service method with the same arguments more than once: /admin/library LibraryReports::countOpen() three times, /admin/messages Contact::countsByStatus() three times, /supercad SuperCadCheckout::seatsLeft() four times, the navbar Downloads::current() twice on every page. Change: db_row/db_rows/db_val/db_exec in core/functions/helpers.php and result_ok/result_fail (or adopt LibraryResult), added once (S) and adopted class by class as they are touched (M); pass page values down to components instead of re-querying. The SQL track's memo row covers the lookup side.

– Site and services Planned 3 – docs/0.9-sessions/perf-wave3/
Interface roadmap — wave 1 26
FeatureUI.20 Link to UI.20Comment on UI.20 Theme switch: sun and moon icons for light and dark wherever the interface says "Paper" and "Model"

Owner 2026-11-09: where we say "paper/model" we switch to sun/moon icons for the light and dark themes. Today the theme toggle is a two-button form with the words Paper and Model: the site navbar (navbar.inc, titles "Paper space: light theme" / "Model space: dark theme"), the app bar in every app and its More menu on phones (app-bar.inc, words shell.bar.paper / shell.bar.model and their titles), the Settings radios (app-settings.inc already draws bi-sun next to "Paper"; shell.settings.theme.paper / .model), and the pre-paint script and site.js that read them. Change: one shared toggle component (space-toggle.inc) drawn as two icon buttons, a sun for light and a moon for dark, from the CadCraft icon set (UI.14: two new drawings, sun and moon, in all three styles) with the pressed state lit, aria-labels and tooltips "Light theme" / "Dark theme" (plus "Match the system" where Settings offers it), the words shell.bar.light / shell.bar.dark in en, es-MX and id and the old keys retired by bin/test-shell-lang.mjs; the navbar, the app bar, its More menu, Settings and the Studio hub use the one component. What stays: the stored values paper / model (localStorage cc-space, the cc_space cookie, data-space) and the brand file names cadcraft-logo-{paper,model}: internal names, nothing a person reads, so no stored setting breaks. What this does NOT touch: CadCraft's real paper space and model space (the drawing's layouts and the MODEL / PAPER status toggle), which keep their names because that is what they are. Help: the theme line in common/getting-started topics; the FAQ answer that names Paper and Model, if any, re-read. Owner addition the same day: in the apps the sun and moon are one row of two icon buttons inside the Craft menu, which opens from the app mark and name at the far left of the absolute top (UI.8, UI.24); icons in a row, never list items; the website navbar keeps its own pair.

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraftStudioSuperCad Studio App shellSite and services Planned 1 – docs/0.9-sessions/interface-wave1/SUMMARY.md
FeatureUI.23 Link to UI.23Comment on UI.23 Layout designer page in the developer section: how to build your own layout theme with the CadCraft icons, with the icon gallery and a layout checker

Owner 2026-11-09: "there should be a designer page, in the developer section, where people learn how to build their own layout theme with our icons." Today /developers is the API only (Overview, Guides, Reference, Console, Changelog, Your keys); layouts are documented for makers in the add-ons repo (docs/LAYOUTS.md) and the help topic common/layout-own, and nothing shows the icons. Change: /developers/layouts (SSR from Catalog and the library data, sub-nav entry "Layouts", in the sitemap and llms.txt): (1) the pattern: the zone map and the dockable-unit rule from UI.1 drawn live (the mockup's dock manager moved onto the site as a component), the block presets, what an adapter fills per app; (2) the icon gallery from the UI.22 build: every icon in Full colour, Outline colorized and the app's theme colour, with key, name and the commands that use it, search, copy-the-key, and the UI.13 drawing rules for anyone adding one; (3) the step-by-step guide: start from a built-in file, place units in zones, choose tool rows and their icons (toolrows.icons "set"), set the tone (UI.16), save as <name>.ccworkspace.json, install it in Settings › Layout › Add a layout file or in SuperCad Studio, or publish it as a layout add-on for everyone; (4) a layout checker: drop a layout file and the page runs the shared format module's resolveLayout in the browser (the engine's own code; progressive enhancement, the guide reads without it) and lists the notes: unknown places, blocks an app cannot fill, missing icon keys; (5) "Design it in the app": opens any app in /use in Arrange mode (UI.5) with one-click export of the result, so the page teaches and the app does; (6) "Themes people made": layout add-ons from the add-ons index, none until there are some. Words in en, es-MX, id; common/layout-own links the page; the 0.10 release note names it. Public and indexable; names no other program (vendor-names rule).

CadCraftBimCraftModelCraftMarkupCraftTakeoffCraftCivilCraftDetailCraftPartCraftSuperCad Studio Site and servicesHelp and wordsTests and tooling Planned 4 UI.1 (the written pattern), UI.22 (gallery data); format v2 (UI.6) when it lands, v1 until then. docs/0.9-sessions/interface-wave1/SUMMARY.md
Suggest an item

Anyone can send one; no account and no name needed. The team reads each idea, and the ones we take on appear on this page. Please leave out web addresses.