vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Remove the merge confetti and canvas-confetti
8032d15aa32facad8f21fad931187520ee81c02b · Keenan Nemetz · 6d ago · parent ac541b26
Remove the merge confetti and canvas-confetti A landing no longer celebrates: the request page is the result, and the vendored library, its loader, and the ?merged=1 flag existed only for the burst. Dropping the flag means a landing redirects to the plain request page. Remove the asset, its license, the NOTICE entry, the loader and its test, and the comments that described them.
server/web/static/app.js −60
| @@ -503,66 +503,6 @@ const copyText = (value, done) => { | |||
| 503 | 503 | }); | |
| 504 | 504 | })(); | |
| 505 | 505 | ||
| 506 | // Landing a change request is the end of the review loop, so the page it lands on | ||
| 507 | // gets one burst of confetti. The library is a separate 25 KB asset and is fetched | ||
| 508 | // only on the redirect that carries ?merged=1: no other page pays for it, and a | ||
| 509 | // reload, bookmark or shared link is an ordinary visit because the flag is dropped | ||
| 510 | // from the address as soon as it has been read. | ||
| 511 | // | ||
| 512 | // The burst is drawn by an instance created with useWorker:false, not by the | ||
| 513 | // library's default export. The default export opts into a Web Worker built from | ||
| 514 | // a blob: URL, and this page's CSP (default-src 'self', no worker-src) blocks that | ||
| 515 | // worker — but the library hands its canvas to the worker anyway, transferring it | ||
| 516 | // with transferControlToOffscreen, and then draws nothing: the canvas is present | ||
| 517 | // and permanently empty, with no error to see. An instance that draws on the main | ||
| 518 | // thread costs a few hundred particles for a few seconds, which is nothing, and | ||
| 519 | // needs no CSP change. | ||
| 520 | // | ||
| 521 | // The volley is the library's "fireworks" preset, cut from fifteen seconds to just | ||
| 522 | // over one, because the page this lands on is one you are about to read. Every | ||
| 523 | // particle is thrown in from the left or right edge of the viewport: nothing opens | ||
| 524 | // in the middle of the page, so the celebration frames the content instead of | ||
| 525 | // covering it. The count steps down with the clock, which keeps it to roughly five | ||
| 526 | // hundred particles in total. | ||
| 527 | (() => { | ||
| 528 | const params = new URLSearchParams(window.location.search); | ||
| 529 | if (!params.has('merged')) return; | ||
| 530 | params.delete('merged'); | ||
| 531 | const rest = params.toString(); | ||
| 532 | window.history.replaceState(null, '', | ||
| 533 | window.location.pathname + (rest ? '?' + rest : '') + window.location.hash); | ||
| 534 | |||
| 535 | const script = document.createElement('script'); | ||
| 536 | script.src = '/static/confetti.browser.js'; | ||
| 537 | script.onload = () => { | ||
| 538 | if (typeof window.confetti !== 'function') return; | ||
| 539 | const burst = window.confetti.create(null, { resize: true, useWorker: false }); | ||
| 540 | const look = { | ||
| 541 | startVelocity: 30, | ||
| 542 | spread: 180, | ||
| 543 | ticks: 400, | ||
| 544 | gravity: 0.5, | ||
| 545 | decay: 0.92, | ||
| 546 | scalar: 1.2, | ||
| 547 | disableForReducedMotion: true, | ||
| 548 | }; | ||
| 549 | |||
| 550 | // The "fireworks" volley, from the two side edges only, tapering as the clock | ||
| 551 | // runs out. | ||
| 552 | const volleyMs = 1200; | ||
| 553 | const end = Date.now() + volleyMs; | ||
| 554 | (function frame() { | ||
| 555 | const left = end - Date.now(); | ||
| 556 | if (left <= 0) return; | ||
| 557 | const particleCount = 60 * (left / volleyMs); | ||
| 558 | burst({ ...look, particleCount, angle: 60, spread: 55, origin: { x: 0 } }); | ||
| 559 | burst({ ...look, particleCount, angle: 120, spread: 55, origin: { x: 1 } }); | ||
| 560 | setTimeout(frame, 250); | ||
| 561 | })(); | ||
| 562 | }; | ||
| 563 | document.head.appendChild(script); | ||
| 564 | })(); | ||
| 565 | |||
| 566 | 506 | // Live CI job output. The server serves the attempt's file from a byte offset and | |
| 567 | 507 | // says when the log is final; the page asks again only while the job is running. | |
| 568 | 508 | // Chunks are decoded with a streaming TextDecoder because a multi-byte character | |