Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
176 commits
Select commit Hold shift + click to select a range
24b3eac
feat: timelapse.sqlite
S-Ou May 26, 2026
32a2c67
refactor: hollup i changed it to snapshots.sqlite
S-Ou May 26, 2026
07483ca
feat: history bucket view
S-Ou May 26, 2026
591a772
feat: latest history rows
S-Ou May 26, 2026
5fbd4ce
feat: latest snapshot
S-Ou May 26, 2026
5ddb431
feat: actually build the snapshot
S-Ou May 26, 2026
b6897d6
feat: scheduler
S-Ou May 27, 2026
c332ff7
feat: env to control snapshot generation
S-Ou May 28, 2026
c707191
feat: error on broken snapshot manifest
S-Ou May 28, 2026
222d143
refactor: move sqlite file
S-Ou May 28, 2026
2299da5
fix: path in prisma
S-Ou May 28, 2026
f755b9e
refactor: get rid of history id related stuff
S-Ou May 28, 2026
fffffc8
fix: migration
S-Ou May 28, 2026
dae57fa
fix: blank pixel background
S-Ou May 28, 2026
dd2a0a7
Merge branch 'main' into feat/snapshots
S-Ou May 28, 2026
581b8be
feat: snapshot service
S-Ou May 28, 2026
23a3b7e
feat: apply policy
S-Ou May 28, 2026
d0911e4
feat: last_included_history_at
S-Ou May 28, 2026
ac8a24f
fix: missing param
S-Ou May 28, 2026
a3cd626
fix: some typing stuff
S-Ou May 28, 2026
f8f8392
feat: basic timelapse generator
S-Ou May 28, 2026
01b65c6
feat: endpoint
S-Ou May 28, 2026
37805a7
feat: background color
S-Ou May 28, 2026
57134e2
feat: crop to bounds
S-Ou May 28, 2026
706bfc8
feat: scaling
S-Ou May 28, 2026
101f637
feat: end hold time
S-Ou May 28, 2026
9559957
feat: auto-scale
S-Ou May 28, 2026
eed1ac0
docs: todo comment
S-Ou May 28, 2026
e7ba361
chore: tidy up
S-Ou May 28, 2026
cb3620e
refactor: fetch extra snapshot for full range
S-Ou May 29, 2026
f468600
feat: timeline manifest
S-Ou May 28, 2026
30bb614
feat: cache timelapses
S-Ou May 28, 2026
f7fbcf6
refactor: breaking up a big function
S-Ou May 28, 2026
8fde067
feat: handle in-flight requests
S-Ou May 28, 2026
da32f26
fix: pnpm package builds
S-Ou May 28, 2026
cafb15a
fix: import
S-Ou May 28, 2026
4a8d19f
fix: tests
S-Ou May 28, 2026
050be56
fix: build
S-Ou May 28, 2026
97788a9
Merge branch 'main' into feat/timelapse
S-Ou May 29, 2026
ac38c42
Merge branch 'main' into feat/snapshots
S-Ou May 29, 2026
df49fca
Merge branch 'feat/snapshots' into feat/timelapse
S-Ou May 29, 2026
a83a9be
fix: somehow these generated timelapses got committed despite gitignore
S-Ou May 29, 2026
935dcb3
refactor: lock up the canvas timelapse endpoint
S-Ou May 29, 2026
be42899
refactor: change endpoint
S-Ou May 29, 2026
b48c2d3
feat: frame endpoint
S-Ou May 29, 2026
3d87c3f
feat: auto-trim frame timelapse length
S-Ou May 29, 2026
e3fa8f6
feat: added end card
S-Ou May 29, 2026
513f5df
refactor: better transition
S-Ou May 29, 2026
e05e4de
fix: tests
S-Ou May 29, 2026
7f3470b
Merge branch 'main' into feat/snapshots
Rocked03 May 29, 2026
2e6eee6
Merge branch 'feat/snapshots' into feat/timelapse
Rocked03 May 29, 2026
8f487e7
feat: set snapshot cursor as dirty when modifying history
S-Ou May 29, 2026
b22f3ee
fix: pregenerate cursor
S-Ou May 29, 2026
381734f
Merge branch 'feat/snapshots' into feat/timelapse
S-Ou May 29, 2026
14c74e7
fix: tests
S-Ou May 29, 2026
5d980d5
Merge branch 'feat/snapshots' into feat/timelapse
Rocked03 May 29, 2026
b018694
feat: snapshot manifest and image endpoints
S-Ou May 29, 2026
7acd36b
refactor: webp instead of png
S-Ou May 29, 2026
230ca84
Merge branch 'feat/snapshots' into feat/timeline
S-Ou May 29, 2026
378b2a1
Merge branch 'feat/snapshots' into feat/timelapse
S-Ou May 29, 2026
3fde419
refactor: file name
S-Ou May 29, 2026
1c419f5
refactor: raw timelapse cache
S-Ou May 29, 2026
f8d5f7f
feat: lossless quality raw timelapse
S-Ou May 29, 2026
ccd49ea
feat: most basic timeline video
S-Ou May 29, 2026
45a6f80
Merge branch 'feat/timeline-old' into feat/timeline
S-Ou May 29, 2026
0cefb2d
refactor: removed now-redundant endpoint
S-Ou May 29, 2026
6b3dce5
feat: use manifest data
S-Ou May 29, 2026
9d0c1fa
fix: path extensions
S-Ou May 29, 2026
19b83be
fix: stream to proper file
S-Ou May 29, 2026
eec3c66
fix: save to cache
S-Ou May 29, 2026
585829a
fix: stream to proper file
S-Ou May 29, 2026
509dc0c
fix: save to cache
S-Ou May 29, 2026
4f84679
fix: path extensions
S-Ou May 29, 2026
37a23b1
Merge branch 'feat/timelapse' into feat/timeline
S-Ou May 29, 2026
a8cec2b
fix: getting the slider working
S-Ou May 29, 2026
4507ade
fix: smoother slider
S-Ou May 30, 2026
61b767f
feat: block timelapse generation when not available
S-Ou May 30, 2026
1ec619b
refactor: slider component
S-Ou May 30, 2026
acf7c2b
fix: canvas file type
S-Ou May 30, 2026
c2dfbe5
fix: other file extension
S-Ou May 30, 2026
759d29d
Merge branch 'feat/timelapse' into feat/timeline
S-Ou May 30, 2026
f6f6187
style: slider
S-Ou May 30, 2026
76168e3
refactor: timelinecontext
S-Ou May 30, 2026
2323202
feat: date
S-Ou May 30, 2026
60cd01f
feat: clocks
S-Ou May 30, 2026
3ec45b7
refactor: active timeline state and lazy load
S-Ou May 30, 2026
9e4cae1
feat: timeline controls base
S-Ou May 30, 2026
c3dbb0f
feat: seek buttons
S-Ou May 30, 2026
f699f57
feat: play button
S-Ou May 30, 2026
9b3f56b
feat: playback
S-Ou May 30, 2026
13cce16
fix: playback restart
S-Ou May 30, 2026
5172d59
refactor: restructure controls
S-Ou May 30, 2026
371c2a5
feat: 16x
S-Ou May 30, 2026
d835709
feat: filter info
S-Ou May 30, 2026
9183cb4
chore: fix build and lint
S-Ou May 30, 2026
9e0c508
fix: grabbing
S-Ou May 30, 2026
3f84d3a
style: some imperfect mobile responsiveness
S-Ou May 30, 2026
ef12782
Merge branch 'main' into feat/snapshots
S-Ou Jun 1, 2026
420c89f
Merge branch 'feat/snapshots' into feat/timelapse
Rocked03 Jun 1, 2026
98bed17
Merge branch 'feat/timelapse' into feat/timeline
S-Ou Jun 1, 2026
515a35f
chore comment
S-Ou Jun 1, 2026
cd0e43d
Merge branch 'main' into feat/snapshots
S-Ou Jun 7, 2026
7e76cb5
fix: pnpm lock
S-Ou Jun 7, 2026
6f914d1
refactor: kysely
S-Ou Jun 7, 2026
d48e899
feat: base otel traces
S-Ou Jun 7, 2026
72ae2ed
feat: trace auth'd user
S-Ou Jun 7, 2026
824bc48
feat: docker compose
S-Ou Jun 7, 2026
c79ce11
feat: custom tags
S-Ou Jun 7, 2026
089f268
feat: pixel place tags
S-Ou Jun 7, 2026
b451003
feat: statistics spans
S-Ou Jun 7, 2026
cad53bf
feat: palette span tags
S-Ou Jun 7, 2026
8778f72
feat: notice span tags
S-Ou Jun 7, 2026
ebe0b84
refactor: attribute names
S-Ou Jun 7, 2026
789a378
feat: history span tags
S-Ou Jun 7, 2026
751ea03
feat: frame spans
S-Ou Jun 7, 2026
5736de3
feat: event span
S-Ou Jun 7, 2026
375e777
feat: discord spans
S-Ou Jun 7, 2026
694c7aa
feat: canvas spans
S-Ou Jun 7, 2026
b19a24d
feat: blocklist spans
S-Ou Jun 7, 2026
999ef91
feat: audit log spans
S-Ou Jun 7, 2026
fe14753
refactor: update name
S-Ou Jun 7, 2026
57a89c9
chore: lint
S-Ou Jun 7, 2026
58c934d
docs: readme
S-Ou Jun 7, 2026
9f13bd0
feat: oauth2
S-Ou Jun 7, 2026
37ea146
refactor: jaeger v2
S-Ou Jun 7, 2026
24ea92c
fix: jaeger (somewhat)
S-Ou Jun 7, 2026
9bfbe70
refactor: split up docker compose and jaeger
S-Ou Jun 8, 2026
a292850
fix: opensearch x jaeger
S-Ou Jun 8, 2026
ae5928c
feat: local docker compose
S-Ou Jun 8, 2026
b031d07
nit: tiny things
S-Ou Jun 8, 2026
19f0e2a
feat: caddy
S-Ou Jun 8, 2026
8da2b71
chore: lint
S-Ou Jun 8, 2026
da0435c
refactor: local dev
S-Ou Jun 8, 2026
de9eb69
refactor: unnecessary memlock
S-Ou Jun 8, 2026
6569f69
refactor: remove memlock (2)
S-Ou Jun 8, 2026
593756a
feat: more prisma otel
S-Ou Jun 9, 2026
0bfcf57
Merge branch 'feat/open-telemetry' into feat/snapshots
S-Ou Jun 9, 2026
69c01f9
Merge branch 'main' into feat/snapshots
S-Ou Jun 9, 2026
8b74f79
refactor: migration
S-Ou Jun 10, 2026
36b1663
feat: smarter buckets
S-Ou Jun 10, 2026
2cbeaab
feat: safer dirty timestamp
S-Ou Jun 10, 2026
80254f6
refactor: better cursor
S-Ou Jun 10, 2026
f9c60ee
fix: better gating
S-Ou Jun 10, 2026
65e2213
Merge branch 'main' into feat/snapshots
S-Ou Jun 10, 2026
3ca278e
refactor: redundant cursor reference
S-Ou Jun 11, 2026
417ec04
Merge branch 'main' into feat/snapshots
S-Ou Jun 11, 2026
0305a41
Merge branch 'feat/snapshots' into feat/timelapse
S-Ou Jun 11, 2026
c43f6aa
Merge branch 'main' into feat/timelapse
S-Ou Jun 11, 2026
48789e2
fix: pnpm lock
S-Ou Jun 11, 2026
d9bb7d1
Merge branch 'main' into feat/timelapse
S-Ou Jun 11, 2026
1217ec0
Merge branch 'feat/timelapse' into feat/timeline
S-Ou Jun 11, 2026
15435be
fix: duped endpoint
S-Ou Jun 11, 2026
874f20c
feat: add spans
S-Ou Jun 11, 2026
1924bc9
refactor: the easy comments
S-Ou Jun 11, 2026
49fb6d2
refactor: kysely
S-Ou Jun 11, 2026
f9da4d6
refactor: tmpridr
S-Ou Jun 11, 2026
f2e7258
refactor: exclusive coords
S-Ou Jun 11, 2026
2314118
feat: drain
S-Ou Jun 11, 2026
e0b3a6f
refactor: better eviction
S-Ou Jun 11, 2026
b0cb1c5
feat: tests
S-Ou Jun 11, 2026
164ac3e
refactor muxer
S-Ou Jun 12, 2026
3b1fa07
refactor: serve from file path
S-Ou Jun 12, 2026
e285882
refactor: format
S-Ou Jun 12, 2026
e5781e9
refactor: raw is separate
S-Ou Jun 12, 2026
fed2a7a
feat: temporary mod restriction
S-Ou Jun 13, 2026
82b43cf
refactor: stuff
S-Ou Jun 13, 2026
ca27209
refactor: tmpdir
S-Ou Jun 13, 2026
e72316e
refactor: timelapse format map
S-Ou Jun 13, 2026
98b8d8b
refactor: split up timelapseService
S-Ou Jun 13, 2026
02ac530
refactor: tidy up
S-Ou Jun 13, 2026
6265551
Merge branch 'main' into feat/timelapse
S-Ou Jun 13, 2026
258a2b3
fix: soft locked up
S-Ou Jun 13, 2026
745e3fa
Merge branch 'feat/timelapse' into feat/timeline
S-Ou Jun 13, 2026
d95d4fb
Merge branch 'main' into feat/timeline
S-Ou Jun 13, 2026
ad50b56
refactor: stray migration file
S-Ou Jun 13, 2026
18a8df9
fix: merge
S-Ou Jun 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

