7 Commits

Author SHA1 Message Date
GuavaPopper e18641ff52 fix: run RODA with Vite dev 2026-06-05 17:55:51 +07:00
GuavaPopper f5dfb68bcc Use RODA server for dashboard proxy 2026-06-05 17:06:39 +07:00
GuavaPopper 1a90411509 Merge remote-tracking branch 'origin/feat/RODA' into feat/RODA 2026-06-05 14:43:20 +07:00
GuavaPopper 9755149741 Merge branch 'prod' into feat/RODA 2026-06-05 14:42:01 +07:00
Raditya_Indra_Putranto afe51be562 merge upstream 2026-06-05 07:37:36 +00:00
Raditya_Indra_Putranto 06fcc936e5 merge upstream 2026-06-05 06:31:03 +00:00
GuavaPopper 7957e6d50f Aktifkan fallback proxy paralel publikasi:failover 2026-06-05 13:30:05 +07:00
9 changed files with 147 additions and 94 deletions
+4
View File
@@ -0,0 +1,4 @@
DASHBOARD_TARGET_URL=https://dashboard.informatika.untan.ac.id/index.php?penelitian
DASHBOARD_PROXY_1=https://api.codetabs.com/v1/proxy?quest={{url}}
DASHBOARD_PROXY_2=https://api.allorigins.win/get?url={{url}}&ts={{ts}}
DASHBOARD_PROXY_3=https://thingproxy.freeboard.io/fetch/{{rawUrl}}
+3 -3
View File
@@ -76,6 +76,7 @@
window.APP_CONFIG = window.APP_CONFIG || {
debugLogs: false,
dashboardRuntimeFetch: 'always',
dashboardApiUrl: 'api/dashboard',
dashboardTargetUrl: 'https://dashboard.informatika.untan.ac.id/index.php?penelitian',
dashboardProxyTemplates: [
'https://api.codetabs.com/v1/proxy?quest={{url}}',
@@ -1677,14 +1678,13 @@
}
const targetUrl = window.APP_CONFIG?.dashboardTargetUrl?.trim();
const dashboardApiUrl = window.APP_CONFIG?.dashboardApiUrl?.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'] : [];
const proxyServers = dashboardApiUrl ? [() => dashboardApiUrl] : [];
// Deployment static memakai proxy publik karena tidak menjalankan server.js.
if (targetUrl) {
proxyTemplates.forEach((template) => {
proxyServers.push((url) => template
-13
View File
@@ -1,13 +0,0 @@
{
"name": "roda",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roda",
"version": "1.0.0",
"license": "ISC"
}
}
}
+1 -12
View File
@@ -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"
"type": "commonjs"
}
+22 -9
View File
@@ -7,8 +7,10 @@ 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 DEFAULT_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);
@@ -62,7 +64,10 @@ function getAppConfig() {
].filter(Boolean);
return {
dashboardTargetUrl: env.DASHBOARD_TARGET_URL || '',
debugLogs: env.DEBUG_LOGS === 'true',
dashboardRuntimeFetch: env.DASHBOARD_RUNTIME_FETCH || 'always',
dashboardApiUrl: env.DASHBOARD_API_URL || 'api/dashboard',
dashboardTargetUrl: env.DASHBOARD_TARGET_URL || DEFAULT_DASHBOARD_TARGET_URL,
dashboardProxyTemplates
};
}
@@ -113,10 +118,6 @@ function validateAppConfig(config) {
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;
}
@@ -195,17 +196,29 @@ 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`) {
fetchDashboardHtml()
.then(({ html, cached }) => {
res.writeHead(200, {
@@ -237,7 +250,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
+3 -1
View File
@@ -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"
+94
View File
@@ -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 };
}
+3
View File
@@ -0,0 +1,3 @@
import { startDevServers } from './dev-runner.js';
startDevServers();
+16 -55
View File
@@ -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),