Merge: promote dev to prod
This commit is contained in:
commit
f0c58b16aa
31 changed files with 65 additions and 56 deletions
|
|
@ -1,9 +1,9 @@
|
|||
# ---- Copy this file to .env and fill in the values ----
|
||||
|
||||
# Postgres (used by docker-compose for the db service and the DATABASE_URL)
|
||||
POSTGRES_USER=subfeed
|
||||
POSTGRES_USER=siftlode
|
||||
POSTGRES_PASSWORD=change-this-password
|
||||
POSTGRES_DB=subfeed
|
||||
POSTGRES_DB=siftlode
|
||||
|
||||
# Host port the app is exposed on (http://localhost:<APP_PORT>)
|
||||
APP_PORT=8080
|
||||
|
|
@ -51,7 +51,7 @@ SMTP_FROM=
|
|||
# --- Deployment role ---
|
||||
# DATABASE_URL is set automatically by docker-compose.yml / docker-compose.server.yml to the
|
||||
# bundled `db` service. Override it only for local dev against the central server DB, e.g.:
|
||||
# DATABASE_URL=postgresql+psycopg://subfeed:<password>@your-db-host:5432/subfeed
|
||||
# DATABASE_URL=postgresql+psycopg://siftlode:<password>@your-db-host:5432/siftlode
|
||||
# Exactly one instance may run the background scheduler. The central server keeps it on; every
|
||||
# other instance (local dev, etc.) must set SCHEDULER_ENABLED=false. The compose files set this
|
||||
# for you (server=true via docker-compose.server.yml, local=false via docker-compose.localdev.yml).
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ sync.
|
|||
depend on a refresh token (which expires after 7 days while the OAuth screen is in *Testing*).
|
||||
|
||||
- **Local dev** (`docker-compose.localdev.yml`): webapp only, no local DB, no scheduler. In `.env`
|
||||
set `DATABASE_URL` to the central DB (e.g. `…@your-db-host:5432/subfeed`), then
|
||||
set `DATABASE_URL` to the central DB (e.g. `…@your-db-host:5432/siftlode`), then
|
||||
`docker compose -f docker-compose.localdev.yml up --build` and browse http://localhost:8080.
|
||||
|
||||
**Exactly one instance may run the scheduler.** The server keeps `SCHEDULER_ENABLED=true`; every
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.14.0
|
||||
0.14.1
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ BASE_SCOPES = "openid email profile"
|
|||
READ_SCOPES = f"{BASE_SCOPES} {READ_SCOPE}"
|
||||
WRITE_SCOPES = f"{BASE_SCOPES} {READ_SCOPE} {WRITE_SCOPE}"
|
||||
|
||||
log = logging.getLogger("subfeed.auth")
|
||||
log = logging.getLogger("siftlode.auth")
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class Settings(BaseSettings):
|
|||
git_sha: str = "unknown"
|
||||
build_date: str = ""
|
||||
|
||||
database_url: str = "postgresql+psycopg://subfeed:subfeed@db:5432/subfeed"
|
||||
database_url: str = "postgresql+psycopg://siftlode:siftlode@db:5432/siftlode"
|
||||
|
||||
# Session cookie signing key.
|
||||
secret_key: str = _DEFAULT_SECRET_KEY
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from app import sysconfig
|
|||
from app.config import settings
|
||||
from app.db import SessionLocal
|
||||
|
||||
log = logging.getLogger("subfeed.email")
|
||||
log = logging.getLogger("siftlode.email")
|
||||
|
||||
|
||||
def _smtp() -> dict:
|
||||
|
|
|
|||
|
|
@ -5,20 +5,20 @@ from pathlib import Path
|
|||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
|
||||
# When started via uvicorn --log-config the "subfeed" logger is already configured
|
||||
# When started via uvicorn --log-config the "siftlode" logger is already configured
|
||||
# (see log_config.json). This block is a timestamped fallback for other entrypoints
|
||||
# (tests, scripts) so our logs are never silently dropped.
|
||||
_subfeed_logger = logging.getLogger("subfeed")
|
||||
if not _subfeed_logger.handlers:
|
||||
_siftlode_logger = logging.getLogger("siftlode")
|
||||
if not _siftlode_logger.handlers:
|
||||
_handler = logging.StreamHandler(sys.stdout)
|
||||
_handler.setFormatter(
|
||||
logging.Formatter("%(asctime)s %(levelname)-5s [%(name)s] %(message)s")
|
||||
)
|
||||
_subfeed_logger.addHandler(_handler)
|
||||
_subfeed_logger.setLevel(logging.INFO)
|
||||
_subfeed_logger.propagate = False
|
||||
_siftlode_logger.addHandler(_handler)
|
||||
_siftlode_logger.setLevel(logging.INFO)
|
||||
_siftlode_logger.propagate = False
|
||||
|
||||
log = logging.getLogger("subfeed.app")
|
||||
log = logging.getLogger("siftlode.app")
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ from app.youtube.client import YouTubeClient, YouTubeError
|
|||
|
||||
router = APIRouter(prefix="/api/channels", tags=["channels"])
|
||||
|
||||
log = logging.getLogger("subfeed.api")
|
||||
log = logging.getLogger("siftlode.api")
|
||||
|
||||
|
||||
@router.get("")
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ from app.sync.runner import (
|
|||
run_subscription_resync,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("subfeed.scheduler")
|
||||
logger = logging.getLogger("siftlode.scheduler")
|
||||
|
||||
_scheduler: BackgroundScheduler | None = None
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
|
|||
from app.config import settings
|
||||
from app.models import AppState
|
||||
|
||||
log = logging.getLogger("subfeed.state")
|
||||
log = logging.getLogger("siftlode.state")
|
||||
|
||||
# Bounds for the admin-set maintenance re-validation batch (videos re-checked per run).
|
||||
MAINTENANCE_BATCH_MIN = 100
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import re
|
|||
from sqlalchemy import and_, exists, func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
log = logging.getLogger("subfeed.autotag")
|
||||
log = logging.getLogger("siftlode.autotag")
|
||||
|
||||
from app import progress, sysconfig
|
||||
from app.models import Channel, ChannelTag, Tag, Video
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ from app.sync.runner import get_service_user
|
|||
from app.sync.videos import apply_video_details, parse_dt
|
||||
from app.youtube.client import YouTubeClient
|
||||
|
||||
log = logging.getLogger("subfeed.maintenance")
|
||||
log = logging.getLogger("siftlode.maintenance")
|
||||
|
||||
# Reasons recorded on Video.unavailable_reason. "removed" = absent from videos.list
|
||||
# (deleted or made private); "abandoned" = an upcoming premiere that never went live.
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from app.models import Channel, OAuthToken, Playlist, PlaylistItem, User, Video
|
|||
from app.sync.videos import apply_video_details, parse_dt
|
||||
from app.youtube.client import YouTubeClient, YouTubeError
|
||||
|
||||
log = logging.getLogger("subfeed.sync")
|
||||
log = logging.getLogger("siftlode.sync")
|
||||
|
||||
# Cost of a single YouTube write op (playlists/playlistItems insert/update/delete).
|
||||
WRITE_UNIT_COST = 50
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from sqlalchemy.orm import Session
|
|||
|
||||
from app import progress, quota, sysconfig
|
||||
|
||||
log = logging.getLogger("subfeed.sync")
|
||||
log = logging.getLogger("siftlode.sync")
|
||||
from app.models import Channel, OAuthToken, Subscription, User
|
||||
from app.sync.subscriptions import import_subscriptions
|
||||
from app.sync.videos import (
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
|
|||
from app.models import Channel, Subscription, User
|
||||
from app.youtube.client import YouTubeClient, best_thumbnail
|
||||
|
||||
log = logging.getLogger("subfeed.sync")
|
||||
log = logging.getLogger("siftlode.sync")
|
||||
|
||||
|
||||
def _to_int(value) -> int | None:
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from app.config import settings
|
|||
from app.models import User
|
||||
from app.security import decrypt
|
||||
|
||||
log = logging.getLogger("subfeed.youtube")
|
||||
log = logging.getLogger("siftlode.youtube")
|
||||
|
||||
GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
|
||||
API_BASE = "https://www.googleapis.com/youtube/v3"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,6 @@
|
|||
"uvicorn": { "handlers": ["default"], "level": "INFO", "propagate": false },
|
||||
"uvicorn.error": { "handlers": ["default"], "level": "INFO", "propagate": false },
|
||||
"uvicorn.access": { "handlers": ["access"], "level": "INFO", "propagate": false },
|
||||
"subfeed": { "handlers": ["plain"], "level": "INFO", "propagate": false }
|
||||
"siftlode": { "handlers": ["plain"], "level": "INFO", "propagate": false }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,15 +15,15 @@ services:
|
|||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-subfeed}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-subfeed}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-subfeed}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-siftlode}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-siftlode}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-siftlode}
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
security_opt:
|
||||
- apparmor:unconfined
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-subfeed} -d ${POSTGRES_DB:-subfeed}"]
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-siftlode} -d ${POSTGRES_DB:-siftlode}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
|
@ -40,7 +40,7 @@ services:
|
|||
env_file: .env
|
||||
environment:
|
||||
# Own local database (overrides whatever DATABASE_URL is in .env).
|
||||
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-subfeed}:${POSTGRES_PASSWORD:-subfeed}@db:5432/${POSTGRES_DB:-subfeed}
|
||||
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-siftlode}:${POSTGRES_PASSWORD:-siftlode}@db:5432/${POSTGRES_DB:-siftlode}
|
||||
# This instance owns its scheduler now (its own DB, so no double-write concern).
|
||||
SCHEDULER_ENABLED: "true"
|
||||
depends_on:
|
||||
|
|
|
|||
|
|
@ -14,16 +14,16 @@ services:
|
|||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-subfeed}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-siftlode}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set (run install.sh)}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-subfeed}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-siftlode}
|
||||
volumes:
|
||||
- siftlode_pgdata:/var/lib/postgresql/data
|
||||
networks: [internal]
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-subfeed} -d ${POSTGRES_DB:-subfeed}"]
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-siftlode} -d ${POSTGRES_DB:-siftlode}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
|
@ -34,7 +34,7 @@ services:
|
|||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-subfeed}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-subfeed}
|
||||
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-siftlode}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-siftlode}
|
||||
# This instance owns the background scheduler (single writer).
|
||||
SCHEDULER_ENABLED: "true"
|
||||
depends_on:
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ services:
|
|||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-subfeed}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-subfeed}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-subfeed}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-siftlode}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-siftlode}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-siftlode}
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-subfeed} -d ${POSTGRES_DB:-subfeed}"]
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-siftlode} -d ${POSTGRES_DB:-siftlode}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
|
@ -24,7 +24,7 @@ services:
|
|||
BUILD_DATE: ${BUILD_DATE:-}
|
||||
env_file: .env
|
||||
environment:
|
||||
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-subfeed}:${POSTGRES_PASSWORD:-subfeed}@db:5432/${POSTGRES_DB:-subfeed}
|
||||
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-siftlode}:${POSTGRES_PASSWORD:-siftlode}@db:5432/${POSTGRES_DB:-siftlode}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
|
|
|||
|
|
@ -57,9 +57,9 @@ const DEFAULT_FILTERS: FeedFilters = {
|
|||
show: "unwatched",
|
||||
};
|
||||
|
||||
const FILTERS_KEY = "subfeed.filters";
|
||||
const FILTERS_KEY = "siftlode.filters";
|
||||
const PAGE_KEY = "siftlode.page";
|
||||
const PERF_KEY = "subfeed.perfMode";
|
||||
const PERF_KEY = "siftlode.perfMode";
|
||||
|
||||
// The preferences edited on the Settings page. They apply locally for instant preview but
|
||||
// persist to the server only on an explicit Save — so App holds both the live "draft" (the
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// App-wide "hints" toggle: when on, Tooltip components reveal short explanatory
|
||||
// captions on hover so the UI is somewhat self-documenting for new users. Experienced
|
||||
// users can turn it off in Settings. Persisted to localStorage (+ server preferences).
|
||||
const KEY = "subfeed.hints";
|
||||
const KEY = "siftlode.hints";
|
||||
|
||||
let enabled = load();
|
||||
let listeners: Array<() => void> = [];
|
||||
|
|
|
|||
|
|
@ -71,8 +71,8 @@ export interface NotifyInput {
|
|||
transient?: boolean; // live status: sticky toast, not persisted (see Notification.transient)
|
||||
}
|
||||
|
||||
const HISTORY_KEY = "subfeed.notifications";
|
||||
const SETTINGS_KEY = "subfeed.notifSettings";
|
||||
const HISTORY_KEY = "siftlode.notifications";
|
||||
const SETTINGS_KEY = "siftlode.notifSettings";
|
||||
const MAX_HISTORY = 100;
|
||||
const ERROR_TTL = 15000;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@
|
|||
// suppresses the auto-popup on future logins (they
|
||||
// can still reopen it from Settings → Account).
|
||||
|
||||
export const ONBOARD_ACTIVE = "subfeed.onboarding.active";
|
||||
export const ONBOARD_DISMISSED = "subfeed.onboarding.dismissed";
|
||||
export const ONBOARD_ACTIVE = "siftlode.onboarding.active";
|
||||
export const ONBOARD_DISMISSED = "siftlode.onboarding.dismissed";
|
||||
|
||||
export function shouldAutoOpenOnboarding(me: {
|
||||
can_read: boolean;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,15 @@ export interface ReleaseEntry {
|
|||
}
|
||||
|
||||
export const RELEASE_NOTES: ReleaseEntry[] = [
|
||||
{
|
||||
version: "0.14.1",
|
||||
date: "2026-06-21",
|
||||
summary:
|
||||
"Internal cleanup: the project's original working name was fully retired in favour of Siftlode.",
|
||||
chores: [
|
||||
"Renamed the last remaining references to the project's original codename throughout the codebase, deployment config and database identity. No behavioural change. One side effect: locally remembered view preferences (theme, feed filters, dismissed hints) reset to their defaults once, because the browser-storage keys were renamed too.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "0.14.0",
|
||||
date: "2026-06-21",
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export const DEFAULT_LAYOUT: SidebarLayout = {
|
|||
hidden: {},
|
||||
};
|
||||
|
||||
const KEY = "subfeed.sidebarLayout";
|
||||
const KEY = "siftlode.sidebarLayout";
|
||||
|
||||
// Tolerate stale/partial data: keep only known widgets and append any that are missing
|
||||
// (e.g. a widget added in a later version) so nothing silently disappears.
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export function applyTheme(t: ThemePrefs): void {
|
|||
el.style.setProperty("--font-scale", String(t.fontScale));
|
||||
}
|
||||
|
||||
const KEY = "subfeed.theme";
|
||||
const KEY = "siftlode.theme";
|
||||
|
||||
export function loadLocalTheme(): ThemePrefs {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Dumps the Siftlode Postgres database to backupssiftlode-<timestamp>.dump (custom format).
|
||||
# Run from the project root: .\scripts\backup.ps1
|
||||
$ErrorActionPreference = "Stop"
|
||||
$user = if ($env:POSTGRES_USER) { $env:POSTGRES_USER } else { "subfeed" }
|
||||
$dbname = if ($env:POSTGRES_DB) { $env:POSTGRES_DB } else { "subfeed" }
|
||||
$user = if ($env:POSTGRES_USER) { $env:POSTGRES_USER } else { "siftlode" }
|
||||
$dbname = if ($env:POSTGRES_DB) { $env:POSTGRES_DB } else { "siftlode" }
|
||||
New-Item -ItemType Directory -Force -Path "backups" | Out-Null
|
||||
$ts = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$out = "backups\siftlode-$ts.dump"
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
# Dumps the Siftlode Postgres database to backups/siftlode-<timestamp>.dump (custom format).
|
||||
# Run from the project root: ./scripts/backup.sh
|
||||
set -e
|
||||
USER_NAME="${POSTGRES_USER:-subfeed}"
|
||||
DB_NAME="${POSTGRES_DB:-subfeed}"
|
||||
USER_NAME="${POSTGRES_USER:-siftlode}"
|
||||
DB_NAME="${POSTGRES_DB:-siftlode}"
|
||||
mkdir -p backups
|
||||
TS=$(date +%Y%m%d-%H%M%S)
|
||||
OUT="backups/siftlode-$TS.dump"
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
# Usage: .\scripts\restore.ps1 backupssiftlode-YYYYMMDD-HHMMSS.dump
|
||||
param([Parameter(Mandatory = $true)][string]$DumpFile)
|
||||
$ErrorActionPreference = "Stop"
|
||||
$user = if ($env:POSTGRES_USER) { $env:POSTGRES_USER } else { "subfeed" }
|
||||
$dbname = if ($env:POSTGRES_DB) { $env:POSTGRES_DB } else { "subfeed" }
|
||||
$user = if ($env:POSTGRES_USER) { $env:POSTGRES_USER } else { "siftlode" }
|
||||
$dbname = if ($env:POSTGRES_DB) { $env:POSTGRES_DB } else { "siftlode" }
|
||||
if (-not (Test-Path $DumpFile)) { throw "Dump file not found: $DumpFile" }
|
||||
Get-Content -Encoding Byte $DumpFile | docker compose exec -T db pg_restore -U $user -d $dbname --clean --if-exists
|
||||
Write-Output "Restored $DumpFile"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ set -e
|
|||
FILE="$1"
|
||||
if [ -z "$FILE" ]; then echo "Usage: restore.sh <dumpfile>"; exit 1; fi
|
||||
if [ ! -f "$FILE" ]; then echo "Dump file not found: $FILE"; exit 1; fi
|
||||
USER_NAME="${POSTGRES_USER:-subfeed}"
|
||||
DB_NAME="${POSTGRES_DB:-subfeed}"
|
||||
USER_NAME="${POSTGRES_USER:-siftlode}"
|
||||
DB_NAME="${POSTGRES_DB:-siftlode}"
|
||||
docker compose exec -T db pg_restore -U "$USER_NAME" -d "$DB_NAME" --clean --if-exists < "$FILE"
|
||||
echo "Restored $FILE"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue