Skip to content

Commit dd7dc67

Browse files
committed
FIX - backend review not loading
1 parent 74f1e89 commit dd7dc67

8 files changed

Lines changed: 131 additions & 36 deletions

File tree

.dockerignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
node_modules
2+
client/dist
3+
server/uploads
4+
.git
5+
.env
6+
.env.*
7+
!.env.example
8+
*.log
9+
.DS_Store
10+
docker-compose.yml
11+
backups

Dockerfile

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Production image for Coolify / Docker deployments.
2+
# Build stage installs devDependencies (Vite); runtime stage omits them.
3+
4+
FROM node:22-alpine AS build
5+
6+
WORKDIR /app
7+
8+
COPY package.json package-lock.json ./
9+
COPY client/package.json ./client/
10+
COPY server/package.json ./server/
11+
12+
RUN npm ci
13+
14+
COPY client ./client
15+
COPY server ./server
16+
COPY fonts ./fonts
17+
COPY web_assets ./web_assets
18+
19+
RUN npm run build
20+
21+
FROM node:22-alpine AS runtime
22+
23+
WORKDIR /app
24+
25+
ENV NODE_ENV=production
26+
ENV HOST=0.0.0.0
27+
ENV PORT=3000
28+
29+
COPY package.json package-lock.json ./
30+
COPY client/package.json ./client/
31+
COPY server/package.json ./server/
32+
33+
RUN npm ci --omit=dev
34+
35+
COPY server ./server
36+
COPY --from=build /app/client/dist ./client/dist
37+
38+
RUN mkdir -p server/uploads
39+
40+
EXPOSE 3000
41+
42+
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
43+
CMD node -e "fetch('http://127.0.0.1:' + (process.env.PORT || 3000) + '/api/health').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
44+
45+
CMD ["node", "server/index.js"]

client/src/components/AdminReviewPage.jsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useAuth } from "../auth/AuthContext.jsx";
22
import { useEffect, useMemo, useState } from "react";
33
import { JournalDescription } from "./JournalDescription.jsx";
4+
import { readJsonResponse } from "../utils/fetchJson.js";
45
import { resolveStackAssetUrl } from "../utils/mediaUrls.js";
56
import "./AdminReviewPage.css";
67

