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),