Compare commits

..
8 Commits
Author SHA1 Message Date
Bendik Aagaard Lynghaug 3d229754bf Merge main
Lint and reload / lint (pull_request) Successful in 2s
Lint and reload / reload (pull_request) Skipped
2026-08-30 14:29:47 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 588e35fd44 Hero: every path moves on its own, and the tip glides
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
The trace advanced a whole sample at a time (floor of points x
progress) - visible steps; the tip now interpolates between samples.
The single field-wide rotate is gone: each path turns at its own
seeded rate and direction about its own center, drifts on its own
small orbit, carries its own wave and traces itself at its own tempo.
Every rate is an integer number of cycles, so the loop stays
seamless. Same generator feeds the paint worklet and the canvas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 14:29:39 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 ecd77fae35 Trial event page retired - announced pages verified live
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 14:28:46 +02:00
Bendik Aagaard Lynghaug 9a56a0fe8b Trial event: second window for the header capture
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
2026-08-30 14:27:59 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 79ab3b37b0 Review desk: announced pages; trial event page
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 12:37:18 +02:00
Bendik Aagaard Lynghaug f4e9db8475 Pin portal v0.3.6 - announced pages
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
2026-08-30 12:36:48 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 039d2bf54d Deploy enables the instance unit
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 2s
Lint and reload / reload (push) Successful in 0s
A host reboot on 2026-08-30 left the instance down: deploys only ever
started it. enable is best-effort until the host's polkit rule grants
manage-unit-files to deploy-runner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 12:09:14 +02:00
Bendik Aagaard Lynghaug 60050d80ae Pin portal v0.3.5 - gesture ghost for v2 keys
Deploy instance / deploy (push) Successful in 1s
Lint and reload / lint (push) Successful in 3s
Lint and reload / reload (push) Successful in 0s
2026-08-30 11:43:10 +02:00
3 changed files with 99 additions and 21 deletions
+8 -1
View File
@@ -18,7 +18,7 @@ on:
- .gitea/workflows/deploy.yml
env:
PORTAL_RELEASE: v0.3.4
PORTAL_RELEASE: v0.3.6
INSTANCE: redoal-portal
jobs:
@@ -83,9 +83,16 @@ jobs:
# No sudo: the runner's unit sets NoNewPrivileges=yes - systemctl
# talks to PID1 over D-Bus, authorized by the polkit rule scoped
# 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
run: |
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
# 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;
}
// 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) {
const rand = mulberry32(seed);
const paths = [];
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;
}
// t in [0, 1) is the animation phase. Each path traces itself in over
// one cycle, offset by its index so the field is always mid-draw, and
// fades as it completes; the whole field drifts slowly and a soft
// wave runs through every line. `colors` is [accent, dim] - the
// palette comes from the page's own custom properties either way.
// t in [0, 1) is the animation phase. Each path traces itself in at
// its own tempo and phase, fades as it completes, turns at its own
// rate about its own center and drifts on its own small orbit; a wave
// with its own frequency runs along it. The tip is interpolated
// 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) {
ctx.clearRect(0, 0, width, height);
const s = Math.min(width, height) * 0.9;
ctx.save();
ctx.translate(width / 2, height / 2);
ctx.rotate(t * Math.PI * 2 * 0.08);
ctx.lineWidth = 1;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
const n = paths.length;
const wave = t * Math.PI * 2;
for (let i = 0; i < n; i++) {
const pts = paths[i];
const phase = (t + i / n) % 1;
// ease: quick to appear, long to complete
const progress = Math.min(1, phase * 1.35);
const count = Math.max(2, Math.floor((pts.length / 2) * progress));
const TAU = Math.PI * 2;
for (let i = 0; i < paths.length; i++) {
const p = paths[i];
const phase = (t * p.tempo + p.phase) % 1;
// ease: quick to appear, long to complete, then fade
const progress = smooth(Math.min(1, phase * 1.35));
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.globalAlpha = alpha * (i % 3 === 0 ? 1 : 0.55);
ctx.beginPath();
for (let k = 0; k < count; k++) {
const x = pts[k * 2], y = pts[k * 2 + 1];
const wy = y + Math.sin(x * 7 + wave + i) * 0.008;
if (k === 0) ctx.moveTo(x * s, wy * s);
else ctx.lineTo(x * s, wy * s);
const at = (k) => {
const x = p.pts[k * 2] - p.cx, y = p.pts[k * 2 + 1] - p.cy;
return [x, y + Math.sin(x * p.waveF + wave) * p.waveA];
};
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.restore();
}
ctx.restore();
}
+23
View File
@@ -37,3 +37,26 @@ alternatives:
transitions:
- to: unsubscribed
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