2 changes: 2 additions & 0 deletions packages/backend/src/routes/api/v1/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ import { getUserCanvasCooldown } from "@/services/pixelService";
import { generateTimelapse } from "@/services/timelapse/timelapseService";
import { addSpanAttributes } from "@/utils/otel";
import { pixelRouter } from "./pixel";
import { snapshotRouter } from "./snapshot";

export const canvasRouter = typedRouter(Router());

canvasRouter.use("/:canvasId/pixel", pixelRouter);
canvasRouter.use("/:canvasId/snapshots", snapshotRouter);

canvasRouter.get("/", async (req, res) => {
const canvases = await getCanvases();
Expand Down
35 changes: 35 additions & 0 deletions packages/backend/src/routes/api/v1/snapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { CanvasIdParamModel } from "@blurple-canvas-web/types";
import { Router } from "express";
import { typedRouter } from "@/middleware/typedRouter";
import { validate } from "@/middleware/validate";
import { getSnapshots } from "@/services/snapshot";

export const snapshotRouter = typedRouter(Router({ mergeParams: true }));

snapshotRouter.get(
"/",
validate({ params: CanvasIdParamModel }),
async (req, res) => {
const snapshots = await getSnapshots({ canvasId: req.params.canvasId });

res
.status(200)
.json(
snapshots.map((snapshot) =>
serializeSnapshot(snapshot, req.params.canvasId),
),
);
},
);

function serializeSnapshot(
snapshot: Awaited<ReturnType<typeof getSnapshots>>[number],
canvasId: number,
) {
return {
canvasId,
snapshotAt: snapshot.snapshot_at.toISOString(),
lastIncludedHistoryAt: snapshot.last_included_history_at.toISOString(),
historyCount: snapshot.history_count,
};
}
2 changes: 2 additions & 0 deletions packages/backend/src/services/canvasService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import { getCurrentEvent } from "./eventService";
import { getEventPalette } from "./paletteService";
import { type BulkPlaceEntry, createBulkPlaceEntries } from "./pixelService";
import { isSnapshotAvailableForCanvas } from "./snapshot";

/**
* A locked canvas cannot be edited by users. It is therefore, safe to store it as an image on the
Expand Down Expand Up @@ -739,5 +740,6 @@ function canvasToCanvasInfo(canvas: canvas): CanvasInfo {
webPlacingEnabled: config.webPlacingEnabled,
allColorsGlobal: canvas.all_colors_global,
cooldownDuration: canvas.cooldown_length,
timelineEnabled: isSnapshotAvailableForCanvas(canvas.id),
};
}
10 changes: 5 additions & 5 deletions packages/backend/src/services/historyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,11 +527,11 @@ export async function restorePixelHistoryEntries(
async ([canvasId, coordinates]) => {
await restorePixelsAfterHistoryModification(canvasId, coordinates);

await setSnapshotDirtyTimestamp(
canvasId,
// biome-ignore lint/style/noNonNullAssertion: is generated above, will always exist
earliestEntryTimestampsByCanvas.get(canvasId)!,
);
const earliestEntryTimestamp =
earliestEntryTimestampsByCanvas.get(canvasId);
if (earliestEntryTimestamp) {
await setSnapshotDirtyTimestamp(canvasId, earliestEntryTimestamp);
}
},
),
);
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/services/snapshot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "@/snapshot/paths";
export * from "./generateSnapshotService";
export * from "./scheduler";
export * from "./snapshotPolicy";
export * from "./snapshotService";
2 changes: 2 additions & 0 deletions packages/backend/src/services/snapshot/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ async function persistSnapshot({
canvas_id: canvasId,
snapshot_at: snapshotAt,
history_count: historyCount,
file_size_bytes: image.byteLength,
image_path: filePath,
last_included_history_at: lastIncludedHistoryAt,
},
update: {
history_count: historyCount,
file_size_bytes: image.byteLength,
image_path: filePath,
last_included_history_at: lastIncludedHistoryAt,
},
Expand Down
20 changes: 18 additions & 2 deletions packages/backend/src/services/snapshot/snapshotService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export async function getSnapshots({

const manifests = await snapshotPrisma.snapshot_manifest.findMany({
where,
orderBy: { last_included_history_at: "desc" },
orderBy: { last_included_history_at: "asc" },
});

if (to) {
Expand All @@ -41,7 +41,7 @@ export async function getSnapshots({
});

if (extra && !manifests.some((manifest) => manifest.id === extra.id)) {
manifests.unshift(extra);
manifests.push(extra);
}
}

Expand Down Expand Up @@ -91,3 +91,19 @@ export async function setSnapshotDirtyTimestamp(
});
}
}

export async function getSnapshotManifest(
canvasId: CanvasInfo["id"],
snapshotAt: Date,
) {
if (!isSnapshotAvailableForCanvas(canvasId)) {
return null;
}

return snapshotPrisma.snapshot_manifest.findFirst({
where: {
canvas_id: canvasId,
snapshot_at: snapshotAt,
},
});
}
5 changes: 5 additions & 0 deletions packages/backend/src/services/timelapse/timelapseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "@/snapshot/paths";
import { calculateScale, clamp, normalizeBounds } from "@/utils";
import { getCanvasInfo } from "../canvasService";
import { isSnapshotAvailableForCanvas } from "../snapshot";
import { getSnapshots } from "../snapshot/snapshotService";
import {
buildTimelapseCacheKey,
Expand Down Expand Up @@ -45,6 +46,10 @@ export async function generateTimelapse({
backgroundColor = notQuiteBlackRgba,
raw = "default",
}: GenerateTimelapseParams): Promise<string> {
if (!isSnapshotAvailableForCanvas(canvasId)) {
throw new Error(`Snapshots are not available for canvas ${canvasId}`);
}

if (raw === "raw") {
endHoldDurationMs = null;
scale = 1;
Expand Down
38 changes: 23 additions & 15 deletions packages/frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import { cookies } from "next/headers";
import config from "@/config/clientConfig";
import serverConfig from "@/config/serverConfig";
import "../styles/core.css";
import { AuthProvider, CanvasProvider, QueryClientProvider } from "@/contexts";
import {
AuthProvider,
CanvasProvider,
QueryClientProvider,
TimelineProvider,
} from "@/contexts";
import { isDatabaseUnavailableError } from "@/util/axios";
import "../styles/core.css";
import { CircleAlert, CircleCheck, Info, TriangleAlert } from "lucide-react";
Expand Down Expand Up @@ -84,6 +89,7 @@ const defaultCanvasInfo = {
webPlacingEnabled: false,
allColorsGlobal: false,
cooldownDuration: 0,
timelineEnabled: false,
} satisfies CanvasInfo;

const toasterIcons = {
Expand Down Expand Up @@ -119,20 +125,22 @@ async function LayoutProviders({ children }: { children: React.ReactNode }) {
<QueryClientProvider>
<AuthProvider profile={profile}>
<CanvasProvider mainCanvasInfo={canvasInfo}>
<AppProviders>
{children}
<Toaster
icons={toasterIcons}
position="bottom-left" // bottom right overlaps with the action panel
theme="dark"
toastOptions={{
style: {
backgroundColor: "var(--discord-legacy-not-quite-black)",
boxShadow: "0 0 10px rgba(0 0 0 / 25%)",
},
}}
/>
</AppProviders>
<TimelineProvider>
<AppProviders>
{children}
<Toaster
icons={toasterIcons}
position="bottom-left" // bottom right overlaps with the action panel
theme="dark"
toastOptions={{
style: {
backgroundColor: "var(--discord-legacy-not-quite-black)",
boxShadow: "0 0 10px rgba(0 0 0 / 25%)",
},
}}
/>
</AppProviders>
</TimelineProvider>
</CanvasProvider>
</AuthProvider>
</QueryClientProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { useId, useState } from "react";
import { toast } from "sonner";
import { ButtonSupplement, DynamicButton } from "@/components/button";
import Pagination from "@/components/Pagination";
import { useCanvasContext, useCanvasViewContext } from "@/contexts";
import TimelineControls from "@/components/timeline/TimelineControls";
import {
useCanvasContext,
useCanvasViewContext,
useTimelineContext,
} from "@/contexts";
import {
type PixelHistoryParams,
usePixelHistory,
Expand Down Expand Up @@ -106,6 +111,7 @@ export default function PixelInfoTab({
const { data, isLoading } = usePixelHistory(canvasId, historyParams, {
enabled: active,
});
const { currentTimelineTimestamp, timelineIsActive } = useTimelineContext();

if (
point &&
Expand All @@ -118,9 +124,18 @@ export default function PixelInfoTab({
if (historyParams !== null && historyParams?.page !== page)
setHistoryParams((prev) => (prev ? { ...prev, page } : null));

const pixelHistory = data?.entries ?? [];
const rawPixelHistory = data?.entries ?? [];
const truePage = data?.page ?? 1;

const pixelHistory =
timelineIsActive && currentTimelineTimestamp ?
rawPixelHistory.filter((record) => {
const recordTime = new Date(record.timestamp);
const timelineTime = currentTimelineTimestamp;
return recordTime <= timelineTime;
})
: rawPixelHistory;

const pixelUrl =
(adjustedCoords &&
containerRef.current &&
Expand Down Expand Up @@ -203,6 +218,7 @@ export default function PixelInfoTab({
</DynamicButton>
)}
</ActionPanelTabBody>
<TimelineControls />
</PixelInfoTabBlock>
);
}
6 changes: 5 additions & 1 deletion packages/frontend/src/components/button/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export { BasicButton, DestructiveButton } from "./BasicButtons";
export {
BasicButton,
BasicHighlightButton,
DestructiveButton,
} from "./BasicButtons";
export { Button } from "./Button";
export { default as ButtonSupplement } from "./ButtonSupplement";
export { DynamicAnchorButton, default as DynamicButton } from "./DynamicButton";
Expand Down
Loading