Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
108 changes: 30 additions & 78 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ All API routes are SvelteKit `+server.ts` files under `src/routes/api/`. Authent

### GET /api/auth
```
Response: { user: { id, username, phone, groupId, themePreference, autoScroll, mutedByDefault, avatarPath }, group: { id, name, inviteCode, accentColor, retentionDays, ... } }
Response: { user: { id, username, phone, groupId, themePreference, autoScroll, mutedByDefault, avatarPath, postCredits }, group: { id, name, inviteCode, accentColor, retentionDays, reciprocitySeed, reciprocityCap, reciprocityRatio, ... } }
```

### POST /api/auth
Expand Down Expand Up @@ -81,17 +81,17 @@ Each clip includes: id, originalUrl, title, addedByUsername, addedByAvatar, stat
### POST /api/clips
```
Request: { "url": "https://tiktok.com/...", "title": "optional caption" }
Response: { "clip": { "id", "status": "downloading", "contentType" } } (201 Created)
Response: { "clip": { "id", "status": "downloading", "contentType" }, "queued": false, "credits": 4, "queueLength": 0 } (201 Created)
```
Triggers the download pipeline via the active provider. Requires a download provider to be configured (see Settings). Returns immediately with status `downloading`.
Triggers the download pipeline via the active provider. Requires a download provider to be configured (see Settings). Returns immediately with status `downloading`. `queued` is `true` when the clip went to the user's queue instead of posting directly (no credit available or the queue was non-empty). The credit is spent at publish time, so a failed download costs nothing.

### POST /api/clips/share
Authenticated via `?token=` query parameter (iOS Shortcut token) or session cookie (web view). Allows sharing clips without a session cookie.
```
Request: { "url": "https://tiktok.com/...", "phone": "+1234567890" }
Response: { "ok": true, "clipId": "...", "status": "downloading" } (201 Created)
Response: { "success": 1, "message": "...", "clipId": "...", "queued": false, "credits": 4, "queueLength": 0 } (201 Created)
```
Also accepts `"phones": ["+1234567890"]` (array) for legacy Shortcut backward compatibility. Records legacy share timestamp for upgrade banner tracking.
Same reciprocity rule as `POST /api/clips`: posts directly when the sharer has a credit and an empty queue, otherwise holds it in the queue. Also accepts `"phones": ["+1234567890"]` (array) for legacy Shortcut backward compatibility. Records legacy share timestamp for upgrade banner tracking.

