Loading panel/dev/mockApi.ts +59 −4 Changes for panel/dev/mockApi.ts: 59 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -853,10 +853,65 @@ async function handleImageRoute(ctx: SessionContext): Promise<boolean> { } async function handleSubmissionRoute(ctx: SessionContext): Promise<boolean> { if (ctx.parts[2] !== "submissions") return false; const isAdminSubmissions = ctx.parts[2] === "submissions"; const isMeSubmissions = ctx.parts[2] === "me" && ctx.parts[3] === "submissions"; if (!isAdminSubmissions && !isMeSubmissions) return false; // GET /api/v1/me/submissions if (isMeSubmissions && is("GET", ctx) && ctx.parts.length === 4) { const userSubs = ctx.state.submissions.filter((s) => s.submitted_by === ctx.account.email); sendJSON(ctx.res, 200, { submissions: userSubs }); return true; } // POST /api/v1/me/submissions if (isMeSubmissions && is("POST", ctx) && ctx.parts.length === 4) { const body = await readJSON<{ display_name?: string }>(ctx.req); const displayName = body.display_name?.trim(); if (!displayName) { sendError(ctx.res, 400, "bad_request", "display_name is required"); return true; } const id = `sub-${Date.now()}`; const newSub: Submission = { id, submitted_by: ctx.account.email, display_name: displayName, context_ref: `minio/contexts/${id}/context.tar.gz`, status: "pending_review", created_at: new Date().toISOString(), }; ctx.state.submissions.unshift(newSub); sendJSON(ctx.res, 201, newSub); return true; } // POST /api/v1/me/submissions/{id}/context if (isMeSubmissions && is("POST", ctx) && ctx.parts[5] === "context" && ctx.parts.length === 6) { const id = ctx.parts[4]; const sub = ctx.state.submissions.find((s) => s.id === id); if (!sub) { sendError(ctx.res, 404, "not_found", "submission not found"); return true; } if (sub.submitted_by !== ctx.account.email) { sendError(ctx.res, 404, "not_found", "submission not found"); return true; } if (sub.status !== "pending_review") { sendError(ctx.res, 409, "already_reviewed", "submission has already been reviewed"); return true; } // Read the body stream to end so the socket is clean for await (const _ of ctx.req) { /* discard */ } sendJSON(ctx.res, 200, sub); return true; } // GET /api/v1/submissions if (is("GET", ctx) && ctx.parts.length === 3) { if (isAdminSubmissions && is("GET", ctx) && ctx.parts.length === 3) { if (ctx.account.role !== "admin") { sendError(ctx.res, 403, "forbidden", "admin account required"); return true; Loading @@ -866,7 +921,7 @@ async function handleSubmissionRoute(ctx: SessionContext): Promise<boolean> { } // POST /api/v1/submissions/{id}/approve if (is("POST", ctx) && ctx.parts[4] === "approve" && ctx.parts.length === 5) { if (isAdminSubmissions && is("POST", ctx) && ctx.parts[4] === "approve" && ctx.parts.length === 5) { if (ctx.account.role !== "admin") { sendError(ctx.res, 403, "forbidden", "admin account required"); return true; Loading Loading @@ -917,7 +972,7 @@ async function handleSubmissionRoute(ctx: SessionContext): Promise<boolean> { } // POST /api/v1/submissions/{id}/reject if (is("POST", ctx) && ctx.parts[4] === "reject" && ctx.parts.length === 5) { if (isAdminSubmissions && is("POST", ctx) && ctx.parts[4] === "reject" && ctx.parts.length === 5) { if (ctx.account.role !== "admin") { sendError(ctx.res, 403, "forbidden", "admin account required"); return true; Loading panel/src/App.tsx +2 −0 Changes for panel/src/App.tsx: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -15,6 +15,7 @@ import { Account } from "@/pages/Account"; import { ImageAdmin } from "@/pages/admin/ImageAdmin"; import { ImageBuildPage } from "@/pages/admin/ImageBuildPage"; import { SubmissionsPage } from "@/pages/admin/SubmissionsPage"; import { MySubmissionsPage } from "@/pages/MySubmissionsPage"; // Three UX surfaces over two Zero-Trust tiers (DESIGN-WEB-3SIDES): // / User-Side — app-tier, every authenticated principal Loading Loading @@ -45,6 +46,7 @@ export default function App() { <Route path="servers/:name" element={<ServerConsole />} /> <Route path="servers/:name/players" element={<ServerPlayers />} /> <Route path="servers/:name/backups" element={<ServerBackups />} /> <Route path="submissions" element={<MySubmissionsPage />} /> <Route path="account" element={<Account />} /> {/* Admin-Side — admin-tier (server & content ops). Loading panel/src/i18n/index.ts +4 −0 Changes for panel/src/i18n/index.ts: 4 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -11,6 +11,7 @@ import enOps from "./resources/en-US/ops.json"; import enErrors from "./resources/en-US/errors.json"; import enNavigation from "./resources/en-US/navigation.json"; import enBackups from "./resources/en-US/backups.json"; import enSubmissions from "./resources/en-US/submissions.json"; import zhCommon from "./resources/zh-CN/common.json"; import zhAuth from "./resources/zh-CN/auth.json"; import zhDashboard from "./resources/zh-CN/dashboard.json"; Loading @@ -21,6 +22,7 @@ import zhOps from "./resources/zh-CN/ops.json"; import zhErrors from "./resources/zh-CN/errors.json"; import zhNavigation from "./resources/zh-CN/navigation.json"; import zhBackups from "./resources/zh-CN/backups.json"; import zhSubmissions from "./resources/zh-CN/submissions.json"; i18next .use(LanguageDetector) Loading @@ -38,6 +40,7 @@ i18next errors: enErrors, navigation: enNavigation, backups: enBackups, submissions: enSubmissions, }, "zh-CN": { common: zhCommon, Loading @@ -50,6 +53,7 @@ i18next errors: zhErrors, navigation: zhNavigation, backups: zhBackups, submissions: zhSubmissions, }, }, fallbackLng: "en-US", Loading panel/src/i18n/resources/en-US/navigation.json +1 −0 Changes for panel/src/i18n/resources/en-US/navigation.json: 1 added line, 0 removed lines. Original line number Diff line number Diff line { "dashboard": "Dashboard", "my_servers": "Servers", "my_submissions": "My Submissions", "account": "Account", "admin_section": "Admin", "admin_images": "Images", Loading panel/src/i18n/resources/en-US/submissions.json 0 → 100644 +33 −0 Changes for panel/src/i18n/resources/en-US/submissions.json: 33 added lines, 0 removed lines. Original line number Diff line number Diff line { "title": "My Submissions", "subtitle": "Submit your custom modpacks for review and track their build status. Approved submissions will automatically trigger safe builds.", "submit_card_title": "Submit New Modpack", "submit_card_desc": "Provide a name and upload your build context gzip tarball (.tar.gz).", "display_name_label": "Display Name", "display_name_placeholder": "e.g., Pixelmon Adventure Pack", "file_label": "Build Context (.tar.gz)", "file_drag_hint": "Drag and drop a .tar.gz file here, or click to browse", "file_selected": "Selected file: {{name}} ({{size}})", "submit_btn": "Submit", "submitting_create": "Creating submission...", "submitting_upload": "Uploading build context...", "submit_success": "Modpack submitted successfully!", "list_card_title": "Submission History", "filter_all": "All Statuses", "status_pending_review": "Pending Review", "status_approved": "Approved", "status_rejected": "Rejected", "table_display_name": "Modpack Name", "table_status": "Status", "table_created_at": "Submitted At", "table_reviewed_by": "Reviewed By", "table_image_ref": "Image Reference", "table_reject_reason": "Reject Reason", "no_submissions_title": "No Submissions Found", "no_submissions_hint": "You haven't submitted any modpacks yet.", "search_placeholder": "Search submissions...", "error_file_type": "Please upload a valid .tar.gz file.", "error_file_size": "File exceeds the allowed size limit.", "error_name_required": "Display name is required.", "error_file_required": "Build context file is required." } Loading
panel/dev/mockApi.ts +59 −4 Changes for panel/dev/mockApi.ts: 59 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -853,10 +853,65 @@ async function handleImageRoute(ctx: SessionContext): Promise<boolean> { } async function handleSubmissionRoute(ctx: SessionContext): Promise<boolean> { if (ctx.parts[2] !== "submissions") return false; const isAdminSubmissions = ctx.parts[2] === "submissions"; const isMeSubmissions = ctx.parts[2] === "me" && ctx.parts[3] === "submissions"; if (!isAdminSubmissions && !isMeSubmissions) return false; // GET /api/v1/me/submissions if (isMeSubmissions && is("GET", ctx) && ctx.parts.length === 4) { const userSubs = ctx.state.submissions.filter((s) => s.submitted_by === ctx.account.email); sendJSON(ctx.res, 200, { submissions: userSubs }); return true; } // POST /api/v1/me/submissions if (isMeSubmissions && is("POST", ctx) && ctx.parts.length === 4) { const body = await readJSON<{ display_name?: string }>(ctx.req); const displayName = body.display_name?.trim(); if (!displayName) { sendError(ctx.res, 400, "bad_request", "display_name is required"); return true; } const id = `sub-${Date.now()}`; const newSub: Submission = { id, submitted_by: ctx.account.email, display_name: displayName, context_ref: `minio/contexts/${id}/context.tar.gz`, status: "pending_review", created_at: new Date().toISOString(), }; ctx.state.submissions.unshift(newSub); sendJSON(ctx.res, 201, newSub); return true; } // POST /api/v1/me/submissions/{id}/context if (isMeSubmissions && is("POST", ctx) && ctx.parts[5] === "context" && ctx.parts.length === 6) { const id = ctx.parts[4]; const sub = ctx.state.submissions.find((s) => s.id === id); if (!sub) { sendError(ctx.res, 404, "not_found", "submission not found"); return true; } if (sub.submitted_by !== ctx.account.email) { sendError(ctx.res, 404, "not_found", "submission not found"); return true; } if (sub.status !== "pending_review") { sendError(ctx.res, 409, "already_reviewed", "submission has already been reviewed"); return true; } // Read the body stream to end so the socket is clean for await (const _ of ctx.req) { /* discard */ } sendJSON(ctx.res, 200, sub); return true; } // GET /api/v1/submissions if (is("GET", ctx) && ctx.parts.length === 3) { if (isAdminSubmissions && is("GET", ctx) && ctx.parts.length === 3) { if (ctx.account.role !== "admin") { sendError(ctx.res, 403, "forbidden", "admin account required"); return true; Loading @@ -866,7 +921,7 @@ async function handleSubmissionRoute(ctx: SessionContext): Promise<boolean> { } // POST /api/v1/submissions/{id}/approve if (is("POST", ctx) && ctx.parts[4] === "approve" && ctx.parts.length === 5) { if (isAdminSubmissions && is("POST", ctx) && ctx.parts[4] === "approve" && ctx.parts.length === 5) { if (ctx.account.role !== "admin") { sendError(ctx.res, 403, "forbidden", "admin account required"); return true; Loading Loading @@ -917,7 +972,7 @@ async function handleSubmissionRoute(ctx: SessionContext): Promise<boolean> { } // POST /api/v1/submissions/{id}/reject if (is("POST", ctx) && ctx.parts[4] === "reject" && ctx.parts.length === 5) { if (isAdminSubmissions && is("POST", ctx) && ctx.parts[4] === "reject" && ctx.parts.length === 5) { if (ctx.account.role !== "admin") { sendError(ctx.res, 403, "forbidden", "admin account required"); return true; Loading
panel/src/App.tsx +2 −0 Changes for panel/src/App.tsx: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -15,6 +15,7 @@ import { Account } from "@/pages/Account"; import { ImageAdmin } from "@/pages/admin/ImageAdmin"; import { ImageBuildPage } from "@/pages/admin/ImageBuildPage"; import { SubmissionsPage } from "@/pages/admin/SubmissionsPage"; import { MySubmissionsPage } from "@/pages/MySubmissionsPage"; // Three UX surfaces over two Zero-Trust tiers (DESIGN-WEB-3SIDES): // / User-Side — app-tier, every authenticated principal Loading Loading @@ -45,6 +46,7 @@ export default function App() { <Route path="servers/:name" element={<ServerConsole />} /> <Route path="servers/:name/players" element={<ServerPlayers />} /> <Route path="servers/:name/backups" element={<ServerBackups />} /> <Route path="submissions" element={<MySubmissionsPage />} /> <Route path="account" element={<Account />} /> {/* Admin-Side — admin-tier (server & content ops). Loading
panel/src/i18n/index.ts +4 −0 Changes for panel/src/i18n/index.ts: 4 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -11,6 +11,7 @@ import enOps from "./resources/en-US/ops.json"; import enErrors from "./resources/en-US/errors.json"; import enNavigation from "./resources/en-US/navigation.json"; import enBackups from "./resources/en-US/backups.json"; import enSubmissions from "./resources/en-US/submissions.json"; import zhCommon from "./resources/zh-CN/common.json"; import zhAuth from "./resources/zh-CN/auth.json"; import zhDashboard from "./resources/zh-CN/dashboard.json"; Loading @@ -21,6 +22,7 @@ import zhOps from "./resources/zh-CN/ops.json"; import zhErrors from "./resources/zh-CN/errors.json"; import zhNavigation from "./resources/zh-CN/navigation.json"; import zhBackups from "./resources/zh-CN/backups.json"; import zhSubmissions from "./resources/zh-CN/submissions.json"; i18next .use(LanguageDetector) Loading @@ -38,6 +40,7 @@ i18next errors: enErrors, navigation: enNavigation, backups: enBackups, submissions: enSubmissions, }, "zh-CN": { common: zhCommon, Loading @@ -50,6 +53,7 @@ i18next errors: zhErrors, navigation: zhNavigation, backups: zhBackups, submissions: zhSubmissions, }, }, fallbackLng: "en-US", Loading
panel/src/i18n/resources/en-US/navigation.json +1 −0 Changes for panel/src/i18n/resources/en-US/navigation.json: 1 added line, 0 removed lines. Original line number Diff line number Diff line { "dashboard": "Dashboard", "my_servers": "Servers", "my_submissions": "My Submissions", "account": "Account", "admin_section": "Admin", "admin_images": "Images", Loading
panel/src/i18n/resources/en-US/submissions.json 0 → 100644 +33 −0 Changes for panel/src/i18n/resources/en-US/submissions.json: 33 added lines, 0 removed lines. Original line number Diff line number Diff line { "title": "My Submissions", "subtitle": "Submit your custom modpacks for review and track their build status. Approved submissions will automatically trigger safe builds.", "submit_card_title": "Submit New Modpack", "submit_card_desc": "Provide a name and upload your build context gzip tarball (.tar.gz).", "display_name_label": "Display Name", "display_name_placeholder": "e.g., Pixelmon Adventure Pack", "file_label": "Build Context (.tar.gz)", "file_drag_hint": "Drag and drop a .tar.gz file here, or click to browse", "file_selected": "Selected file: {{name}} ({{size}})", "submit_btn": "Submit", "submitting_create": "Creating submission...", "submitting_upload": "Uploading build context...", "submit_success": "Modpack submitted successfully!", "list_card_title": "Submission History", "filter_all": "All Statuses", "status_pending_review": "Pending Review", "status_approved": "Approved", "status_rejected": "Rejected", "table_display_name": "Modpack Name", "table_status": "Status", "table_created_at": "Submitted At", "table_reviewed_by": "Reviewed By", "table_image_ref": "Image Reference", "table_reject_reason": "Reject Reason", "no_submissions_title": "No Submissions Found", "no_submissions_hint": "You haven't submitted any modpacks yet.", "search_placeholder": "Search submissions...", "error_file_type": "Please upload a valid .tar.gz file.", "error_file_size": "File exceeds the allowed size limit.", "error_name_required": "Display name is required.", "error_file_required": "Build context file is required." }