diff --git a/groups/RODA/index.html b/groups/RODA/index.html index 71a8e09..5e38f45 100644 --- a/groups/RODA/index.html +++ b/groups/RODA/index.html @@ -76,12 +76,7 @@ window.APP_CONFIG = window.APP_CONFIG || { debugLogs: false, dashboardRuntimeFetch: 'always', - dashboardTargetUrl: 'https://dashboard.informatika.untan.ac.id/index.php?penelitian', - dashboardProxyTemplates: [ - 'https://api.codetabs.com/v1/proxy?quest={{url}}', - 'https://api.allorigins.win/get?url={{url}}&ts={{ts}}', - 'https://thingproxy.freeboard.io/fetch/{{rawUrl}}' - ] + dashboardApiUrl: 'https://proxykbk.netlify.app/.netlify/functions/dashboard', }; @@ -1661,57 +1656,33 @@ } let lastError = null; + + if (isDashboardRuntimeFetchAllowed()) { + const dashboardApiUrl = window.APP_CONFIG?.dashboardApiUrl?.trim(); + + if (dashboardApiUrl) { + try { + const rows = await fetchProxyRows(dashboardApiUrl); + applyDashboardRows(rows); + return; + } catch (error) { + lastError = error; + } + } + } + + // Fallback ke data yang di-embed saat build jika runtime fetch gagal atau dinonaktifkan const embeddedHtml = readEmbeddedDashboardHtml(); if (embeddedHtml) { try { applyDashboardRows(parseDashboardRows(embeddedHtml)); return; } catch (error) { - lastError = error; + lastError = lastError || error; } } - if (!isDashboardRuntimeFetchAllowed()) { - renderRisetFeedback('Pemuatan Data Gagal', getDashboardErrorMessage(lastError || new Error('DASHBOARD_RUNTIME_FETCH_DISABLED')), 'Coba Lagi', 'fetchDashboardData()'); - return; - } - - const targetUrl = window.APP_CONFIG?.dashboardTargetUrl?.trim(); - const proxyTemplates = Array.isArray(window.APP_CONFIG?.dashboardProxyTemplates) - ? window.APP_CONFIG.dashboardProxyTemplates.filter(Boolean) - : []; - - // Endpoint server.js hanya ada saat development lokal. - const proxyServers = shouldUseLocalDashboardProxy() ? [() => '/api/dashboard'] : []; - - // Deployment static memakai proxy publik karena tidak menjalankan server.js. - if (targetUrl) { - proxyTemplates.forEach((template) => { - proxyServers.push((url) => template - .replaceAll('{{url}}', encodeURIComponent(url)) - .replaceAll('{{rawUrl}}', url) - .replaceAll('{{ts}}', String(Date.now()))); - }); - } - - if (!proxyServers.length) { - renderRisetFeedback('Pemuatan Data Gagal', getDashboardErrorMessage(new Error('DASHBOARD_PROXY_NOT_CONFIGURED')), 'Coba Lagi', 'fetchDashboardData()'); - return; - } - - try { - const proxyUrls = proxyServers.map((getProxyUrl) => getProxyUrl(targetUrl)); - const rows = await Promise.any(proxyUrls.map((proxyUrl) => fetchProxyRows(proxyUrl))); - applyDashboardRows(rows); - return; - } catch (error) { - lastError = error instanceof AggregateError && error.errors?.length - ? error.errors[error.errors.length - 1] - : error; - } - - // Jika semua proxy gagal - renderRisetFeedback('Pemuatan Data Gagal', getDashboardErrorMessage(lastError), 'Coba Lagi', 'fetchDashboardData()'); + renderRisetFeedback('Pemuatan Data Gagal', getDashboardErrorMessage(lastError || new Error('DASHBOARD_RUNTIME_FETCH_DISABLED')), 'Coba Lagi', 'fetchDashboardData()'); } diff --git a/groups/RODA/package-lock.json b/groups/RODA/package-lock.json deleted file mode 100644 index 2155203..0000000 --- a/groups/RODA/package-lock.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "roda", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "roda", - "version": "1.0.0", - "license": "ISC" - } - } -} diff --git a/groups/RODA/package.json b/groups/RODA/package.json index b0606c6..5bbefff 100644 --- a/groups/RODA/package.json +++ b/groups/RODA/package.json @@ -1,14 +1,3 @@ { - "name": "roda", - "version": "1.0.0", - "description": "Project website untuk menampilkan sorotan dan prestasi karya mahasiswa program studi Informatika Universitas Tanjungpura.", - "main": "server.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node server.js", - "dev": "node server.js" - }, - "keywords": [], - "author": "", - "license": "ISC" -} \ No newline at end of file + "type": "commonjs" +} diff --git a/groups/RODA/server.js b/groups/RODA/server.js index 42dd119..73dfe38 100644 --- a/groups/RODA/server.js +++ b/groups/RODA/server.js @@ -6,64 +6,21 @@ const { URL } = require('url'); const ROOT_DIR = __dirname; const PARENT_DIR = path.join(__dirname, '..', '..'); const INDEX_PATH = path.join(ROOT_DIR, 'index.html'); -const ENV_PATH = path.join(ROOT_DIR, '.env'); -const HOST = '127.0.0.1'; +const HOST = process.env.HOST || '127.0.0.1'; const PORT = Number(process.env.PORT || 3000); +const DASHBOARD_TARGET_URL = 'https://dashboard.informatika.untan.ac.id/index.php?penelitian'; +const RODA_PATH_PREFIX = '/groups/RODA'; function logServerError(scope, error, extra = {}) { console.error(`[${scope}]`, error && error.stack ? error.stack : error, extra); } -function parseEnvFile(filePath) { - if (!fs.existsSync(filePath)) { - return {}; - } - - const env = {}; - const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/); - - for (const rawLine of lines) { - const line = rawLine.trim(); - if (!line || line.startsWith('#')) { - continue; - } - - const separatorIndex = line.indexOf('='); - if (separatorIndex === -1) { - continue; - } - - const key = line.slice(0, separatorIndex).trim(); - let value = line.slice(separatorIndex + 1).trim(); - - if ( - (value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")) - ) { - value = value.slice(1, -1); - } - - env[key] = value; - } - - return env; -} - function getAppConfig() { - const env = { - ...parseEnvFile(ENV_PATH), - ...process.env - }; - - const dashboardProxyTemplates = [ - env.DASHBOARD_PROXY_1, - env.DASHBOARD_PROXY_2, - env.DASHBOARD_PROXY_3 - ].filter(Boolean); - return { - dashboardTargetUrl: env.DASHBOARD_TARGET_URL || '', - dashboardProxyTemplates + debugLogs: false, + dashboardRuntimeFetch: 'always', + dashboardApiUrl: 'api/dashboard', + dashboardTargetUrl: DASHBOARD_TARGET_URL, }; } @@ -71,10 +28,6 @@ const DASHBOARD_CACHE_TTL_MS = 5 * 60 * 1000; let dashboardCache = { html: null, timestamp: 0 }; async function fetchDashboardHtml() { - const targetUrl = getAppConfig().dashboardTargetUrl; - if (!targetUrl) { - throw new Error('DASHBOARD_TARGET_URL kosong'); - } const now = Date.now(); if (dashboardCache.html && (now - dashboardCache.timestamp) < DASHBOARD_CACHE_TTL_MS) { @@ -84,7 +37,7 @@ async function fetchDashboardHtml() { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 12000); try { - const response = await fetch(targetUrl, { + const response = await fetch(DASHBOARD_TARGET_URL, { signal: controller.signal, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36' @@ -106,39 +59,14 @@ async function fetchDashboardHtml() { } } -function validateAppConfig(config) { - const errors = []; - - if (!config.dashboardTargetUrl) { - errors.push('DASHBOARD_TARGET_URL belum diisi.'); - } - - if (!Array.isArray(config.dashboardProxyTemplates) || !config.dashboardProxyTemplates.length) { - errors.push('Minimal satu DASHBOARD_PROXY_* harus diisi.'); - } - - return errors; -} - function injectConfig(html) { const configScript = ``; - const defaultConfigScript = ''; - - if (html.includes(defaultConfigScript)) { - return html.replace(defaultConfigScript, configScript); - } - return html.replace('', ` ${configScript}\n`); } function sendHtml(res) { try { const html = fs.readFileSync(INDEX_PATH, 'utf8'); - const configErrors = validateAppConfig(getAppConfig()); - if (configErrors.length) { - logServerError('config', new Error('Invalid app config'), { configErrors }); - } - const withConfig = injectConfig(html); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(withConfig); @@ -195,28 +123,53 @@ function sendNotFound(res) { res.end('Not found'); } +function getRodaStaticPathname(pathname) { + if (pathname === RODA_PATH_PREFIX || pathname === `${RODA_PATH_PREFIX}/` || pathname === `${RODA_PATH_PREFIX}/index.html`) { + return '/index.html'; + } + + if (pathname.startsWith(`${RODA_PATH_PREFIX}/`)) { + return pathname.slice(RODA_PATH_PREFIX.length); + } + + return pathname; +} + const server = http.createServer((req, res) => { try { const requestUrl = new URL(req.url, `http://${req.headers.host}`); const pathname = requestUrl.pathname; - if (pathname === '/' || pathname === '/index.html') { + if (pathname === '/' || pathname === '/index.html' || pathname === RODA_PATH_PREFIX || pathname === `${RODA_PATH_PREFIX}/` || pathname === `${RODA_PATH_PREFIX}/index.html`) { sendHtml(res); return; } - if (pathname === '/api/dashboard') { + if (pathname === '/api/dashboard' || pathname === `${RODA_PATH_PREFIX}/api/dashboard`) { + if (req.method === 'OPTIONS') { + res.writeHead(204, { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET', + }); + res.end(); + return; + } + fetchDashboardHtml() .then(({ html, cached }) => { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', - 'X-Cache': cached + 'Access-Control-Allow-Origin': '*', + 'X-Cache': cached, }); res.end(html); }) .catch((error) => { logServerError('api-dashboard', error); - res.writeHead(502, { 'Content-Type': 'text/plain; charset=utf-8' }); + res.writeHead(502, { + 'Content-Type': 'text/plain; charset=utf-8', + 'Access-Control-Allow-Origin': '*', + }); res.end('DASHBOARD_FETCH_FAILED'); }); return; @@ -237,7 +190,7 @@ const server = http.createServer((req, res) => { } // Serve static files from ROOT_DIR - const filepath = path.join(ROOT_DIR, pathname); + const filepath = path.join(ROOT_DIR, getRodaStaticPathname(pathname)); const normalizedPath = path.normalize(filepath); // Security check: ensure the requested file is within ROOT_DIR diff --git a/package.json b/package.json index b0c5938..4c0e3a0 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,9 @@ "type": "module", "description": "Student-safe static page assignment kit for Informatika UNTAN", "scripts": { - "dev": "vite", + "dev": "node scripts/dev.js", + "dev:vite": "vite", + "dev:roda": "node groups/RODA/server.js", "build": "vite build", "test:tapops": "node --test groups/Tapops/tests/*.test.mjs", "preview": "vite preview" diff --git a/scripts/dev-runner.js b/scripts/dev-runner.js new file mode 100644 index 0000000..4a85906 --- /dev/null +++ b/scripts/dev-runner.js @@ -0,0 +1,94 @@ +import { spawn } from 'node:child_process'; + +export function createDevProcessSpecs({ + nodeCommand = process.execPath, + env = process.env, +} = {}) { + return { + roda: { + command: nodeCommand, + args: ['groups/RODA/server.js'], + options: { + env: { + ...env, + HOST: env.RODA_HOST || '127.0.0.1', + PORT: env.RODA_PORT || '3000', + }, + stdio: ['ignore', 'ignore', 'pipe'], + }, + }, + vite: { + command: nodeCommand, + args: ['node_modules/vite/bin/vite.js'], + options: { + env: { ...env }, + stdio: 'inherit', + }, + }, + }; +} + +export function startDevServers({ + spawnProcess = spawn, + specs = createDevProcessSpecs(), +} = {}) { + const roda = spawnProcess(specs.roda.command, specs.roda.args, specs.roda.options); + const vite = spawnProcess(specs.vite.command, specs.vite.args, specs.vite.options); + let shuttingDown = false; + let rodaErrorOutput = ''; + + roda.stderr?.on('data', (chunk) => { + const message = String(chunk); + rodaErrorOutput += message; + if (!message.includes('EADDRINUSE')) { + process.stderr.write(`[roda] ${message}`); + } + }); + + function stopChildren() { + shuttingDown = true; + if (!roda.killed) { + roda.kill(); + } + if (!vite.killed) { + vite.kill(); + } + } + + vite.on('exit', (code, signal) => { + shuttingDown = true; + if (!roda.killed) { + roda.kill(); + } + + if (signal) { + process.exitCode = 1; + return; + } + + process.exitCode = code ?? 0; + }); + + roda.on('exit', (code, signal) => { + if (shuttingDown) { + return; + } + + if (rodaErrorOutput.includes('EADDRINUSE')) { + process.stderr.write('[roda] Port 3000 is already in use; continuing with the existing RODA server.\n'); + return; + } + + const reason = signal ? `signal ${signal}` : `exit code ${code}`; + process.stderr.write(`[roda] server stopped unexpectedly (${reason})\n`); + if (!vite.killed) { + vite.kill(); + } + process.exitCode = code || 1; + }); + + process.once('SIGINT', stopChildren); + process.once('SIGTERM', stopChildren); + + return { roda, vite }; +} diff --git a/scripts/dev.js b/scripts/dev.js new file mode 100644 index 0000000..73df19f --- /dev/null +++ b/scripts/dev.js @@ -0,0 +1,3 @@ +import { startDevServers } from './dev-runner.js'; + +startDevServers(); diff --git a/vite.config.js b/vite.config.js index f076183..941ca2a 100644 --- a/vite.config.js +++ b/vite.config.js @@ -10,9 +10,11 @@ const rodaDashboardCacheTtlMs = 5 * 60 * 1000; const dashboardRequestHeaders = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36', }; +const rodaHost = process.env.RODA_HOST || '127.0.0.1'; +const rodaPort = process.env.RODA_PORT || '3000'; +const rodaTarget = `http://${rodaHost}:${rodaPort}`; let rodaDashboardHtmlCache; -let rodaDashboardDevCache = { html: '', timestamp: 0 }; function collectHtmlInputs(dir) { const inputs = {}; @@ -124,61 +126,20 @@ function embedRodaDashboardData() { }; } -function serveRodaDashboardApi() { - return { - name: 'serve-roda-dashboard-api', - configureServer(server) { - server.middlewares.use(async (req, res, next) => { - const requestUrl = new URL(req.url || '/', 'http://localhost'); - if (requestUrl.pathname !== '/api/dashboard') { - next(); - return; - } - - if (req.method !== 'GET' && req.method !== 'HEAD') { - res.writeHead(405, { 'Content-Type': 'text/plain; charset=utf-8' }); - res.end('METHOD_NOT_ALLOWED'); - return; - } - - const now = Date.now(); - try { - let html = rodaDashboardDevCache.html; - let cacheStatus = 'HIT'; - - if (!html || (now - rodaDashboardDevCache.timestamp) >= rodaDashboardCacheTtlMs) { - html = await requestRodaDashboardHtml(); - rodaDashboardDevCache = { html, timestamp: Date.now() }; - cacheStatus = 'MISS'; - } - - res.writeHead(200, { - 'Content-Type': 'text/html; charset=utf-8', - 'X-Cache': cacheStatus, - }); - res.end(req.method === 'HEAD' ? '' : html); - } catch (error) { - if (rodaDashboardDevCache.html) { - res.writeHead(200, { - 'Content-Type': 'text/html; charset=utf-8', - 'X-Cache': 'STALE', - }); - res.end(req.method === 'HEAD' ? '' : rodaDashboardDevCache.html); - return; - } - - const message = error instanceof Error ? error.message : String(error); - console.warn(`[roda-dashboard] Dev dashboard fetch failed: ${message}`); - res.writeHead(502, { 'Content-Type': 'text/plain; charset=utf-8' }); - res.end('DASHBOARD_FETCH_FAILED'); - } - }); - }, - }; -} - export default defineConfig({ - plugins: [serveRodaDashboardApi(), embedRodaDashboardData(), copyStaticAssets()], + plugins: [embedRodaDashboardData(), copyStaticAssets()], + server: { + proxy: { + '/groups/RODA/api/dashboard': { + target: rodaTarget, + changeOrigin: true, + }, + '/api/dashboard': { + target: rodaTarget, + changeOrigin: true, + }, + }, + }, build: { rollupOptions: { input: collectHtmlInputs(root),