### GET /api/clips/[id]
Returns full clip detail with user context, interaction state, and metadata.
Expand Down Expand Up @@ -161,8 +161,9 @@ Returns dismissed clips with thumbnail, platform, uploader info, and dismissal t
### POST /api/clips/[id]/watched
```
Request: { "watchPercent": 85 } (optional, 0–100)
Response: { "watched": true }
Response: { "watched": true, "credits": 3 }
```
`credits` is the user's reciprocity balance after this watch. The first watch of another member's clip earns a credit (subject to the group's `ratio` and the launch cutoff); self-watches and repeat watches don't.

### PATCH /api/clips/[id]/watched
Updates watch percent without marking the clip as watched. Only updates existing watched records — does not create new ones. Used for periodic progress tracking while the user is still viewing.
Expand Down Expand Up @@ -245,62 +246,27 @@ Extends the trim deadline for a music clip in `pending_trim` status. The client
Response: { "ok": true }
```

## Clout (Reputation)
## Reciprocity (Credits)

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/clout` | Get user's clout score, tier, and breakdown |
| POST | `/api/clout` | Acknowledge tier change modal was shown |

### GET /api/clout
Returns the user's clout score and tier when queue pacing is enabled. Clout is computed from the engagement on the user's last 10 eligible clips (watched by ≥75% of group). Users with fewer than 10 eligible clips default to Rising tier.
```
Response: {
"enabled": true,
"score": 0.8,
"tier": "viral",
"tierName": "Viral",
"cooldownMinutes": 120,
"burstSize": 3,
"queueLimit": null,
"icon": "/icons/clout/viral.png",
"breakdown": [{ "clipId": "...", "score": 2 }, ...],
"nextTier": { "tier": "iconic", "tierName": "Iconic", "minScore": 1.0, "burst": 5, "queueLimit": null, "icon": "..." },
"lastTier": "rising",
"tierChanged": true
}
```

**Tiers:** Fresh (<0.4) → Rising (0.4–0.6) → Viral (0.7–0.9) → Iconic (≥1.0). Each tier adjusts cooldown multiplier, burst size, and queue depth limit.

**Per-clip scoring:** 0 = no reactions/favorites from others, 1 = reaction or favorite but no comment, 2 = reaction/favorite AND comment. Self-interactions excluded. Only clips watched by ≥75% of other group members are eligible.

**Rank-down protection:** Rank-ups apply immediately. Rank-downs only take effect if the user has held their current tier for ≥4 days. The `cloutTierChangedAt` column tracks when the effective tier last changed.

**Tier change detection:** The server tracks each user's last effective tier (`cloutTier`) and when it changed (`cloutTierChangedAt`). When the tier actually changes (after rank-down protection), `tierChanged: true` is returned.

### POST /api/clout
Acknowledges that the tier change modal was shown. Updates the user's stored tier and resets the cooldown timer.
```
Response: { "ok": true }
```
Pacing is governed by a per-user credit balance: watching others' clips earns credits, posting spends them. See `docs/data-model.md` for the full model (including the launch cutoff that stops the pre-launch backlog from minting credits). The balance is surfaced on `GET /api/auth` (`user.postCredits`) and returned by the watch and queue-post endpoints; there is no standalone GET endpoint.

## Queue Management

Manage queued clips when share pacing is enabled.
The queue is an unlimited personal holding area. Clips wait here (`status: 'queued'`) until the user spends a credit to post them — there is no timed/automatic publishing.

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/queue` | List queued clips for user |
| GET | `/api/queue` | List queued clips for user (paginated) |
| DELETE | `/api/queue` | Clear entire queue |
| GET | `/api/queue/count` | Queued clip count |
| POST | `/api/queue/post` | Post selected queued clips to the feed |
| DELETE | `/api/queue/[id]` | Cancel a queued clip |
| POST | `/api/queue/[id]/move-to-top` | Move entry to top of queue |
| PATCH | `/api/queue/reorder` | Reorder queue entries |

### GET /api/queue
Sorted newest-first by default.
```
Response: { "queue": [{ "id", "clipId", "position", "scheduledAt", "sharesIn", "createdAt", "title", "originalUrl", "platform", "contentType", "status", "thumbnailPath" }] }
Query params: ?limit=20&offset=0&order=newest|oldest
Response: { "queue": [{ "id", "clipId", "createdAt", "title", "originalUrl", "platform", "contentType", "status", "thumbnailPath" }], "total": 12, "hasMore": true }
```

### DELETE /api/queue
Expand All @@ -314,22 +280,16 @@ Response: { "cleared": 3 }
Response: { "count": 5 }
```

### DELETE /api/queue/[id]
Cancels a single queued clip. Only the uploader can cancel.
```
Response: { "ok": true }
```

### POST /api/queue/[id]/move-to-top
Moves a queue entry to position 0 (next to publish).
### POST /api/queue/post
Posts selected queued clips to the feed, spending one credit each. Only the caller's own `'queued'` entries are eligible; the server self-limits to available credits (extra IDs are skipped once credits run out).
```
Response: { "ok": true }
Request: { "ids": ["entry-id-1", "entry-id-2"] }
Response: { "posted": ["clip-id-1", "clip-id-2"], "credits": 3, "queueLength": 0 }
```

### PATCH /api/queue/reorder
Reorders all queue entries and recalculates scheduled publish times.
### DELETE /api/queue/[id]
Cancels a single queued clip. Only the uploader can cancel.
```
Request: { "orderedIds": ["entry-id-1", "entry-id-2", "entry-id-3"] }
Response: { "ok": true }
```

Expand All @@ -344,8 +304,7 @@ Host-only endpoints (unless noted). Requires `createdBy === currentUser`.
| PATCH | `/api/group/retention` | Set retention policy |
| PATCH | `/api/group/max-file-size` | Set max file size limit |
| PATCH | `/api/group/platforms` | Set platform filter |
| PATCH | `/api/group/daily-share-limit` | Set daily share limit per user |
| PATCH | `/api/group/share-pacing` | Configure share pacing mode |
| PATCH | `/api/group/reciprocity` | Configure reciprocity pacing (seed/cap/ratio) |
| GET | `/api/group/provider` | List download providers |
| PATCH | `/api/group/provider` | Set active provider |
| POST | `/api/group/provider/install` | Install a provider |
Expand Down Expand Up @@ -389,23 +348,16 @@ Request: { "mode": "all", "platforms": [] } (mode: "all" | "allow" | "block")
Response: { "platformFilterMode": "all", "platformFilterList": null }
```

### PATCH /api/group/daily-share-limit
```
Request: { "dailyShareLimit": 5 } (positive integer, or null to remove limit)
Response: { "dailyShareLimit": 5 }
```

### PATCH /api/group/share-pacing
Host-only. Configures queue-based share pacing. When switching away from `queue` mode, all queued clips are flushed to `ready`.
### PATCH /api/group/reciprocity
Host-only. Configures the watch-to-post credit economy. Partial updates merge with current values. Lowering the cap clamps existing members' balances down to it.
```
Request: { "sharePacingMode": "queue", "shareBurst": 2, "shareCooldownMinutes": 120, "dailyShareLimit": null, "cloutEnabled": true }
Response: { "sharePacingMode": "queue", "shareBurst": 2, "shareCooldownMinutes": 120, "dailyShareLimit": null, "cloutEnabled": true }
Request: { "seed": 5, "cap": 5, "ratio": 1, "dailyPostLimit": null }
Response: { "seed": 5, "cap": 5, "ratio": 1, "dailyPostLimit": null }
```
- `sharePacingMode`: `"off"` | `"daily_cap"` | `"queue"`
- `shareBurst`: 1–10 (clips per scheduled time slot)
- `shareCooldownMinutes`: 30 | 60 | 120 | 240 | 360
- `dailyShareLimit`: positive integer or null
- `cloutEnabled`: boolean (enables reputation-based queue adjustments)
- `seed`: starting credits for new members (`0 ≤ seed ≤ cap`)
- `cap`: max credits a member can hold (`1`–`20`)
- `ratio`: qualifying watches needed to earn one credit (`1`–`10`)
- `dailyPostLimit`: hard ceiling on posts per rolling 24h, or `null` for unlimited (`1`–`100`)

### GET /api/group/provider
```
Expand Down
21 changes: 10 additions & 11 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ scrolly/
│ │ │ ├── auth.ts # Session management, invite code validation
│ │ │ ├── push.ts # web-push wrapper, group notifications
│ │ │ ├── share-limit.ts # Share pacing modes and daily limit enforcement
│ │ │ ├── queue.ts # Clip queue management (enqueue, publish, reorder)
│ │ │ ├── queue.ts # Personal holding queue (enqueue, publish, post-selected)
│ │ │ ├── credits.ts # Reciprocity credit economy (earn/spend/config/cutoff)
│ │ │ ├── scheduler.ts # Retention policy enforcement (periodic cleanup)
│ │ │ └── download-lock.ts # Prevents duplicate concurrent downloads
│ │ ├── components/
Expand All @@ -97,10 +98,9 @@ scrolly/
│ │ │ ├── AddVideo.svelte # Add video form
│ │ │ ├── AddVideoModal.svelte # Modal wrapper for AddVideo
│ │ │ ├── AvatarCropModal.svelte # Profile picture crop UI
│ │ │ ├── QueueSheet.svelte # Bottom sheet for viewing/reordering queued clips
│ │ │ ├── QueueCloutBanner.svelte # Clout tier banner inside QueueSheet
│ │ │ ├── CloutChangeModal.svelte # Tier change celebration modal
│ │ │ ├── CloutTipsView.svelte # Clout tips/breakdown view (tier details, scoring)
│ │ │ ├── QueueSheet.svelte # Bottom sheet: multi-select + post queued clips
│ │ │ ├── CreditDots.svelte # Tappable credits indicator (opens CreditInfoModal)
│ │ │ ├── CreditInfoModal.svelte # Explains the watch-to-post economy
│ │ │ ├── CatchUpModal.svelte # Catch-up modal for bulk unwatched clips
│ │ │ ├── MeGrid.svelte # Profile clip grid (favorites/uploads)
│ │ │ ├── MeReelView.svelte # Profile reel overlay view
Expand All @@ -122,15 +122,13 @@ scrolly/
│ │ │ ├── PlatformIcon.svelte # Platform logo (TikTok, IG, etc.)
│ │ │ ├── InlineError.svelte
│ │ │ ├── FilterBar.svelte # Feed filter tabs
│ │ │ ├── ShareLimitDots.svelte # Daily share limit indicator dots
│ │ │ ├── ShortcutGuideSheet.svelte # iOS Shortcut setup guide
│ │ │ ├── ShortcutUpgradeBanner.svelte # Legacy shortcut upgrade prompt
│ │ │ └── settings/
│ │ │ ├── GroupNameEdit.svelte
│ │ │ ├── InviteLink.svelte
│ │ │ ├── MemberList.svelte
│ │ │ ├── DailyShareLimitPicker.svelte # Daily per-user share limit control
│ │ │ ├── SharePacingPicker.svelte # Share pacing mode, burst, and cooldown config
│ │ │ ├── ReciprocityPicker.svelte # Watch-to-post seed/cap/ratio config
│ │ │ ├── RetentionPicker.svelte
│ │ │ ├── SkippedClips.svelte # Dismissed/skipped clips viewer with restore
│ │ │ ├── ClipsManager.svelte
Expand All @@ -155,6 +153,7 @@ scrolly/
│ │ │ ├── homeTap.ts # Double-tap home to scroll to top
│ │ │ ├── catchUpModal.ts # Catch-up modal dismissal state (12-hour cooldown)
│ │ │ ├── queue.ts # Queue count store and fetch function
│ │ │ ├── credits.ts # Reciprocity credits, cap, derived feed-gate state
│ │ │ ├── queueSheet.ts # Queue sheet visibility state
│ │ │ ├── shortcutNudge.ts # Share shortcut install nudge
│ │ │ └── shortcutUpgrade.ts # Shortcut upgrade banner state
Expand Down Expand Up @@ -191,12 +190,12 @@ scrolly/
│ │ │ │ └── [id]/publish/+server.ts # Publish after trim
│ │ │ ├── gifs/
│ │ │ ├── queue/
│ │ │ │ ├── +server.ts # GET list / DELETE clear queue
│ │ │ │ ├── +server.ts # GET list (paginated) / DELETE clear queue
│ │ │ │ ├── count/+server.ts # GET queue count
│ │ │ │ ├── reorder/+server.ts # PATCH reorder entries
│ │ │ │ ├── post/+server.ts # POST selected queued clips to feed
│ │ │ │ └── [id]/+server.ts # DELETE cancel entry
│ │ │ ├── group/
│ │ │ │ ├── share-pacing/+server.ts # PATCH configure share pacing
│ │ │ │ ├── reciprocity/+server.ts # PATCH configure watch-to-post pacing
│ │ │ ├── notifications/
│ │ │ │ └── [id]/+server.ts # Delete single notification
│ │ │ ├── profile/
Expand Down
Loading
Loading