Route Prefix: /admin Access: Cloudflare Access (prod), token (dev)


Core flows

Dashboard

  • Quick links to Events, Results, Import, Settings
  • At-a-glance: next events, top floors, last import

Events management

  • Search + filter by day/sport
  • Create/edit: title, sport, description, location, start/end, capacity, form_url, source_label
  • Links: “View public page” + “Open Google Form”

Results entry

  • Pick event → enter floor rows
  • Medal buttons auto-apply placement + points
  • Confirmation before save
  • Undo last save

CSV import

  • Upload Google Forms CSV export
  • Preview first rows before commit
  • Store raw submissions + import run stats
  • Optional delta mode: /api/admin/import/csv?delta=1 only inserts rows newer than the latest stored response timestamp
  • Cloudflare Access service tokens can be used for automated imports (Apps Script)
  • Imports overwrite older submissions for the same room (latest wins).

Automated import (Apps Script)

const API_URL = "https://api.games.ihnyc-rc.org/api/admin/import/csv?delta=1";
const CF_ACCESS_CLIENT_ID = "YOUR_CF_ACCESS_CLIENT_ID";
const CF_ACCESS_CLIENT_SECRET = "YOUR_CF_ACCESS_CLIENT_SECRET";
const SHEET_ID = "YOUR_SHEET_ID";
const SHEET_NAME = "Form Responses 1";
 
function uploadResponses() {
  const sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(SHEET_NAME);
  const values = sheet.getDataRange().getValues();
 
  const csv = values
    .map(row =>
      row.map(cell => {
        const s = String(cell ?? "");
        if (s.includes('"') || s.includes(",") || s.includes("\n")) {
          return `"${s.replace(/"/g, '""')}"`;
        }
        return s;
      }).join(",")
    )
    .join("\n");
 
  const res = UrlFetchApp.fetch(API_URL, {
    method: "post",
    contentType: "text/csv",
    payload: csv,
    headers: {
      "CF-Access-Client-Id": CF_ACCESS_CLIENT_ID,
      "CF-Access-Client-Secret": CF_ACCESS_CLIENT_SECRET,
    },
    muteHttpExceptions: true,
  });
 
  Logger.log(res.getResponseCode());
  Logger.log(res.getContentText());
}
 
function createFiveMinuteTrigger() {
  ScriptApp.newTrigger("uploadResponses")
    .timeBased()
    .everyMinutes(5)
    .create();
}

Submissions review

  • Search by name/email/room
  • Delete invalid rows

Admin protection

  • Protect /admin* with Cloudflare Access (email allowlist).
  • Protect /api/admin* with Access and/or X-Admin-Token.