@@ -156,7 +157,7 @@ function AdminReviewIndex() {
156157
const response = await fetch(`/api/admin/review/projects?shipSort=${encodeURIComponent(shipSort)}`, {
157158
credentials: "include",
158159
});
159-
const data = await response.json();
160+
const data = await readJsonResponse(response);
160161
if (!response.ok) throw new Error(data.error || "Failed to load review projects.");
161162
setProjects(data.projects || []);
162163
setPendingProjects(data.pendingProjects || []);
@@ -298,7 +299,7 @@ function AdminReviewDetail({ projectId }) {
298299
setMessage("");
299300
try {
300301
const response = await fetch(`/api/admin/review/projects/${projectId}`, { credentials: "include" });
301-
const data = await response.json();
302+
const data = await readJsonResponse(response);
302303
if (!response.ok) throw new Error(data.error || "Failed to load project.");
303304
setProject(data.project);
304305
setJournalEntries(data.journalEntries || []);
@@ -321,7 +322,7 @@ function AdminReviewDetail({ projectId }) {
321322
headers: { "Content-Type": "application/json" },
322323
body: JSON.stringify({ fraudFlag: nextChecked }),
323324
});
324-
const data = await response.json();
325+
const data = await readJsonResponse(response);
325326
if (!response.ok) throw new Error(data.error || "Could not save fraud flag.");
326327
setProject((current) => ({
327328
...current,
@@ -377,7 +378,7 @@ function AdminReviewDetail({ projectId }) {
377378
headers: { "Content-Type": "application/json" },
378379
body: JSON.stringify(body),
379380
});
380-
const data = await response.json();
381+
const data = await readJsonResponse(response);
381382
if (!response.ok) throw new Error(data.error || "Failed to submit review.");
382383
setExceedModalOpen(false);
383384
setExceedAcknowledged(false);
@@ -425,7 +426,7 @@ function AdminReviewDetail({ projectId }) {
425426
method: "DELETE",
426427
credentials: "include",
427428
});
428-
const data = await response.json().catch(() => ({}));
429+
const data = await readJsonResponse(response);
429430
if (!response.ok) throw new Error(data.error || "Failed to delete project.");
430431
window.location.href = "/admin/review";
431432
} catch (err) {

client/src/utils/fetchJson.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export async function readJsonResponse(response) {
2+
const body = await response.text();
3+
if (!body) return {};
4+
5+
try {
6+
return JSON.parse(body);
7+
} catch {
8+
const gotHtml = body.trimStart().startsWith("<!");
9+
const hint = gotHtml
10+
? " The API returned HTML instead of JSON. Run `npm run dev:full` and make sure the Node server started (look for `Server http://...:3000` in the terminal). If you run a second copy of this repo, give it different PORT and VITE_DEV_PORT values so they do not fight over :3000 / :5173."
11+
: "";
12+
throw new Error(`Invalid server response (${response.status}).${hint}`);
13+
}
14+
}

client/vite.config.js

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,42 @@
1-
import { defineConfig } from "vite";
1+
import { defineConfig, loadEnv } from "vite";
22
import react from "@vitejs/plugin-react";
33
import path from "path";
4+
import { fileURLToPath } from "url";
45

5-
export default defineConfig({
6-
plugins: [react()],
7-
resolve: {
8-
alias: {
9-
"@assets": path.resolve(__dirname, "../web_assets"),
10-
"@fonts": path.resolve(__dirname, "../fonts"),
11-
},
12-
},
13-
server: {
14-
host: "127.0.0.1",
15-
port: 5173,
16-
proxy: {
17-
"/api": {
18-
target: "http://127.0.0.1:3000",
19-
changeOrigin: true,
20-
},
21-
"/auth": {
22-
target: "http://127.0.0.1:3000",
23-
changeOrigin: true,
6+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
7+
8+
export default defineConfig(({ mode }) => {
9+
const env = loadEnv(mode, path.resolve(__dirname, ".."), "");
10+
const apiPort = env.PORT || "3000";
11+
const devPort = Number(env.VITE_DEV_PORT || 5173);
12+
const apiTarget = `http://127.0.0.1:${apiPort}`;
13+
14+
return {
15+
plugins: [react()],
16+
resolve: {
17+
alias: {
18+
"@assets": path.resolve(__dirname, "../web_assets"),
19+
"@fonts": path.resolve(__dirname, "../fonts"),
2420
},
25-
"/uploads": {
26-
target: "http://127.0.0.1:3000",
27-
changeOrigin: true,
21+
},
22+
server: {
23+
host: "127.0.0.1",
24+
port: devPort,
25+
strictPort: true,
26+
proxy: {
27+
"/api": {
28+
target: apiTarget,
29+
changeOrigin: true,
30+
},
31+
"/auth": {
32+
target: apiTarget,
33+
changeOrigin: true,
34+
},
35+
"/uploads": {
36+
target: apiTarget,
37+
changeOrigin: true,
38+
},
2839
},
2940
},
30-
},
41+
};
3142
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"db:up": "docker compose up -d",
1212
"db:down": "docker compose down",
1313
"build": "npm run build --workspace=client",
14-
"start": "npm run start --workspace=server"
14+
"start": "NODE_ENV=production node server/index.js"
1515
},
1616
"devDependencies": {
1717
"concurrently": "^9.1.0"

server/index.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,10 @@ if (isProd) {
904904
const dist = path.join(__dirname, "../client/dist");
905905
app.use(express.static(dist));
906906
app.get("*", (req, res) => {
907+
if (req.path.startsWith("/api/")) {
908+
notFoundInProduction(req, res);
909+
return;
910+
}
907911
res.sendFile(path.join(dist, "index.html"));
908912
});
909913
}
@@ -922,12 +926,22 @@ async function startServer() {
922926
}
923927
}
924928

925-
app.listen(PORT, HOST, () => {
929+
const server = app.listen(PORT, HOST, () => {
926930
console.log(
927931
`Server http://${HOST}:${PORT} (${isProd ? "serving React build" : "API only - use Vite on :5173 for UI"})`
928932
);
929933
});
930934

935+
server.on("error", (error) => {
936+
if (error?.code === "EADDRINUSE") {
937+
console.error(
938+
`[server] Port ${PORT} is already in use. Stop the other project using it, or set PORT in .env (and VITE_DEV_PORT + APP_ORIGIN for the Vite client).`
939+
);
940+
process.exit(1);
941+
}
942+
throw error;
943+
});
944+
931945
startPeriodicAirtableSync();
932946
}
933947

server/projects.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -536,18 +536,17 @@ export async function listAdminReviewProjects({ shipSort = "oldest" } = {}) {
536536
ORDER BY projects.shipped_at ${direction} NULLS LAST, projects.updated_at ${direction}, projects.id ${direction}
537537
`;
538538

539-
const initial = await pool.query(reviewProjectsSql);
540-
const userIds = [...new Set(initial.rows.map((row) => Number(row.user_id)).filter(Number.isFinite))];
541-
await Promise.all(
539+
const result = await pool.query(reviewProjectsSql);
540+
const userIds = [...new Set(result.rows.map((row) => Number(row.user_id)).filter(Number.isFinite))];
541+
// Refresh Hackatime in the background so the review queue is not blocked (or timed out) in production.
542+
void Promise.all(
542543
userIds.map((userId) =>
543544
refreshProjectHackatimeHoursForUser(userId).catch((error) => {
544545
console.error("[review] hackatime refresh failed:", error?.message || error);
545546
})
546547
)
547548
);
548549

549-
const result = await pool.query(reviewProjectsSql);
550-
551550
const projects = result.rows.map(toAdminReviewProject);
552551
const pendingProjects = projects.filter(isPendingReviewProject);
553552
return {

0 commit comments

Comments
 (0)