From 393f2c36eaea377c8cd0c6e5310684d19ab146f8 Mon Sep 17 00:00:00 2001 From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com> Date: Sun, 1 Mar 2026 17:34:50 -0600 Subject: [PATCH 1/3] docs: update api, architecture, and notifications for v1.7.0 --- docs/api.md | 17 +++++++++++++---- docs/architecture.md | 4 ++++ docs/data-model.md | 1 + docs/notifications.md | 1 + 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/api.md b/docs/api.md index 5a4c0c0..3e1cdb9 100644 --- a/docs/api.md +++ b/docs/api.md @@ -68,9 +68,11 @@ Response: { "user": { ... }, "group": { ... } } ### GET /api/clips ``` -Query params: ?filter=unwatched|watched|favorites&limit=20&offset=0 +Query params: ?filter=unwatched|watched|favorites&sort=oldest|round-robin&limit=20&offset=0 Response: { "clips": [...], "hasMore": true } ``` +Only returns clips with `status: 'ready'`. Default sort is `oldest` (chronological). `round-robin` interleaves clips across members so no single poster dominates the feed. The `watched` filter sorts by most-recently-watched instead. + Each clip includes: id, originalUrl, title, addedByUsername, addedByAvatar, status, durationSeconds, platform, contentType, createdAt, watched, favorited, reactions, commentCount, unreadCommentCount, viewCount, seenByOthers. ### POST /api/clips @@ -335,10 +337,10 @@ Response: { "preferences": { ... } } ### POST /api/profile/preferences ``` -Request: { "themePreference": "dark", "autoScroll": true, "mutedByDefault": false } -Response: { "themePreference": "dark", "autoScroll": true, "mutedByDefault": false } +Request: { "themePreference": "dark", "autoScroll": true, "mutedByDefault": false, "feedSortOrder": "oldest" } +Response: { "themePreference": "dark", "autoScroll": true, "mutedByDefault": false, "feedSortOrder": "oldest" } ``` -All fields optional — only provided fields are updated. +All fields optional — only provided fields are updated. `feedSortOrder` accepts `"oldest"` or `"round-robin"`. ### POST /api/profile/avatar Upload a profile picture as `multipart/form-data`. @@ -373,6 +375,7 @@ Response: { "gifs": [{ "id", "title", "url", "stillUrl", "width", "height" }] } |--------|------|-------------| | POST | `/api/push/subscribe` | Register a push subscription | | DELETE | `/api/push/subscribe` | Unregister | +| POST | `/api/push/test` | Send a test notification to current user | ### POST /api/push/subscribe ``` @@ -380,6 +383,12 @@ Request: { "endpoint": "...", "keys": { "p256dh": "...", "auth": "..." } } Response: { "id": "subscription-id" } (201 Created) ``` +### POST /api/push/test +Sends a test push notification to the current user after a 10-second delay. Requires at least one active push subscription. +``` +Response: { "sent": true, "sentAt": 1740000000000 } +``` + ## Media | Method | Path | Description | diff --git a/docs/architecture.md b/docs/architecture.md index 38fb380..9275372 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -97,6 +97,10 @@ scrolly/ │ │ │ ├── MemberList.svelte │ │ │ ├── RetentionPicker.svelte │ │ │ ├── ClipsManager.svelte +│ │ │ ├── NotificationSettings.svelte # Push toggle + test button +│ │ │ ├── ShortcutManager.svelte # iOS Shortcut config wrapper +│ │ │ ├── ShortcutSheet.svelte # Shortcut setup sheet content +│ │ │ ├── ValidationResults.svelte # Shared validation display │ │ │ ├── GettingStartedChecklist.svelte │ │ │ └── SetupDoneState.svelte │ │ ├── stores/ diff --git a/docs/data-model.md b/docs/data-model.md index b169191..ba24314 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -34,6 +34,7 @@ SQLite database via Drizzle ORM. All IDs are UUIDs stored as text. Timestamps ar | theme_preference | text | `'system'` / `'light'` / `'dark'`. Default `'system'`. | | auto_scroll | integer | Boolean (0/1). Default 0. | | muted_by_default | integer | Boolean (0/1). Default 1. | +| feed_sort_order | text | `'oldest'` / `'round-robin'`. Default `'oldest'`. | | avatar_path | text | Nullable. Path to uploaded profile picture. | | removed_at | integer | Nullable. Unix timestamp when removed from group. | | created_at | integer | Unix timestamp | diff --git a/docs/notifications.md b/docs/notifications.md index 71ab9be..c6c5b97 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -96,6 +96,7 @@ VAPID_SUBJECT=mailto:you@example.com |-------|------|------| | Server push utility | `src/lib/server/push.ts` | VAPID init, `sendNotification()`, `sendGroupNotification()`, `notifyNewClip()` | | Subscribe API | `src/routes/api/push/subscribe/+server.ts` | POST/DELETE push subscriptions | +| Test API | `src/routes/api/push/test/+server.ts` | POST test notification to current user | | Notifications API | `src/routes/api/notifications/+server.ts` | GET notification feed | | Mark-read API | `src/routes/api/notifications/mark-read/+server.ts` | POST mark as read | | Unread-count API | `src/routes/api/notifications/unread-count/+server.ts` | GET unread badge count | From 64ee652365773324a0c149122c8ae6ddad6b85f3 Mon Sep 17 00:00:00 2001 From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com> Date: Sun, 1 Mar 2026 17:39:24 -0600 Subject: [PATCH 2/3] fix(ci): remove paths-ignore from security workflow The security-status check is required by branch protection, but paths-ignore prevented the workflow from running on docs-only PRs. The changes job already handles skipping heavy scans for non-code changes, so paths-ignore is redundant and causes merge blocks. --- .github/workflows/security.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 4c81d93..2bd9750 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -3,10 +3,8 @@ name: Security on: pull_request: branches: [main] - paths-ignore: ['docs/**', '*.md'] push: branches: [main] - paths-ignore: ['docs/**', '*.md'] schedule: - cron: '0 0 * * 1' # Weekly on Monday midnight UTC workflow_dispatch: From da6c6d98ad30395cc19b124ace4d3b743679a3b1 Mon Sep 17 00:00:00 2001 From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com> Date: Sun, 1 Mar 2026 17:39:31 -0600 Subject: [PATCH 3/3] fix(ci): improve release PR check trigger reliability - Add workflow_run fallback trigger to release-pr-checks.yml so checks run even if the close/reopen mechanism fails to fire events - Add state verification and retry logic to the close/reopen step - Use outputs to pass PR HEAD SHA between jobs for both trigger types --- .github/workflows/release-pr-checks.yml | 51 ++++++++++++++++++++++--- .github/workflows/release.yml | 48 ++++++++++++++++++++--- 2 files changed, 88 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release-pr-checks.yml b/.github/workflows/release-pr-checks.yml index dfa85ca..ad2d03b 100644 --- a/.github/workflows/release-pr-checks.yml +++ b/.github/workflows/release-pr-checks.yml @@ -6,6 +6,12 @@ # Without this, release PRs have no status checks and can't be merged when # branch protection requires them. # +# Triggers: +# 1. pull_request_target (opened/synchronize/reopened) — fired by close/reopen +# in release.yml or by direct PR updates +# 2. workflow_run (Release workflow completed) — fallback if close/reopen fails +# to fire pull_request_target events +# # Security: restricted to release-please branches only. The checkout uses the # PR's HEAD SHA, which is safe because release-please PRs come from within the # same repository (not forks) and only modify version/changelog files. @@ -16,26 +22,54 @@ on: pull_request_target: branches: [main] types: [opened, synchronize, reopened] + workflow_run: + workflows: [Release] + types: [completed] permissions: read-all jobs: - # Gate: only run for release-please PRs + # Gate: only run for release-please PRs. Finds PR details from either trigger. should-run: - if: startsWith(github.head_ref, 'release-please--') + if: | + (github.event_name == 'pull_request_target' && startsWith(github.head_ref, 'release-please--')) || + github.event_name == 'workflow_run' runs-on: ubuntu-latest + outputs: + head_sha: ${{ steps.find-pr.outputs.head_sha }} steps: - - run: echo "Running CI for release-please PR" + - name: Find release PR + id: find-pr + env: + GH_TOKEN: ${{ github.token }} + run: | + if [ "${{ github.event_name }}" = "pull_request_target" ]; then + echo "Triggered by pull_request_target" + echo "head_sha=${{ github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT" + else + # workflow_run trigger — find the open release-please PR + PR=$(gh pr list --repo "$GITHUB_REPOSITORY" --json number,headRefName,headRefOid --jq '.[] | select(.headRefName | startswith("release-please--"))' 2>/dev/null || echo "") + if [ -z "$PR" ] || [ "$PR" = "null" ]; then + echo "No open release-please PR found, skipping" + echo "head_sha=" >> "$GITHUB_OUTPUT" + exit 0 + fi + HEAD_SHA=$(echo "$PR" | jq -r '.headRefOid') + PR_NUM=$(echo "$PR" | jq -r '.number') + echo "Found release PR #$PR_NUM (sha: $HEAD_SHA)" + echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT" + fi lint-and-check: needs: [should-run] + if: needs.should-run.outputs.head_sha != '' runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout PR code uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ needs.should-run.outputs.head_sha }} - name: Setup Node.js uses: actions/setup-node@v4 @@ -64,10 +98,14 @@ jobs: ci: runs-on: ubuntu-latest if: always() - needs: [lint-and-check] + needs: [should-run, lint-and-check] steps: - name: Check CI status run: | + if [[ "${{ needs.should-run.outputs.head_sha }}" == "" ]]; then + echo "No release PR found, skipping" + exit 0 + fi if [[ "${{ needs.lint-and-check.result }}" == "failure" || "${{ needs.lint-and-check.result }}" == "cancelled" ]]; then echo "CI failed" exit 1 @@ -76,6 +114,7 @@ jobs: codeql: needs: [should-run] + if: needs.should-run.outputs.head_sha != '' runs-on: ubuntu-latest timeout-minutes: 15 permissions: @@ -84,7 +123,7 @@ jobs: - name: Checkout PR code uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ needs.should-run.outputs.head_sha }} - name: Initialize CodeQL uses: github/codeql-action/init@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24db1ac..9ad7582 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,6 +35,7 @@ jobs: # When release-please creates or updates a PR via GITHUB_TOKEN, the # pull_request_target `synchronize` event doesn't always fire. Closing # and reopening the PR ensures the `reopened` event triggers CI checks. + # We verify both state transitions and retry once on failure. trigger-pr-checks: needs: [release-please] if: needs.release-please.outputs.pr != '' && needs.release-please.outputs.release_created != 'true' @@ -45,8 +46,45 @@ jobs: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ fromJSON(needs.release-please.outputs.pr).number }} run: | - echo "Triggering checks on release PR #$PR_NUMBER" - gh pr close "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" - sleep 2 - gh pr reopen "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" - echo "Release PR #$PR_NUMBER reopened — checks should now trigger" + trigger_checks() { + echo "Closing release PR #$PR_NUMBER..." + gh pr close "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" + + # Wait and verify closed state + for i in 1 2 3; do + sleep 3 + STATE=$(gh pr view "$PR_NUMBER" --json state --jq '.state' --repo "$GITHUB_REPOSITORY") + if [ "$STATE" = "CLOSED" ]; then break; fi + echo " Waiting for close to propagate (attempt $i)..." + done + + echo "Reopening release PR #$PR_NUMBER..." + gh pr reopen "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" + + # Verify reopened state + sleep 3 + STATE=$(gh pr view "$PR_NUMBER" --json state --jq '.state' --repo "$GITHUB_REPOSITORY") + if [ "$STATE" = "OPEN" ]; then + echo "Release PR #$PR_NUMBER successfully reopened" + return 0 + else + echo "WARNING: PR state is $STATE after reopen attempt" + return 1 + fi + } + + # First attempt + if trigger_checks; then + exit 0 + fi + + echo "First attempt failed, retrying in 10s..." + sleep 10 + + # Retry once + if trigger_checks; then + exit 0 + fi + + echo "ERROR: Failed to reopen PR after 2 attempts" + exit 1