Implementation guide
How the dev team connects the two trackers to the real backend.
Overview
Two static HTML trackers (Matchmaker, Ask ACG) run today against seed JSON plus browser storage . All data access is in assets/adapter.js (ACG.api) . Going live means: implement the REST contract below, store the three collections, and rewrite the adapter's function bodies to call it. No other front-end file changes.
REST contract
JSON over HTTP , base path /api. Collections return { "items": [...] } (threads also total). Errors: { "error": "..." } or { "errors": [...] } with the status codes listed .
| Method | Path | Purpose | Success |
|---|---|---|---|
GET | /api/config | Environments, time zones, statuses, issue tags, base URLs | 200 config object |
GET | /api/matchmaker/schema | Wizard questions and option lists | 200 schema object |
GET | /api/matchmaker/cases | All case definitions | 200 {items:[...]} |
GET | /api/matchmaker/cases/:id | One case | 200 / 404 |
POST | /api/matchmaker/cases | Create a case (server assigns id) | 201 saved case |
PUT | /api/matchmaker/cases/:id | Replace a case | 200 saved case |
DELETE | /api/matchmaker/cases/:id | Delete a case and its runs | 204 |
GET | /api/matchmaker/runs | All runs; optional ?case_id= | 200 list |
POST | /api/matchmaker/runs | Log a run | 201 saved run |
PUT | /api/matchmaker/runs/:id | Edit a run | 200 saved run |
DELETE | /api/matchmaker/runs/:id | Delete a run | 204 |
GET | /api/askacg/threads | Threads with pairs; filters below | 200 {items,total} |
GET | /api/askacg/threads/:id | One thread | 200 / 404 |
POST | /api/askacg/threads | Create a manual thread | 201 saved thread |
PUT | /api/askacg/threads/:id | Update; backend logs: annotations only | 200 / 422 |
DELETE | /api/askacg/threads/:id | Delete a manual thread | 204 / 409 for source backend |
Thread list filters (all optional): env (comma list), source, user_type, flagged, from/to (UTC instants), q, page, limit, min_pairs, max_pairs, plus status/verdict/rating on pairs.
Data schemas
IDs are server-generated. Instants are UTC ISO strings . Environment is a list on cases, a single value on runs and threads.
Matchmaker case
{
"id": "MM-0001",
"title": "Low budget, nationwide",
"environments": ["dev", "review"],
"status": "active",
"type": "regression",
"answers": { "budget": 20000, "gpa": 3.4, "sat": 1200, "distance": null,
"campus_setting": ["city"], "priorities": ["affordability"] },
"expected_count": 42,
"must_include": ["Example State University"],
"must_exclude": [],
"notes": "",
"created_at": "2026-10-31T18:12:00Z",
"updated_at": "2026-10-31T18:12:00Z"
}Matchmaker run
{
"id": "RUN-0001", "case_id": "MM-0001", "environment": "dev",
"ran_at": "2026-10-31T18:12:00Z",
"actual_count": 42,
"include_checks": { "Example State University": true },
"exclude_checks": {},
"result": "pass", // pass | fail | blocked | skipped
"overridden": false, "override_reason": "",
"tester": "", "build_version": "", "data_version": "", "notes": ""
}Ask ACG thread
{
"id": "TH-0001", "thread_uid": "c017d1f4-...", "source": "backend", // backend | manual
"environment": "live",
"created_at": "2026-10-31T18:12:00Z",
"user": { "type": "visitor", "email": "", "nickname": "", "session_id": "v-9f3a21..." },
"tester": "", "build_version": "", "data_version": "",
"flagged": false, "notes": "",
"pairs": [{
"id": "P-1", "ts": "2026-10-31T18:12:00Z",
"question": "What is Early Decision?", "answer": "...",
"expected_answer": "", "status": "showed_correct", "status_as_of": "2026-10-31",
"verdict": "pass", // pass | fail | null
"rating": 5, "issue_tags": [], "notes": "",
"status_history": [{ "at": "...", "from": "not_reviewed", "to": "showed_correct", "as_of": "2026-10-31", "by": "" }]
}]
}Pass rule, as implemented in the UI and worth repeating server-side: pass = actual count equals expected AND every include/exclude check ticked; a manual override needs a reason.
Storage (MongoDB)
Three collections; Ask ACG pairs are embedded because a thread is always read whole. Convert ISO strings to Date on write and back on read. Indexes :
// db.mm_cases one document per case (as above)
// db.mm_runs one per run; case_id references mm_cases.id
// db.aa_threads one per thread, pairs embedded (a thread is always read whole)
db.mm_cases.createIndex({ id: 1 }, { unique: true });
db.mm_runs.createIndex({ id: 1 }, { unique: true });
db.mm_runs.createIndex({ case_id: 1, ran_at: -1 });
db.aa_threads.createIndex({ id: 1 }, { unique: true });
db.aa_threads.createIndex({ thread_uid: 1 }, { unique: true });
db.aa_threads.createIndex({ created_at: -1 });
db.aa_threads.createIndex({ "user.email": 1 });
db.aa_threads.createIndex({ environment: 1, source: 1 });
db.aa_threads.createIndex({ question_text: "text", answer_text: "text" }); // optional keyword search
Server rules
- Backend-written threads are read-only except annotations; DELETE returns 409 .
- Validate everything again on the server , including "showed correct needs an as-of date".
- Deleting a case deletes its runs.
- Sign-in required on every route ; emails are personal data .
- Use same-origin hosting or add CORS .
Next.js handler examples
Route Handlers (docs) with the Node driver (docs). Sketches, not drop-in code: getDb, nextId, toIso, fromIso and validateThread are yours to write.
// app/api/askacg/threads/route.js (Next.js App Router Route Handler)
import { NextResponse } from "next/server";
import { getDb } from "@/lib/mongo"; // your existing connection helper
export async function GET(req) {
const p = new URL(req.url).searchParams;
const q = {};
if (p.get("env")) q.environment = { $in: p.get("env").split(",") };
if (p.get("source")) q.source = p.get("source");
if (p.get("user_type")) q["user.type"] = p.get("user_type");
if (p.get("flagged") === "true") q.flagged = true;
if (p.get("from") || p.get("to")) {
q.created_at = {};
if (p.get("from")) q.created_at.$gte = new Date(p.get("from"));
if (p.get("to")) q.created_at.$lt = new Date(p.get("to"));
}
const limit = Math.min(+p.get("limit") || 100, 500), skip = ((+p.get("page") || 1) - 1) * limit;
const db = await getDb();
const [items, total] = await Promise.all([
db.collection("aa_threads").find(q, { projection: { _id: 0 } }).sort({ created_at: -1 }).skip(skip).limit(limit).toArray(),
db.collection("aa_threads").countDocuments(q),
]);
return NextResponse.json({ items: items.map(toIso), total }); // toIso: Date -> ISO string
}
export async function POST(req) {
const body = await req.json();
const errors = validateThread(body); // see "Validate on the server"
if (errors.length) return NextResponse.json({ errors }, { status: 422 });
const db = await getDb();
const doc = { ...body, source: "manual", id: await nextId(db, "TH"), created_at: new Date(body.created_at) };
await db.collection("aa_threads").insertOne(doc);
return NextResponse.json(toIso(doc), { status: 201 });
}// app/api/askacg/threads/[id]/route.js
const ANNOTATION_FIELDS = ["tester","build_version","data_version","flagged","notes"];
const PAIR_ANNOTATIONS = ["expected_answer","status","status_as_of","verdict","rating","issue_tags","notes","status_history"];
export async function PUT(req, { params }) {
const { id } = await params; // params is a Promise in recent Next.js versions
const body = await req.json(), db = await getDb();
const cur = await db.collection("aa_threads").findOne({ id });
if (!cur) return NextResponse.json({}, { status: 404 });
let next = body;
if (cur.source === "backend") { // keep identity, Q, A and timestamps from the stored copy
next = { ...cur };
ANNOTATION_FIELDS.forEach(k => next[k] = body[k]);
next.pairs = cur.pairs.map(p => { const b = body.pairs.find(x => x.id === p.id) || {}; const o = { ...p }; PAIR_ANNOTATIONS.forEach(k => o[k] = b[k]); return o; });
}
const errors = validateThread(next);
if (errors.length) return NextResponse.json({ errors }, { status: 422 });
await db.collection("aa_threads").replaceOne({ id }, fromIso(next));
return NextResponse.json(next);
}
export async function DELETE(_req, { params }) {
const { id } = await params, db = await getDb();
const cur = await db.collection("aa_threads").findOne({ id });
if (!cur) return new Response(null, { status: 404 });
if (cur.source === "backend") return NextResponse.json({ error: "Backend logs cannot be deleted." }, { status: 409 });
await db.collection("aa_threads").deleteOne({ id });
return new Response(null, { status: 204 });
}
Adapter swap
Replace the body of assets/adapter.js with the following. Names and return shapes match the mock exactly .
// assets/adapter.js (after the swap; same function names, same return shapes)
(function () {
var ACG = window.ACG, API = "/api";
function call(method, path, body) {
return fetch(API + path, {
method: method, credentials: "same-origin",
headers: body ? { "Content-Type": "application/json" } : undefined,
body: body ? JSON.stringify(body) : undefined
}).then(function (r) {
if (r.status === 204) return true;
return r.json().catch(function () { return {}; }).then(function (j) {
if (!r.ok) throw new Error((j && (j.error || (j.errors || []).join("; "))) || ("HTTP " + r.status));
return j;
});
});
}
var api = ACG.api = { mode: "live" };
api.getConfig = function () { return call("GET", "/config"); };
api.matchmaker = {
getSchema: function () { return call("GET", "/matchmaker/schema"); },
listCases: function () { return call("GET", "/matchmaker/cases").then(function (r) { return r.items; }); },
getCase: function (id) { return call("GET", "/matchmaker/cases/" + id); },
saveCase: function (c) { return c.id ? call("PUT", "/matchmaker/cases/" + c.id, c) : call("POST", "/matchmaker/cases", c); },
deleteCase: function (id) { return call("DELETE", "/matchmaker/cases/" + id); },
listRuns: function (cid) { return call("GET", "/matchmaker/runs" + (cid ? "?case_id=" + encodeURIComponent(cid) : "")).then(function (r) { return r.items; }); },
saveRun: function (r) { return r.id ? call("PUT", "/matchmaker/runs/" + r.id, r) : call("POST", "/matchmaker/runs", r); },
deleteRun: function (id) { return call("DELETE", "/matchmaker/runs/" + id); }
};
api.askacg = {
listThreads: function () { return call("GET", "/askacg/threads?limit=500").then(function (r) { return r.items; }); },
getThread: function (id) { return call("GET", "/askacg/threads/" + id); },
saveThread: function (t) { return t.id ? call("PUT", "/askacg/threads/" + t.id, t) : call("POST", "/askacg/threads", t); },
deleteThread: function (id) { return call("DELETE", "/askacg/threads/" + id); }
};
// Mock-only helpers: keep harmless stubs so the pages do not break.
api.resetSeed = function () { return Promise.resolve(true); };
api.seedStatus = function () { return Promise.resolve({ stale: false, sample: false, notes: [] }); };
})();Then remove the "Reset to seed data" buttons or leave them as no-ops, and delete data/*.json except what you still want as documentation. data/config.json becomes GET /api/config. Export needs no server work.
Before you start
This guide takes you from the working mock to a live version, one step at a time. You do not need to change how the pages look.
What you have
matchmaker-tracker.htmlandaskacg-tracker.html: the two pages.assets/common.js,styles.css,matchmaker.js,askacg.js: the interface. You will not edit these.assets/adapter.js: the only file that decides where data comes from. This is the file you will rewrite.data/*.json: sample data the mock reads.
What you need
- A place to run server code that talks to your database. The examples use Next.js and MongoDB (inferred from the Ask ACG site; use your own stack if it differs) .
- About half a day for the API and one hour for the swap and checks.
How the pages work today
When a page needs data it calls something like ACG.api.askacg.listThreads(). In mock mode that function fetches data/askacg-threads.json once, copies it into the browser's storage and returns the copy; saves write back to the same copy. Nothing ever leaves the browser.
When you finish, the same call will send an HTTP request to your server instead. The page cannot tell the difference, because the function names and the shape of what they return stay identical.
Start the mock yourself first: double-click start.bat (Windows). It starts a small local server on port 8765 and opens the home page. Click around so you know what correct behaviour looks like.
Step 1: the API contract
An endpoint is a URL plus an HTTP method that does one thing, for example "list all threads". Build these .
| Method | Path | Purpose | Success |
|---|---|---|---|
GET | /api/config | Environments, time zones, statuses, issue tags, base URLs | 200 config object |
GET | /api/matchmaker/schema | Wizard questions and option lists | 200 schema object |
GET | /api/matchmaker/cases | All case definitions | 200 {items:[...]} |
GET | /api/matchmaker/cases/:id | One case | 200 / 404 |
POST | /api/matchmaker/cases | Create a case (server assigns id) | 201 saved case |
PUT | /api/matchmaker/cases/:id | Replace a case | 200 saved case |
DELETE | /api/matchmaker/cases/:id | Delete a case and its runs | 204 |
GET | /api/matchmaker/runs | All runs; optional ?case_id= | 200 list |
POST | /api/matchmaker/runs | Log a run | 201 saved run |
PUT | /api/matchmaker/runs/:id | Edit a run | 200 saved run |
DELETE | /api/matchmaker/runs/:id | Delete a run | 204 |
GET | /api/askacg/threads | Threads with pairs; filters below | 200 {items,total} |
GET | /api/askacg/threads/:id | One thread | 200 / 404 |
POST | /api/askacg/threads | Create a manual thread | 201 saved thread |
PUT | /api/askacg/threads/:id | Update; backend logs: annotations only | 200 / 422 |
DELETE | /api/askacg/threads/:id | Delete a manual thread | 204 / 409 for source backend |
Reading a table row: POST /api/matchmaker/cases means "send an HTTP POST to that address with a JSON body describing a new case; reply 201 with the saved case". The server, not the browser, chooses the id.
Replies use the status codes : 200/201 for success, 404 when the ID does not exist, 422 when the input is invalid, 409 when you try to delete a backend log.
Step 2: the data shapes
These are the exact objects the pages send and expect back. Keep field names unchanged: the pages, the exports and the sample files all use them. Times are UTC strings ending in Z . IDs like MM-0001 are made by the server.
A Matchmaker case (one test scenario)
{
"id": "MM-0001",
"title": "Low budget, nationwide",
"environments": ["dev", "review"],
"status": "active",
"type": "regression",
"answers": { "budget": 20000, "gpa": 3.4, "sat": 1200, "distance": null,
"campus_setting": ["city"], "priorities": ["affordability"] },
"expected_count": 42,
"must_include": ["Example State University"],
"must_exclude": [],
"notes": "",
"created_at": "2026-10-31T18:12:00Z",
"updated_at": "2026-10-31T18:12:00Z"
}
The answers object holds the wizard questions. The list of questions and their allowed values comes from GET /api/matchmaker/schema; you can serve data/matchmaker-schema.json as-is.
A Matchmaker run (one execution of a case)
{
"id": "RUN-0001", "case_id": "MM-0001", "environment": "dev",
"ran_at": "2026-10-31T18:12:00Z",
"actual_count": 42,
"include_checks": { "Example State University": true },
"exclude_checks": {},
"result": "pass", // pass | fail | blocked | skipped
"overridden": false, "override_reason": "",
"tester": "", "build_version": "", "data_version": "", "notes": ""
}
An Ask ACG thread
{
"id": "TH-0001", "thread_uid": "c017d1f4-...", "source": "backend", // backend | manual
"environment": "live",
"created_at": "2026-10-31T18:12:00Z",
"user": { "type": "visitor", "email": "", "nickname": "", "session_id": "v-9f3a21..." },
"tester": "", "build_version": "", "data_version": "",
"flagged": false, "notes": "",
"pairs": [{
"id": "P-1", "ts": "2026-10-31T18:12:00Z",
"question": "What is Early Decision?", "answer": "...",
"expected_answer": "", "status": "showed_correct", "status_as_of": "2026-10-31",
"verdict": "pass", // pass | fail | null
"rating": 5, "issue_tags": [], "notes": "",
"status_history": [{ "at": "...", "from": "not_reviewed", "to": "showed_correct", "as_of": "2026-10-31", "by": "" }]
}]
}
source says who wrote the thread. backend threads are created by your chat system when a real conversation happens (see Step 6). manual threads are typed in by testers using the "New thread" form.
Step 3: the database
Create three collections: mm_cases, mm_runs, aa_threads. Each Ask ACG pair lives inside its thread's pairs array, since you always want the whole conversation together. Convert time strings to real dates when saving and back to strings when returning.
Then create indexes so lists load fast as the log grows. Paste into the Mongo shell or run from a setup script:
// db.mm_cases one document per case (as above)
// db.mm_runs one per run; case_id references mm_cases.id
// db.aa_threads one per thread, pairs embedded (a thread is always read whole)
db.mm_cases.createIndex({ id: 1 }, { unique: true });
db.mm_runs.createIndex({ id: 1 }, { unique: true });
db.mm_runs.createIndex({ case_id: 1, ran_at: -1 });
db.aa_threads.createIndex({ id: 1 }, { unique: true });
db.aa_threads.createIndex({ thread_uid: 1 }, { unique: true });
db.aa_threads.createIndex({ created_at: -1 });
db.aa_threads.createIndex({ "user.email": 1 });
db.aa_threads.createIndex({ environment: 1, source: 1 });
db.aa_threads.createIndex({ question_text: "text", answer_text: "text" }); // optional keyword search
Step 4: write the routes
In Next.js App Router, a file named route.js inside app/api/... exports one function per HTTP method (Next.js docs). The two files below cover Ask ACG; Matchmaker follows the same pattern with simpler rules (no read-only records). They are sketches: write getDb, nextId, toIso, fromIso and validateThread to suit your project.
List and create: app/api/askacg/threads/route.js
// app/api/askacg/threads/route.js (Next.js App Router Route Handler)
import { NextResponse } from "next/server";
import { getDb } from "@/lib/mongo"; // your existing connection helper
export async function GET(req) {
const p = new URL(req.url).searchParams;
const q = {};
if (p.get("env")) q.environment = { $in: p.get("env").split(",") };
if (p.get("source")) q.source = p.get("source");
if (p.get("user_type")) q["user.type"] = p.get("user_type");
if (p.get("flagged") === "true") q.flagged = true;
if (p.get("from") || p.get("to")) {
q.created_at = {};
if (p.get("from")) q.created_at.$gte = new Date(p.get("from"));
if (p.get("to")) q.created_at.$lt = new Date(p.get("to"));
}
const limit = Math.min(+p.get("limit") || 100, 500), skip = ((+p.get("page") || 1) - 1) * limit;
const db = await getDb();
const [items, total] = await Promise.all([
db.collection("aa_threads").find(q, { projection: { _id: 0 } }).sort({ created_at: -1 }).skip(skip).limit(limit).toArray(),
db.collection("aa_threads").countDocuments(q),
]);
return NextResponse.json({ items: items.map(toIso), total }); // toIso: Date -> ISO string
}
export async function POST(req) {
const body = await req.json();
const errors = validateThread(body); // see "Validate on the server"
if (errors.length) return NextResponse.json({ errors }, { status: 422 });
const db = await getDb();
const doc = { ...body, source: "manual", id: await nextId(db, "TH"), created_at: new Date(body.created_at) };
await db.collection("aa_threads").insertOne(doc);
return NextResponse.json(toIso(doc), { status: 201 });
}
Update and delete: app/api/askacg/threads/[id]/route.js
// app/api/askacg/threads/[id]/route.js
const ANNOTATION_FIELDS = ["tester","build_version","data_version","flagged","notes"];
const PAIR_ANNOTATIONS = ["expected_answer","status","status_as_of","verdict","rating","issue_tags","notes","status_history"];
export async function PUT(req, { params }) {
const { id } = await params; // params is a Promise in recent Next.js versions
const body = await req.json(), db = await getDb();
const cur = await db.collection("aa_threads").findOne({ id });
if (!cur) return NextResponse.json({}, { status: 404 });
let next = body;
if (cur.source === "backend") { // keep identity, Q, A and timestamps from the stored copy
next = { ...cur };
ANNOTATION_FIELDS.forEach(k => next[k] = body[k]);
next.pairs = cur.pairs.map(p => { const b = body.pairs.find(x => x.id === p.id) || {}; const o = { ...p }; PAIR_ANNOTATIONS.forEach(k => o[k] = b[k]); return o; });
}
const errors = validateThread(next);
if (errors.length) return NextResponse.json({ errors }, { status: 422 });
await db.collection("aa_threads").replaceOne({ id }, fromIso(next));
return NextResponse.json(next);
}
export async function DELETE(_req, { params }) {
const { id } = await params, db = await getDb();
const cur = await db.collection("aa_threads").findOne({ id });
if (!cur) return new Response(null, { status: 404 });
if (cur.source === "backend") return NextResponse.json({ error: "Backend logs cannot be deleted." }, { status: 409 });
await db.collection("aa_threads").deleteOne({ id });
return new Response(null, { status: 204 });
}
source: "backend" threads, the update copies only the annotation fields from the request and keeps everything else from the stored thread. Never trust the browser to keep identity, questions, answers or timestamps unchanged .Validate every request : required fields present, values from the allowed lists, status_as_of present whenever status is showed_correct.
Step 5: swap the adapter
Open assets/adapter.js. Keep a copy of the original (rename it adapter.mock.js) so you can go back. Then replace the contents with the code below. Each function keeps its name and returns the same kind of value as before, using fetch (MDN) to call your API.
// assets/adapter.js (after the swap; same function names, same return shapes)
(function () {
var ACG = window.ACG, API = "/api";
function call(method, path, body) {
return fetch(API + path, {
method: method, credentials: "same-origin",
headers: body ? { "Content-Type": "application/json" } : undefined,
body: body ? JSON.stringify(body) : undefined
}).then(function (r) {
if (r.status === 204) return true;
return r.json().catch(function () { return {}; }).then(function (j) {
if (!r.ok) throw new Error((j && (j.error || (j.errors || []).join("; "))) || ("HTTP " + r.status));
return j;
});
});
}
var api = ACG.api = { mode: "live" };
api.getConfig = function () { return call("GET", "/config"); };
api.matchmaker = {
getSchema: function () { return call("GET", "/matchmaker/schema"); },
listCases: function () { return call("GET", "/matchmaker/cases").then(function (r) { return r.items; }); },
getCase: function (id) { return call("GET", "/matchmaker/cases/" + id); },
saveCase: function (c) { return c.id ? call("PUT", "/matchmaker/cases/" + c.id, c) : call("POST", "/matchmaker/cases", c); },
deleteCase: function (id) { return call("DELETE", "/matchmaker/cases/" + id); },
listRuns: function (cid) { return call("GET", "/matchmaker/runs" + (cid ? "?case_id=" + encodeURIComponent(cid) : "")).then(function (r) { return r.items; }); },
saveRun: function (r) { return r.id ? call("PUT", "/matchmaker/runs/" + r.id, r) : call("POST", "/matchmaker/runs", r); },
deleteRun: function (id) { return call("DELETE", "/matchmaker/runs/" + id); }
};
api.askacg = {
listThreads: function () { return call("GET", "/askacg/threads?limit=500").then(function (r) { return r.items; }); },
getThread: function (id) { return call("GET", "/askacg/threads/" + id); },
saveThread: function (t) { return t.id ? call("PUT", "/askacg/threads/" + t.id, t) : call("POST", "/askacg/threads", t); },
deleteThread: function (id) { return call("DELETE", "/askacg/threads/" + id); }
};
// Mock-only helpers: keep harmless stubs so the pages do not break.
api.resetSeed = function () { return Promise.resolve(true); };
api.seedStatus = function () { return Promise.resolve({ stale: false, sample: false, notes: [] }); };
})();
If the pages are served from a different address than the API, the browser will refuse the calls until the API sends CORS headers . Hosting both together avoids this.
Finally, load GET /api/config from your server instead of data/config.json (the adapter above already does). Set the base URLs for Dev, Review and Live in that config so "Open Ask ACG in ..." links appear.
Step 6: make the chat system write logs
Real Ask ACG conversations should appear in the tracker without anyone typing them in. At the end of each chat turn (or session), your chat code inserts or updates one document in aa_threads with source: "backend", the environment it ran in, the user (visitor with session ID, or registered with email), and the new question and answer appended to pairs. Give each new pair status: "not_reviewed" and empty annotations.
Use the thread's thread_uid (the chat session ID) to find the existing thread, so a long conversation stays one thread. Set environment from the deployment (dev, review, live). Do not store anything you do not need: registered emails are personal data .
Step 7: check it works
Work through this list with the browser developer tools open (Network tab):
- Home page, Matchmaker and Ask ACG all load with no red errors in the Console.
- Create a Matchmaker case, reload: it is still there. Delete it: it is gone, and so are its runs.
- Log a run and check the result matches the pass rule.
- Create a manual Ask ACG thread with two pairs; reload; both pairs present.
- On a backend thread, change a rating and status: saved. Try editing its question by sending a hand-made PUT: the server ignores it.
- Try DELETE on a backend thread: 409.
- Set a pair to "Showed correct results" with no date through a hand-made request: 422.
- Check times: a thread created at 9 PM Eastern shows the right India time, before and after a daylight saving change.
- Export CSV and JSON from both pages and open them.
- Anyone not signed in gets 401 from every
/apiroute.
If something goes wrong
- The pages show "Could not load the data files". A request failed. Open the Network tab, find the red request, read its status and response body.
- Saves seem to work but vanish. The old mock adapter is still loaded (browser cache). Hard-refresh, and check the adapter says
mode: "live". - Calls blocked, console mentions CORS. See Step 5.
- Times are off by hours. Something stored a local time. Store UTC only .
- Filters return nothing. You probably compare a
Dateto a string. Convert first. - To go back to the mock, restore
adapter.mock.jsasadapter.js.
Linked references open in a new tab. They point to the official documentation of each technology.