Skip to content
Closed
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
57 changes: 57 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Deploy

on:
workflow_dispatch:
schedule:
- cron: '17 3 * * *'
workflow_run:
workflows: [CI]
types: [completed]

concurrency:
group: production-deploy
cancel-in-progress: false

permissions:
contents: read

jobs:
deploy:
name: Deploy
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'schedule' && vars.ELO_ENABLED == 'true') ||
(github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'develop')
runs-on: ubuntu-latest
timeout-minutes: 15
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
steps:
- uses: actions/checkout@v7
with:
ref: develop
- uses: pnpm/action-setup@v6
with:
standalone: true
- uses: actions/setup-node@v7
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Apply Elo migrations
if: vars.ELO_ENABLED == 'true' && github.event_name != 'schedule'
run: pnpm exec wrangler d1 migrations apply profilarr-elo --remote -c worker/wrangler.jsonc
- name: Deploy Elo Worker
if: vars.ELO_ENABLED == 'true' && github.event_name != 'schedule'
run: pnpm exec wrangler deploy -c worker/wrangler.jsonc
- name: Compile Elo ratings
if: vars.ELO_ENABLED == 'true'
run: pnpm compile:elo -- --remote
- run: pnpm compile:api
- run: pnpm build
- name: Deploy site
run: pnpm exec wrangler deploy -c wrangler.jsonc
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ bun.lockb

# Miscellaneous
/static/
worker/worker-configuration.d.ts
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"compile:api": "tsx tooling/api/index.ts",
"compile:elo": "tsx tooling/elo/index.ts",
"worker:dev": "wrangler dev -c worker/wrangler.jsonc",
"worker:migrate": "wrangler d1 migrations apply dictionarry-clicks --local -c worker/wrangler.jsonc"
"worker:migrate": "wrangler d1 migrations apply profilarr-elo --local -c worker/wrangler.jsonc",
"worker:types": "wrangler types worker/worker-configuration.d.ts -c worker/wrangler.jsonc"
},
"devDependencies": {
"@cloudflare/workers-types": "^5.20260804.1",
Expand Down
6 changes: 3 additions & 3 deletions src/lib/client/search/clicks.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Click event recording for the Elo store (docs/backend/search.md). In dev
// the endpoint is the local worker (`pnpm worker:dev`); in production it is
// null until the Worker is deployed, making this a typed no-op. sendBeacon
// survives the navigation that immediately follows every click.
// the dedicated Elo Worker. sendBeacon survives the navigation that
// immediately follows every click.

const CLICK_ENDPOINT: string | null = import.meta.env.DEV
? 'http://localhost:8787/api/click'
: null;
: 'https://elo.profilarr.com/api/click';

export interface ClickEvent {
/** Raw query text; empty string for clicks from the popular view. */
Expand Down
2 changes: 1 addition & 1 deletion tooling/elo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = join(__dirname, '../..');
const outputDir = join(projectRoot, 'src/lib/data/elo');

const DATABASE = 'dictionarry-clicks';
const DATABASE = 'profilarr-elo';
const WRANGLER_CONFIG = 'worker/wrangler.jsonc';
const QUERY = 'SELECT query, clicked, shown, source FROM clicks ORDER BY ts';

Expand Down
36 changes: 24 additions & 12 deletions worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,6 @@
// happens here; ratings are derived at build time by compile:elo. Reads
// happen via `wrangler d1 execute`, so this stays a single endpoint.

export interface Env {
DB: D1Database;
ALLOWED_ORIGIN: string;
/** Optional secret; salts the IP hash. */
IP_SALT?: string;
}

const MAX_QUERY_LENGTH = 200;
const MAX_ROUTE_LENGTH = 300;
const MAX_SHOWN = 20;
Expand All @@ -20,14 +13,25 @@ interface ClickPayload {
shown: string[];
}

function corsHeaders(env: Env): Record<string, string> {
function corsHeaders(origin: string): Record<string, string> {
return {
'Access-Control-Allow-Origin': env.ALLOWED_ORIGIN,
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
'Access-Control-Allow-Headers': 'Content-Type',
Vary: 'Origin'
};
}

function allowedOrigin(request: Request, env: Env): string | null {
const origin = request.headers.get('Origin');
if (origin === env.ALLOWED_ORIGIN) return origin;

const hostname = new URL(request.url).hostname;
const isLocalWorker = hostname === 'localhost' || hostname === '127.0.0.1';
const isLocalSite = origin !== null && /^http:\/\/(localhost|127\.0\.0\.1):\d+$/.test(origin);
return isLocalWorker && isLocalSite ? origin : null;
}

function isRoute(value: unknown, maxLength: number): value is string {
return typeof value === 'string' && value.startsWith('/') && value.length <= maxLength;
}
Expand Down Expand Up @@ -56,7 +60,15 @@ async function hashIp(ip: string, salt: string): Promise<string> {

export default {
async fetch(request: Request, env: Env): Promise<Response> {
const headers = corsHeaders(env);
const origin = allowedOrigin(request, env);
if (!origin) {
return new Response('Forbidden', { status: 403 });
}
const headers = corsHeaders(origin);

if (!env.IP_SALT) {
return new Response('Service unavailable', { status: 503, headers });
}

if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers });
Expand All @@ -78,7 +90,7 @@ export default {
}

const ip = request.headers.get('CF-Connecting-IP') ?? 'unknown';
const ipHash = await hashIp(ip, env.IP_SALT ?? '');
const ipHash = await hashIp(ip, env.IP_SALT);

await env.DB.prepare(
'INSERT INTO clicks (query, clicked, shown, source, ip_hash, ts) VALUES (?, ?, ?, ?, ?, ?)'
Expand Down
5 changes: 2 additions & 3 deletions worker/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noEmit": true,
"types": ["@cloudflare/workers-types"]
"noEmit": true
},
"include": ["src"]
"include": ["src", "worker-configuration.d.ts"]
}
Loading
Loading