Merge main
Lint and reload / lint (pull_request) Successful in 2s
Lint and reload / reload (pull_request) Skipped

This commit is contained in:
Bendik Aagaard Lynghaug
2026-08-30 14:29:47 +02:00
3 changed files with 99 additions and 21 deletions
+8 -1
View File
@@ -18,7 +18,7 @@ on:
- .gitea/workflows/deploy.yml - .gitea/workflows/deploy.yml
env: env:
PORTAL_RELEASE: v0.3.4 PORTAL_RELEASE: v0.3.6
INSTANCE: redoal-portal INSTANCE: redoal-portal
jobs: jobs:
@@ -83,9 +83,16 @@ jobs:
# No sudo: the runner's unit sets NoNewPrivileges=yes - systemctl # No sudo: the runner's unit sets NoNewPrivileges=yes - systemctl
# talks to PID1 over D-Bus, authorized by the polkit rule scoped # talks to PID1 over D-Bus, authorized by the polkit rule scoped
# to deploy-runner + the app@* unit pattern. # to deploy-runner + the app@* unit pattern.
# enable: the unit must come back after a host reboot (2026-08-30 a
# reboot left both portal instances down - deploys had only ever
# started them). Needs the manage-unit-files polkit grant; until
# that's on the host the enable is reported and skipped, never a
# failed deploy.
- name: Activate and restart - name: Activate and restart
run: | run: |
ln -sfn "/srv/app/$INSTANCE/releases/$PORTAL_RELEASE" /srv/app/$INSTANCE/current ln -sfn "/srv/app/$INSTANCE/releases/$PORTAL_RELEASE" /srv/app/$INSTANCE/current
systemctl enable app@$INSTANCE.service \
|| echo "::warning::could not enable app@$INSTANCE (polkit) - unit will not survive a reboot"
systemctl restart app@$INSTANCE.service systemctl restart app@$INSTANCE.service
# www redirects to the apex for the same single-cookie-scope # www redirects to the apex for the same single-cookie-scope
+68 -20
View File
@@ -72,48 +72,96 @@ export function turningFunctionToPoints(turning, samples = 14) {
return pts; return pts;
} }
// A path plus the motion that is its own: where it sits, how it
// spins, how it drifts, how fast it traces itself in. Every rate is
// an integer number of cycles so the field loops seamlessly at t = 1.
export function makePaths(seed, count, stops = 10) { export function makePaths(seed, count, stops = 10) {
const rand = mulberry32(seed); const rand = mulberry32(seed);
const paths = []; const paths = [];
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
paths.push(turningFunctionToPoints(createTurningFunction(rand, stops))); const pts = turningFunctionToPoints(createTurningFunction(rand, stops));
let cx = 0, cy = 0;
for (let k = 0; k < pts.length; k += 2) { cx += pts[k]; cy += pts[k + 1]; }
cx /= pts.length / 2;
cy /= pts.length / 2;
const sign = rand() > 0.5 ? 1 : -1;
paths.push({
pts,
cx,
cy,
// own turns per cycle: a third sit still, the rest turn one
// or two full turns either way over the cycle
spin: rand() < 0.33 ? 0 : sign * (1 + Math.floor(rand() * 2)),
// slow orbit around its own place, radius in frame units
driftR: 0.01 + rand() * 0.03,
driftK: 1 + Math.floor(rand() * 3),
driftPhase: rand() * Math.PI * 2,
// its own wave along the line
waveF: 4 + rand() * 6,
waveK: 1 + Math.floor(rand() * 2),
waveA: 0.004 + rand() * 0.008,
// tracing tempo: how many times it redraws itself per cycle
tempo: 1 + Math.floor(rand() * 2),
phase: rand(),
});
} }
return paths; return paths;
} }
// t in [0, 1) is the animation phase. Each path traces itself in over // t in [0, 1) is the animation phase. Each path traces itself in at
// one cycle, offset by its index so the field is always mid-draw, and // its own tempo and phase, fades as it completes, turns at its own
// fades as it completes; the whole field drifts slowly and a soft // rate about its own center and drifts on its own small orbit; a wave
// wave runs through every line. `colors` is [accent, dim] - the // with its own frequency runs along it. The tip is interpolated
// palette comes from the page's own custom properties either way. // between samples so a trace glides instead of stepping. `colors` is
// [accent, dim] - the palette comes from the page's custom properties.
const smooth = (u) => u * u * (3 - 2 * u);
export function drawPaths(ctx, width, height, paths, t, colors) { export function drawPaths(ctx, width, height, paths, t, colors) {
ctx.clearRect(0, 0, width, height); ctx.clearRect(0, 0, width, height);
const s = Math.min(width, height) * 0.9; const s = Math.min(width, height) * 0.9;
ctx.save(); ctx.save();
ctx.translate(width / 2, height / 2); ctx.translate(width / 2, height / 2);
ctx.rotate(t * Math.PI * 2 * 0.08);
ctx.lineWidth = 1; ctx.lineWidth = 1;
ctx.lineCap = 'round'; ctx.lineCap = 'round';
ctx.lineJoin = 'round'; ctx.lineJoin = 'round';
const n = paths.length; const TAU = Math.PI * 2;
const wave = t * Math.PI * 2; for (let i = 0; i < paths.length; i++) {
for (let i = 0; i < n; i++) { const p = paths[i];
const pts = paths[i]; const phase = (t * p.tempo + p.phase) % 1;
const phase = (t + i / n) % 1; // ease: quick to appear, long to complete, then fade
// ease: quick to appear, long to complete const progress = smooth(Math.min(1, phase * 1.35));
const progress = Math.min(1, phase * 1.35);
const count = Math.max(2, Math.floor((pts.length / 2) * progress));
const alpha = phase < 0.85 ? 0.75 : 0.75 * (1 - (phase - 0.85) / 0.15); const alpha = phase < 0.85 ? 0.75 : 0.75 * (1 - (phase - 0.85) / 0.15);
const total = p.pts.length / 2;
const tip = 1 + (total - 1) * progress; // fractional sample index
const whole = Math.floor(tip);
const frac = tip - whole;
const angle = p.spin * TAU * t;
const dx = Math.cos(p.driftPhase + p.driftK * TAU * t) * p.driftR;
const dy = Math.sin(p.driftPhase + p.driftK * TAU * t) * p.driftR;
const wave = p.waveK * TAU * t;
ctx.save();
ctx.translate((p.cx + dx) * s, (p.cy + dy) * s);
ctx.rotate(angle);
ctx.strokeStyle = i % 3 === 0 ? colors[0] : colors[1]; ctx.strokeStyle = i % 3 === 0 ? colors[0] : colors[1];
ctx.globalAlpha = alpha * (i % 3 === 0 ? 1 : 0.55); ctx.globalAlpha = alpha * (i % 3 === 0 ? 1 : 0.55);
ctx.beginPath(); ctx.beginPath();
for (let k = 0; k < count; k++) { const at = (k) => {
const x = pts[k * 2], y = pts[k * 2 + 1]; const x = p.pts[k * 2] - p.cx, y = p.pts[k * 2 + 1] - p.cy;
const wy = y + Math.sin(x * 7 + wave + i) * 0.008; return [x, y + Math.sin(x * p.waveF + wave) * p.waveA];
if (k === 0) ctx.moveTo(x * s, wy * s); };
else ctx.lineTo(x * s, wy * s); for (let k = 0; k <= Math.min(whole, total - 1); k++) {
const [x, y] = at(k);
if (k === 0) ctx.moveTo(x * s, y * s);
else ctx.lineTo(x * s, y * s);
}
if (whole < total - 1 && frac > 0) {
const [ax, ay] = at(whole), [bx, by] = at(whole + 1);
ctx.lineTo((ax + (bx - ax) * frac) * s, (ay + (by - ay) * frac) * s);
} }
ctx.stroke(); ctx.stroke();
ctx.restore();
} }
ctx.restore(); ctx.restore();
} }
+23
View File
@@ -37,3 +37,26 @@ alternatives:
transitions: transitions:
- to: unsubscribed - to: unsubscribed
label: Unsubscribe label: Unsubscribe
- name: Announced pages
description: >
Pages with an `event:` window, kept here by portal itself. When a
window closes the record becomes a task: post what happened,
then mark it done.
action: /review
consequence: [Confirm]
features:
- name: ""
description: ""
resource:
source:
kind: kv
bucket: portal_events
requires_group: owners
transitions:
- from: awaiting_summary
to: summarized
label: Posted what happened
- from: announced
to: summarized
label: Needs no summary