diff --git a/.claude/commands/release-scrolly.md b/.claude/commands/release-scrolly.md new file mode 100644 index 0000000..9f060f3 --- /dev/null +++ b/.claude/commands/release-scrolly.md @@ -0,0 +1,80 @@ +Take all uncommitted and committed changes on the current branch and drive them through the full release pipeline until a new Docker image is published. Follow every step below in order. + +## 1. Commit & Push Changes + +- Run `git status` and `git diff` to see what's uncommitted +- Break uncommitted changes into small, focused conventional commits (feat:, fix:, refactor:, chore:, etc.) +- If pre-commit hooks fail, fix the issue and create a NEW commit (never amend) +- If already on main, create a feature branch first (`git checkout -b `) +- Push the branch to origin + +## 2. Create a PR + +- Use `gh pr create` targeting main +- Title should summarize the changes concisely (under 70 chars) +- Body should have a ## Summary with bullet points and a ## Test plan +- If a PR already exists for this branch, skip creation + +## 3. Pass CI Checks + +- Watch PR checks with `gh pr checks --watch` +- If any check fails, read the failure logs, fix the issue, commit the fix, and push +- Repeat until ALL checks pass (lint, format, type-check, tests, build, CodeQL) +- Run `npm run check` and `npm run lint` locally before pushing fixes to save time + +## 4. Merge the PR + +- Once all checks are green, merge with `gh pr merge --squash --delete-branch` +- If merge is blocked, investigate why (reviews required, checks pending) and address it +- Confirm merge succeeded + +## 5. Wait for Release-Please PR + +- After merge to main, the Release workflow runs and release-please creates/updates a release PR +- Poll with `gh pr list --label "autorelease: pending"` until the release PR appears +- Note the release PR number + +## 6. Wait for Release PR Checks + +- The release PR gets CI checks via release-pr-checks.yml (triggered by close/reopen) +- Watch with `gh pr checks --watch` +- If checks fail, you may need to push fixes to main and wait for release-please to update the PR + +## 7. Merge the Release PR + +- Once checks pass: `gh pr merge --squash --delete-branch` +- This triggers release-please to publish a GitHub release with version tag + +## 8. Verify the Release + +- Confirm the GitHub release was created: `gh release list --limit 1` +- Note the version number + +## 9. Wait for Docker Image + +- The docker-publish.yml workflow triggers automatically after the Release workflow completes +- Watch it: `gh run list --workflow=docker-publish.yml --limit 1` and `gh run watch ` +- If it doesn't trigger within 2 minutes, check if the workflow_run event fired +- As a fallback, manually trigger: `gh workflow run docker-publish.yml -f version=` + +## 10. Confirm Docker Image Published + +- Verify the image exists: `gh api /orgs/312-dev/packages/container/scrolly/versions --jq '.[0].metadata.container.tags'` +- Confirm both `` and `latest` tags are present +- Report the full image reference: `ghcr.io/312-dev/scrolly:` + +## 11. Update Documentation + +- Review the changes that were released and check if any docs in `docs/` are now outdated or would benefit from updates +- Key docs to check: `docs/api.md` (new/changed endpoints), `docs/data-model.md` (schema changes), `docs/architecture.md` (structural changes, directory tree, ASCII diagrams), `docs/design-guidelines.md` (UI/component changes), `docs/notifications.md` (notification changes) +- Check any ASCII diagrams, directory trees, or architecture diagrams in the docs for inaccuracies — if files were added/removed/moved or the architecture changed, update the diagrams to match reality +- Only update docs that are **clearly affected** by the changes — don't touch docs that are still accurate +- If docs were updated, commit them as `docs: update for ` and push directly to main +- Skip this step entirely if the changes are purely internal (refactors, dep bumps, CI tweaks) with no user-facing or API impact + +## Important Notes + +- Never force-push to main +- If anything gets stuck, check `gh run list` for workflow status and `gh run view --log-failed` for errors +- The full pipeline (CI + release + Docker build) takes ~15-20 minutes total +- Keep the user informed of progress at each major step diff --git a/package-lock.json b/package-lock.json index 1c7f033..4ccc2a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "better-sqlite3": "^12.6.2", + "bplist-parser": "^0.3.2", "drizzle-orm": "^0.45.1", "mrmime": "^2.0.1", "pino": "^10.3.1", @@ -3977,6 +3978,15 @@ "node": "20.x || 22.x || 23.x || 24.x || 25.x" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -4013,6 +4023,18 @@ "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "license": "MIT" }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, "node_modules/brace-expansion": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", diff --git a/package.json b/package.json index b75550b..0dfe9b8 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "better-sqlite3": "^12.6.2", + "bplist-parser": "^0.3.2", "drizzle-orm": "^0.45.1", "mrmime": "^2.0.1", "pino": "^10.3.1", diff --git a/src/app.html b/src/app.html index e74f1f3..59afad6 100644 --- a/src/app.html +++ b/src/app.html @@ -2,7 +2,7 @@ - + @@ -15,7 +15,7 @@ - + diff --git a/src/hooks.server.ts b/src/hooks.server.ts index eb0085e..c46ca98 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,5 +1,5 @@ import type { Handle, RequestEvent } from '@sveltejs/kit'; -import { getUserIdFromCookies, getUserWithGroup } from '$lib/server/auth'; +import { getUserIdFromCookies, getUserWithGroup, getDefaultGroup } from '$lib/server/auth'; import { getAccentColor } from '$lib/colors'; import { startScheduler } from '$lib/server/scheduler'; import { createLogger } from '$lib/server/logger'; @@ -103,6 +103,12 @@ export const handle: Handle = async ({ event, resolve }) => { } } + // For unauthenticated requests, still resolve the group so accent color, + // dynamic icons, and PWA branding work on /join, /onboard, etc. + if (!event.locals.group) { + event.locals.group = await getDefaultGroup(); + } + const response = await resolve(event, { transformPageChunk: ({ html }) => { const theme = event.locals.user?.themePreference; diff --git a/src/lib/components/AddVideo.svelte b/src/lib/components/AddVideo.svelte index 8acb401..8b169a3 100644 --- a/src/lib/components/AddVideo.svelte +++ b/src/lib/components/AddVideo.svelte @@ -187,6 +187,8 @@ import type { Snippet } from 'svelte'; - import { pushState } from '$app/navigation'; + import { pushState, beforeNavigate } from '$app/navigation'; import { onDestroy } from 'svelte'; let { @@ -23,6 +23,11 @@ let closedViaBack = false; let timers: ReturnType[] = []; + // Prevent history.back() in cleanup when a real navigation occurs (e.g. clicking a link inside the sheet) + beforeNavigate(() => { + closedViaBack = true; + }); + function safeTimeout(fn: () => void, ms: number) { const id = setTimeout(fn, ms); timers.push(id); diff --git a/src/lib/components/FilterBar.svelte b/src/lib/components/FilterBar.svelte index 99de8e5..365545b 100644 --- a/src/lib/components/FilterBar.svelte +++ b/src/lib/components/FilterBar.svelte @@ -6,13 +6,17 @@ onfilter, swipeProgress = 0, swiping = false, - hidden = false + hidden = false, + unwatchedCount = 0, + pullOffset = 0 }: { filter: FeedFilter; onfilter: (f: FeedFilter) => void; swipeProgress?: number; swiping?: boolean; hidden?: boolean; + unwatchedCount?: number; + pullOffset?: number; } = $props(); const filters: FeedFilter[] = ['unwatched', 'watched', 'favorites']; @@ -57,7 +61,12 @@ }); -
+
0 ? `translateY(${pullOffset}px)` : undefined} +>
{#each filters as f, i (f)} {/each}
@@ -97,6 +111,12 @@ opacity: 0; } + .filter-bar.pull-snapping { + transition: + opacity 0.3s ease, + transform 0.25s ease; + } + .filter-tabs { position: relative; display: flex; @@ -122,6 +142,24 @@ color: var(--reel-text); } + .badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 5px; + margin-left: 4px; + background: var(--accent-magenta); + color: #fff; + font-family: var(--font-body); + font-size: 0.6875rem; + font-weight: 700; + line-height: 1; + border-radius: var(--radius-full); + vertical-align: middle; + } + .tab-indicator { position: absolute; bottom: 0; diff --git a/src/lib/components/InstallBanner.svelte b/src/lib/components/InstallBanner.svelte index 184d2c4..e4ff76b 100644 --- a/src/lib/components/InstallBanner.svelte +++ b/src/lib/components/InstallBanner.svelte @@ -30,7 +30,7 @@ - scrolly + scrolly
scrolly Add to your Home Screen @@ -68,7 +68,7 @@ - +