added comments

This commit is contained in:
Altair-sh
2026-09-04 19:36:12 +02:00
parent 95880ca080
commit 0ecac6533c
17 changed files with 68 additions and 29 deletions
+4 -4
View File
@@ -38,8 +38,8 @@ export async function redirectRequest(
})
}
/// Browsers send OPTIONS request sometimes to check webserver allowed headers and methods.
/// My backend doesn't support such requests so use proxy to answer to it that all headers and methods are allowed
/// Browsers send OPTIONS request sometimes to check webserver's allowed headers and methods.
/// My C# backend doesn't support OPTIONS, so the proxy answer to OPTIONS request that all headers and methods are allowed.
export async function responseToOPTIONS(req: NextRequest): Promise<NextResponse> {
// console.log('proxy responding to CORS OPTIONS', req.url)
@@ -48,8 +48,8 @@ export async function responseToOPTIONS(req: NextRequest): Promise<NextResponse>
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, HEAD',
'Access-Control-Allow-Headers':
req.headers.get('access-control-request-headers') || 'Content-Type, Authorization',
'Access-Control-Allow-Headers': '*',
// req.headers.get('access-control-request-headers') || 'Content-Type, Authorization'
},
})
}
+2
View File
@@ -1,3 +1,5 @@
// Directories in src/api/proxy/ are routes.
// Their paths are stored here to simplify refactoring.
export const PROXY_PATHS = {
saveParserBackend: '/api/proxy/saveParserBackend',
}
@@ -4,10 +4,12 @@ import { redirectRequest, responseToOPTIONS as respondToOPTIONS } from '@/app/ap
import { PROXY_PATHS } from '@/app/api/proxy/paths'
import { appConfig } from '@/lib/configLoader'
// Forwards the incoming request to the save parser backend.
async function redirect(req: NextRequest): Promise<NextResponse> {
return await redirectRequest(req, PROXY_PATHS.saveParserBackend, appConfig.services.saveParserBackend.url)
}
// handlers for each request method on this route
export const GET = redirect
export const POST = redirect
export const PUT = redirect
+4 -3
View File
@@ -1,15 +1,16 @@
'use client'
import { AgGridReact } from 'ag-grid-react'
import { AllCommunityModule, themeQuartz } from 'ag-grid-community'
import { AllCommunityModule, themeQuartz, colorSchemeDark } from 'ag-grid-community'
import { useState, useEffect } from 'react'
import { colorSchemeDark } from 'ag-grid-community'
// TODO: create tabs with general info, tables, plots
export default function BrowsePage() {
// Page that shows a save file's parsed data in a grid.
export default function BrowseSaveDataPage() {
const [agTheme, setAgTheme] = useState(themeQuartz)
// Keep the grid theme in sync with the browser's light/dark color scheme.
useEffect(() => {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
setAgTheme(prefersDark ? themeQuartz.withPart(colorSchemeDark) : themeQuartz)
+2
View File
@@ -8,7 +8,9 @@ export const metadata: Metadata = {
title: 'Home',
}
// Root layout wrapping every page with the navbar and shared styles.
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
// Register the ag-grid community features
ModuleRegistry.registerModules([AllCommunityModule])
return (
<html lang="en">
+4
View File
@@ -4,6 +4,7 @@ import { useState } from 'react'
import { authService } from '@/services/AuthService'
import FormError from '@/components/FormError'
// Page with the login form.
export default function LoginPage() {
const [formData, setFormData] = useState({
email: '',
@@ -13,14 +14,17 @@ export default function LoginPage() {
const [error, setError] = useState<string | null>(null)
// Update the form data with new field value
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target
setFormData((prev) => ({
...prev,
// checkboxes store value in property "checked"
[name]: type === 'checkbox' ? checked : value,
}))
}
// Submit the form data to the auth service or show an error on failure.
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError(null)
+3
View File
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
import { authService } from '@/services/AuthService'
import FormError from '@/components/FormError'
// Page with the registration form.
export default function RegisterPage() {
const router = useRouter()
@@ -17,6 +18,7 @@ export default function RegisterPage() {
const [error, setError] = useState<string | null>(null)
// Update the matching form field by input name.
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
@@ -24,6 +26,7 @@ export default function RegisterPage() {
}))
}
// Validate the passwords match, register the user, then redirect to login.
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError(null)
+2 -1
View File
@@ -2,8 +2,9 @@ interface FormErrorProps {
message: string | null
}
// Shows a red alert box with an error message, or nothing if there is no message.
export default function FormError(props: FormErrorProps) {
if (!props.message || props.message === '') return null
if (!props.message) return null
return (
<div className="alert alert-danger" role="alert">
+1
View File
@@ -3,6 +3,7 @@
import Link from 'next/link'
import { font_Hack } from '@/lib/myFonts'
// Top navigation bar with a home link and login/register buttons.
export default function Navbar() {
return (
<nav className={`navbar navbar-expand-lg navbar-dark bg-dark px-3 ${font_Hack.className}`}>
@@ -12,6 +12,7 @@ export enum GameEnum {
//TODO: move to separate page
//TODO: add redirect to ?id={id} to see status of long parsing save
// Dialog for picking a game and save file, uploading it, and polling the parse status until done.
export default function SaveFileUploadingDialog() {
const [error, setError] = useState<string | null>(null)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
@@ -20,16 +21,19 @@ export default function SaveFileUploadingDialog() {
const [saveStatus, setSaveStatus] = useState<SaveStatusResponse | null>(null)
const [uploadResult, setUploadResult] = useState<UploadSaveResponse | null>(null)
// Update the selected game from the dropdown.
const handleGameSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
setGame(e.target.value as GameEnum)
}
// Store the file chosen in the file picker.
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setSelectedFile(e.target.files[0])
}
}
// Uploads the selected save file, then repeatedly polls its status until parsing is done or an error occurs.
const handleUpload = async (e: React.FormEvent) => {
e.preventDefault()
setError(null)
@@ -43,6 +47,7 @@ export default function SaveFileUploadingDialog() {
setUploadResult(uploadResponse)
const pollInterval = 1000 // ms
// Poll the backend every second for the parsing status until it reports "done".
const intervalId = setInterval(async () => {
try {
const statusResponse = await saveParserBackendService.getSaveStatus(uploadResponse.id)
@@ -66,6 +71,7 @@ export default function SaveFileUploadingDialog() {
}
}
// Build the <option> list from the GameEnum values.
const gameEnumOptions = Object.values(GameEnum).map((value) => (
<option key={value} value={value}>
{value}
+1
View File
@@ -1,4 +1,5 @@
'server'
// Default app configuration, values can be overrided in runtimeConfig.json
export const defaultConfig = {
services: {
saveParserBackend: {
+5 -2
View File
@@ -1,5 +1,8 @@
import '@/lib/customConsoleLog'
// imports for both server and client go here
export function register() {
// run once when the server starts.
export async function register() {
console.log('hello from instrumentation.ts!')
await import('@/lib/customConsoleLog') // replaces console.log
await import('@/lib/configLoader') // forces loadConfig() to run at server startup
}
+4
View File
@@ -5,14 +5,17 @@ import { AppConfig, defaultConfig } from '@/config/AppConfig'
const configFilePath = path.join(process.cwd(), 'runtimeConfig.json')
// Reads runtimeConfig.json (merging it over the defaults) and creates new config file if missing
function loadConfig(): AppConfig {
let config = defaultConfig
if (fs.existsSync(configFilePath)) {
console.log(`reading config file '${configFilePath}'`)
const raw = fs.readFileSync(configFilePath, 'utf-8')
const parsed = JSON.parse(raw)
config = { ...defaultConfig, ...parsed }
} else {
console.log(`creating default config file '${configFilePath}'`)
fs.mkdirSync(path.dirname(configFilePath), { recursive: true })
}
fs.writeFileSync(configFilePath, JSON.stringify(defaultConfig, null, 4))
@@ -20,4 +23,5 @@ function loadConfig(): AppConfig {
return config
}
// Run once at first file import
export const appConfig = loadConfig()
+7 -4
View File
@@ -1,13 +1,16 @@
let _custom_console_log_injected = false
if (!_custom_console_log_injected) {
_custom_console_log_injected = true
// Replaces console.log with a version that prefixes a timestamp
function injectConsoleLog() {
const originalLog = console.log
function customLog(message?: any, ...optionalParams: any[]) {
const now = new Date()
const timestamp = now.toTimeString().split(' ')[0] // hh:mm:ss
originalLog(`[${timestamp}] ${message}`, ...optionalParams, ':3')
originalLog(`[${timestamp}] ${message}`, ...optionalParams)
}
console.log = customLog
return null
}
// Run once at first file import
export const _inject_run_once = injectConsoleLog()
+3
View File
@@ -1,6 +1,8 @@
import localFont from 'next/font/local'
import { IBM_Plex_Sans } from 'next/font/google'
// Loads the Google-hosted IBM Plex Sans font.
// It is downloaded to build directory during build.
export const font_IbmPlexSans = IBM_Plex_Sans({
subsets: ['latin'],
weight: ['400', '500', '700'],
@@ -8,6 +10,7 @@ export const font_IbmPlexSans = IBM_Plex_Sans({
variable: '--font-ibm-plex-sans',
})
// Loads the local Hack font used by the navbar.
export const font_Hack = localFont({
src: [
{
+3
View File
@@ -3,11 +3,13 @@
class AuthService {
private API_BASE = '/api/auth'
// Hashes the password before sending it to the backend
private hashPassword(password: string) {
// TODO: password hashing
return password
}
// Logs in with email and password, returning the parsed JSON response.
async login(email: string, password: string) {
const response = await fetch(`${this.API_BASE}/login`, {
method: 'POST',
@@ -25,6 +27,7 @@ class AuthService {
return await response.json()
}
// Registers a new user, returning the parsed JSON response.
async register(name: string, email: string, password: string) {
const response = await fetch(`${this.API_BASE}/register`, {
method: 'POST',
+15 -15
View File
@@ -11,18 +11,20 @@ export interface SaveStatusResponse {
uploadDateTime: string
}
// Tries to read the response body as string, returning an empty string if that fails.
async function getBodyTextOrEmpty(res: Response): Promise<string> {
try {
return await res.text()
} catch {
return ''
}
}
export class SaveParserBackendService {
private API_BASE = PROXY_PATHS.saveParserBackend
private async tryGetResponseJson(res: Response): Promise<string> {
try {
const body = await res.json()
return JSON.stringify(body)
} catch {}
return ''
}
//TODO: save size limit 30 mb
// Uploads a save file for the given game and returns its assigned id.
async uploadSave(game: string, file: File): Promise<UploadSaveResponse> {
const url = `${this.API_BASE}/uploadSave?game=${encodeURIComponent(game)}`
const response = await fetch(url, {
@@ -34,33 +36,31 @@ export class SaveParserBackendService {
})
if (!response.ok) {
throw new Error(`Failed to upload save: ${response.statusText} ${await this.tryGetResponseJson(response)}`)
throw new Error(`Failed to upload save: ${response.statusText} ` + (await getBodyTextOrEmpty(response)))
}
return await response.json()
}
// Fetches the current parsing status of a previously uploaded save.
async getSaveStatus(id: string): Promise<SaveStatusResponse> {
const url = `${this.API_BASE}/getSaveStatus?id=${encodeURIComponent(id)}`
const response = await fetch(url)
if (!response.ok) {
throw new Error(
`Failed to get save status: ${response.statusText} ${await this.tryGetResponseJson(response)}`
)
throw new Error(`Failed to get save status: ${response.statusText} ` + (await getBodyTextOrEmpty(response)))
}
return await response.json()
}
// Fetches the fully parsed data for a save once its parsing is done.
async getParsedSave(id: string): Promise<Record<string, any>> {
const url = `${this.API_BASE}/getParsedSave?id=${encodeURIComponent(id)}`
const response = await fetch(url)
if (!response.ok) {
throw new Error(
`Failed to get parsed save: ${response.statusText} ${await this.tryGetResponseJson(response)}`
)
throw new Error(`Failed to get parsed save: ${response.statusText} ` + (await getBodyTextOrEmpty(response)))
}
return await response.json()