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
1 change: 1 addition & 0 deletions .github/workflows/audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:

jobs:
audit:
if: false
runs-on: ubuntu-latest
timeout-minutes: 10
name: npm audit
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ concurrency:

jobs:
validate:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
if: false
runs-on: ubuntu-latest
timeout-minutes: 15
name: Lint and test
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/cleanup-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:

jobs:
close_pull_request_job:
if: false
runs-on: ubuntu-latest
timeout-minutes: 5
name: Close staging environment
Expand Down
3 changes: 2 additions & 1 deletion app/api/recsCacheCleanup.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ async function deleteRows(supabaseUrl, serviceKey, filter) {
return match ? parseInt(match[1], 10) : 0;
}

app.timer('recsCacheCleanup', {
// HIBERNATION: Timer disabled 2026-09-04 — project paused
if (false) app.timer('recsCacheCleanup', {
schedule: '0 3 * * 0',
handler: async (_myTimer, context) => {
const supabaseUrl = process.env.SUPABASE_URL;
Expand Down
56 changes: 23 additions & 33 deletions app/api/sportySync.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,15 +149,20 @@ async function syncGymCalendar(context, { shiftDays = 0, daysBack = 0 } = {}) {
return { ok: true, upserted: rows.length };
}

// ── No timer trigger ──────────────────────────────────────────────────
// Azure Static Web Apps managed functions support HTTP triggers ONLY — timer
// (cron) triggers are silently ignored and never register. The scheduled sync
// is therefore driven externally by a GitHub Actions cron workflow
// (.github/workflows/sporty-sync.yml) that POSTs to /api/sporty-sync at
// 04:00, 11:00, 14:00 and 22:00 UTC with {"daysBack": 7}.
// ── Timer trigger: DISABLED for hibernation ──────────────────────
// Original: 22:00, 04:00, 11:00, and 14:00 UTC daily
// 22:00 UTC = midnight Oslo (CEST/UTC+2) — captures next day's sessions while
// Sporty still returns them as "tomorrow".
// Docs: https://learn.microsoft.com/azure/static-web-apps/apis-functions#constraints
// Sporty still returns them as "tomorrow". Later runs keep the schedule fresh.
// Skipped locally — SWA CLI only supports HTTP triggers.
// HIBERNATION: Project paused 2026-09-04 — timer disabled
if (false && process.env.AZURE_FUNCTIONS_ENVIRONMENT === 'Production') {
app.timer('sportySyncTimer', {
schedule: '0 4,11,14,22 * * *',
handler: async (myTimer, context) => {
await syncGymCalendar(context, { daysBack: 7 });
},
});
}

// ── HTTP trigger: health check ────────────────────────────────────────
// GET /api/sporty-health → returns most-recent gym_calendar row + count
Expand Down Expand Up @@ -225,38 +230,23 @@ app.http('sportySyncHealth', {
},
});

// ── HTTP trigger: scheduled sync (cron) + manual kick + optional backfill ──
// ── HTTP trigger: manual kick + optional backfill ─────────────────────
// POST /api/sporty-sync → sync today
// POST /api/sporty-sync {"daysBack":7} → self-healing 7-day lookback (cron default)
// POST /api/sporty-sync {"shiftDays":-7} → duplicate current data 7 days back
//
// Two auth paths are accepted:
// 1. Automation (GitHub Actions cron): header X-Api-Key: <SPORTY_SYNC_API_KEY>
// SWA managed functions only run HTTP triggers — no timer trigger ever fires
// in production (see .github/workflows/sporty-sync.yml), so an external
// scheduler drives the sync via this endpoint.
// 2. Manual kick from a signed-in user: header X-Supabase-Token: <valid JWT>
// (Azure SWA hijacks the Authorization header — never use it for app JWTs)
// Requires header: X-Supabase-Token: <valid Supabase JWT>
// (Azure SWA hijacks the Authorization header — never use it for app JWTs)
app.http('sportySyncHttp', {
methods: ['POST'],
route: 'sporty-sync',
authLevel: 'anonymous',
handler: async (request, context) => {
const apiKey = request.headers.get('x-api-key');
const expectedKey = process.env.SPORTY_SYNC_API_KEY;
let authorized = Boolean(expectedKey && apiKey === expectedKey);

if (!authorized) {
const token = request.headers.get('x-supabase-token');
const userId = await verifySupabaseJwt(
token,
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
);
authorized = Boolean(userId);
}

if (!authorized) {
const token = request.headers.get('x-supabase-token');
const userId = await verifySupabaseJwt(
token,
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
);
if (!userId) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
Expand Down