INFRAIMPACT Research Data Studio — Master Plan (27 deliverables)
INFRAIMPACT Research Data Studio — Master Plan (27 deliverables)
Status: v1.0 desain implementatif. Sebagian sudah ter-build di MVP 1a (lihat §23). Pemilik: Reza Prama Arviandi — Magister Teknik Sipil UI, Tesis INFRAIMPACT 2026. Target deploy: https://rezaprama.github.io/infraimpact/ (existing) + /admin.html (baru).
1. DIAGNOSIS ARSITEKTUR SAAT INI
- Topologi: GitHub Pages static site, no backend. JSON di
data/, GeoJSON digeo/. Semua komputasi di browser. - Limitasi keras:
- Tidak ada server → tidak ada auth nyata, tidak ada audit-trail server-side, tidak ada multi-user sync.
- File JSON di repo publik = data dapat di-download siapa saja yang mengetahui URL.
fetch()ke local file di-block olehfile://— wajib viahttp://atauhttps://.
- Konsekuensi metodologis:
- Login client-side = security theater, bukan keamanan. Mencegah klik tidak sengaja, tidak mencegah aktor jahat.
- Audit-trail di IndexedDB hilang jika browser di-wipe. Tidak dapat dipakai sebagai bukti forensik.
- Multi-enumerator workflow tidak feasible di local-first.
- Kekuatan yang dipertahankan: zero-ops, gratis, deploy via
git push, reproducible.
2. REKOMENDASI ARSITEKTUR
Bertahap, eksplisit:
| Tahap | Stack | Audiens | Status |
|---|---|---|---|
| MVP 1 (now) | GitHub Pages + IndexedDB + pseudo-login | Reza sendiri, demo akademik | Sebagian terbangun (MVP 1a) |
| MVP 2 | GitHub Pages + Supabase Postgres + Supabase Auth | Reza + 2–3 enumerator | Plan tersedia (§22) |
| MVP 3 | + Role-based RLS, supervisor validation, real-time sync | Pembimbing + tim riset | Plan tersedia |
| MVP 4 | Public release dengan anonimisasi otomatis + DOI Zenodo | Reviewer Q1 + publik | Plan tersedia |
Saya menolak Opsi C (Google Sheets/Airtable) untuk dataset penelitian: audit-trail lemah, skema rigid, dan API rate-limit akan menghambat saat data riil 571 aset + ratusan record QUAL masuk.
3. FILE/FOLDER YANG DITAMBAHKAN
infraimpact-dashboard/
├── admin.html # NEW — Research Data Studio shell
├── assets/
│ ├── css/
│ │ └── admin.css # NEW — studio styling
│ └── js/
│ └── admin/ # NEW folder
│ ├── studio-app.js # bootstrap, router, layout
│ ├── db.js # IndexedDB CRUD (13 stores)
│ ├── auth.js # pseudo-login + role gate
│ ├── audit.js # audit-log writer
│ ├── raw-viewer.js # asset table + filters + export
│ ├── calc-studio.js # MATLAB-style INFRAIMPACT walkthrough
│ ├── bwm-studio.js # BWM wizard + approximate solver
│ ├── forms.js # form engine (schema-driven)
│ ├── form-schemas.js # Interview/FGD/Obs/Doc schemas
│ ├── csv-io.js # PapaParse import/export
│ ├── pdf-export.js # jsPDF reports
│ ├── quality.js # data-quality dashboard
│ └── help.js # plain-language explainers
└── docs/
├── RESEARCH_DATA_STUDIO_PLAN.md # this file
└── SUPABASE_MIGRATION.md # MVP 2 plan
CDN yang ditambahkan: PapaParse, jsPDF, jspdf-autotable.
4. SKEMA DATA LENGKAP (IndexedDB / Supabase)
Database name: infraimpact_research_db, version: 1.
4.1 assets (primary: asset_id)
| Field | Type | Req | Validation | Privacy |
|---|---|---|---|---|
| asset_id | string PK | Y | ^AST-\d{4,}$ | public |
| asset_name | string | Y | 3–120 chars | public |
| asset_type | enum | Y | IPLT | IPAL_komunal | IPAL_kawasan | TPA | TPST | SPALDT | public |
| province_code | string(2) | Y | BPS | public |
| province_name | string | Y | — | public |
| district_code | string(4) | Y | BPS | public |
| district_name | string | Y | — | public |
| latitude | float | N | [-11, 6] | restricted |
| longitude | float | N | [95, 141] | restricted |
| construction_year | int | Y | [2000, current_year] | public |
| handover_year | int | N | ≥ construction_year | public |
| funding_source | enum | Y | APBN | APBD_I | APBD_II | Hibah | PHLN | Mixed | public |
| capex_value_idr | bigint | N | ≥ 0 | internal |
| design_capacity_m3_day | float | Y | > 0 | public |
| operator_entity | string | N | 0–200 | internal |
| data_quality_flag | enum | Y | complete | partial | estimated | missing | inconsistent | placeholder | confidential | public |
| data_source | string | Y | — | public |
| last_updated | date | Y | ISO | public |
| notes | string | N | 0–2000 | internal |
Index: province_code, district_code, asset_type, funding_source, data_quality_flag.
4.2 yearly_observations (primary: observation_id)
Already documented di docs/DATA_DICTIONARY.md. Added fields untuk MVP studio:
infraimpact_index(float, derived)usefulness_score,functionality_score,sustainability_scorerecommendation_ids(array of FK kerecommendations.recommendation_id)scoring_inputs(JSON object — raw indicator values used in calculation, for traceability)
Index: asset_id, year, province_code.
4.3 interviews (primary: interview_id)
| Field | Type | Req | Privacy | |
|---|---|---|---|---|
| interview_id | string PK | Y (INT-YYYY-NNNN) | — | |
| interview_date | date | Y | internal | |
| location | string | Y | internal | |
| province_code, district_code | FK | Y | public (denorm) | |
| asset_id | FK | N | public | |
| interviewer | string | Y | restricted | |
| respondent_code | string | Y | confidential (real name NEVER stored) | |
| respondent_role | enum | Y | UPTD | Dinas_PUPR | Bappeda | operator | masyarakat | konsultan | kontraktor | regulator_nasional | akademisi | NGO | internal |
| institution_type | enum | Y | central_govt | provincial | local | BUMD | KSM | NGO | private | academic | internal |
| consent_status | enum | Y | written_consent | verbal_consent | no_consent_recorded | confidential |
| anonymity_level | enum | Y | named (never used) | role_only | anonymized | fully_redacted | internal |
| interview_mode | enum | Y | in_person | video_call | phone | async_text | internal |
| duration_minutes | int | N | ≥ 0 | internal |
| transcript_summary | text | Y | 50–5000 chars | internal |
| full_transcript_optional | text | N | 0–100k chars | confidential (never publish) |
| key_quotes | array of {quote, timestamp_min, redacted} | N | — | internal |
| observed_problem | text | N | — | internal |
| cause_tags | array | N | enum (13 tags) | public |
| impact_tags | array | N | enum (10 tags) | public |
| evidence_strength | enum | Y | low | medium | high | public |
| linked_indicators | array | N | FK ke indicators | public |
| linked_documents | array | N | FK ke documents | public |
| follow_up_required | bool | N | — | internal |
| researcher_memo | text | N | — | internal |
| created_at, updated_at | datetime | Y | — | internal |
| created_by | string | Y | user session id / role | internal |
| coding_status | enum | Y | draft | coded | validated | rejected | internal |
Validasi keras: consent_status = 'no_consent_recorded' → field full_transcript_optional dipaksa kosong.
4.4 fgds (primary: fgd_id)
Schema sejajar interviews + tambahan:
participant_count(int)participant_groups(array of{group_label, count, role_summary}— tanpa nama personal)moderator,note_taker(internal)discussion_summary,consensus_points,disagreement_points(text)key_quotes(array denganrespondent_codeanonim)policy_recommendations(array)coding_status(sama enum)
4.5 field_observations (primary: observation_id — distinct dari yearly_observations)
Field per brief Anda (§E), plus:
checklist_responses(object: 10 boolean checklist items)evidence_photo_references(array of{filename, sha256, geomasked}— file BUKAN disimpan di IndexedDB; hanya referensi)gps_latitude_masked,gps_longitude_masked(float; jitter ±0.005°)data_quality_flag(enum)
4.6 documents (primary: document_id)
Schema per brief Anda (§F). Tambahan:
file_reference(string — path ke external storage, NOT binary in DB)sha256(string — hash file untuk versioning)verification_statusenum:unverified | verified_self | verified_cross | disputedprivacy_levelenum:public | internal | restricted | confidential
4.7 bwm_experts (primary: expert_id)
| Field | Type |
|---|---|
| expert_id | string PK |
| expert_code | string (anonymous label, mis. “PANEL-A-E01”) |
| affiliation_type | enum: academic, government, practitioner, NGO |
| years_experience | int |
| domain_expertise | array (institusi, regulasi, finansial, layanan, teknis) |
| consent_status | enum |
| created_at | datetime |
4.8 bwm_inputs (primary: input_id)
| Field | Type | Notes |
|---|---|---|
| input_id | string PK | |
| expert_id | FK | |
| block | enum: CAUSE | IMPACT | |
| level | enum: block | dimension | indicator | |
| best_criterion | string | id kriteria yang dipilih “terbaik” |
| worst_criterion | string | id kriteria yang dipilih “terburuk” |
| best_to_others | object | {criterion_id: score 1-9} |
| others_to_worst | object | {criterion_id: score 1-9} |
| created_at | datetime |
4.9 bwm_weights (primary: weight_id)
Output BWM solver. Sama struktur dengan data/weights.json saat ini + tambahan:
derived_from_input_ids(array of FK kebwm_inputs)consistency_ratio(float)xi_star(float — BWM optimality)solver_modeenum:exact_lp | approximate | aggregated_panelvalid_from,valid_to
4.10 indicators
Sama dengan data/indicators.json. Editable lewat Settings tab.
4.11 scores (primary: score_id)
Output Calculation Studio per (asset_id, year, scoring_method_version):
- All 5 derived scores (S_CAUSE, S_IMPACT, usefulness, functionality, sustainability)
diagnostic_quadrant,risk_zonerecommendation_idsscoring_inputs(full JSON trace untuk traceability)derived_from_observation_id,derived_from_weight_idcomputed_at,computed_by
4.12 audit_logs (primary: audit_id)
Per brief Anda (§N). Field tambahan:
before_snapshot_hash/after_snapshot_hash(untuk integrity check)session_id
4.13 settings (key-value)
app_version,scoring_method_version,current_user_role,current_panel_id,consent_to_local_storage,language, dst.
5–6. KOMPONEN UI + ADMIN STUDIO LAYOUT
admin.html
├── <header> — logo, breadcrumb, session badge, logout
├── <aside class="sidebar"> — 10 tabs:
│ ├── 🗄 Raw Asset Database
│ ├── 🎙 Interview Input
│ ├── 👥 FGD Input
│ ├── 🔍 Field Observation
│ ├── 📄 Document Evidence
│ ├── ⚖ BWM Weighting Studio
│ ├── 🧮 Calculation Studio
│ ├── ✅ Data Quality & Audit
│ ├── 📤 Export Center
│ └── ⚙ Settings & Method
├── <main id="studioContent"> — view container (router-controlled)
└── <footer> — version, disclaimer, link kembali ke public dashboard
Setiap tab adalah satu view-module yang mendaftar ke router via Studio.registerView(id, render).
7. RAW ASSET DATABASE VIEWER — Rancangan
Layout 3 baris:
[ Search ____________ ] [ Filters: prov ▾ kab ▾ jenis ▾ status ▾ kuadran ▾ zona ▾ quality ▾ ] [ Reset ]
[ Toolbar: 571 total · 571 visible · 178 IIF · 75 idle · 45 failed | Columns ▾ | Export CSV ▾ ]
┌─ Table (50 rows/page, virtual-scroll if >1k) ─────────────────────────────────────┐
│ asset_id ▼ │ name │ type │ prov │ kab │ year │ status │ S_C │ S_I │ quad │ flag │
└────────────────────────────────────────────────────────────────────────────────────┘
[ Pagination ]
Klik row → slide-in detail drawer (sama struktur dengan asset modal di dashboard publik) dengan tab Overview / Indicators / Trajectory / Recommendations / Quality / Raw Input.
Export buttons: visible rows, all filtered, all raw, processed scores, current row as PDF.
8–10. FORM RANCANGAN (Interview / FGD / Field Observation / Document)
Forms generated dari schema-driven engine di form-schemas.js. Struktur schema:
{
storeId: 'interviews',
prefix: 'INT',
sections: [
{ title: 'Metadata', fields: [
{ id: 'interview_date', label: 'Tanggal wawancara', type: 'date', required: true },
{ id: 'location', label: 'Lokasi', type: 'text', required: true,
helper: 'Nama desa/kelurahan/kota.' },
{ id: 'province_code', label: 'Provinsi', type: 'select-province', required: true },
// ...
]},
{ title: 'Responden (anonim)', fields: [
{ id: 'respondent_code', label: 'Kode responden', type: 'text', required: true,
helper: 'JANGAN tulis nama asli. Pakai kode R-001, R-002, dst.',
validate: v => !/(bapak|ibu|pak|bu|mas|mbak)\s/i.test(v) && !/\b[A-Z][a-z]+\s[A-Z][a-z]+\b/.test(v) || 'Hindari nama asli.'
},
// ...
]},
// ...
]
}
Field types didukung engine: text | textarea | number | date | datetime | select | select-multi | select-province | select-district | select-asset | select-indicator | tags | toggle | rating-1-3 | rating-1-9 | array-of-objects | file-reference.
Sentinel guardrails sebelum save:
- Consent gate: jika
consent_status = no_consent_recorded→full_transcript_optionaldikunci. - PII scanner: regex sederhana untuk warning jika field bebas mengandung pola nama Indonesia (Bapak/Ibu/Pak/Bu + Capitalized words).
- Coordinate masker: lat/long > 4 desimal → otomatis di-truncate untuk public_visible records.
11. BWM WEIGHTING STUDIO — Rancangan
9-step wizard (sama dengan brief §G):
Step 1 → pilih panel + expert
Step 2 → pilih blok CAUSE / IMPACT / dimension / indicator
Step 3 → pilih Best criterion
Step 4 → pilih Worst criterion
Step 5 → isi Best-to-Others scale (1-9)
Step 6 → isi Others-to-Worst scale (1-9)
Step 7 → klik "Calculate weights" → tampilkan bobot
Step 8 → tampilkan consistency ratio + warning kalau > 0.25
Step 9 → save → bobot masuk ke bwm_weights store
Solver mode:
- Approximate (MVP 1, default): rumus closed-form Rezaei (2015) approximate weights — tidak butuh LP solver:
w_B = 1 / (1 + Σ a_Bj/a_Bw untuk j ≠ B) w_W = w_B × a_Bw / a_BW (untuk W) w_j = w_B × ... (skala proporsional) Normalisasi: Σ w = 1 - Exact LP (MVP 2): integrasi dengan glpk.js atau javascript-lp-solver. Saya belum include di MVP 1a karena bundle size >300 KB; harus opt-in.
Educational sidebar:
- “Apa itu BWM?” — 3 paragraf bahasa awam.
- “Mengapa bukan AHP?” — bullet point komparasi (BWM butuh n-1 perbandingan, AHP n(n-1)/2).
- “Apa arti ξ*?” — penjelasan visual dengan thermometer 0–0.25–0.5+.
- Bar chart bobot final dengan tooltip hover.
- Sensitivity simulator: slider perturbasi bobot ±20% → live update sampel skor INFRAIMPACT.
Wajib peringatan top-bar:
“Bobot BWM final untuk tesis Q1 tidak boleh berasal dari input simulasi tanpa validasi panel pakar formal (Rezaei, 2015). Mode ini adalah training/demo.”
12. INFRAIMPACT CALCULATION STUDIO — Rancangan
9 step MATLAB-style, masing-masing dengan card terlihat:
[Step 1: Raw Input] → tabel indikator + raw value + sumber data
[Step 2: Data Cleaning] → flag missing/outlier; user toggle imputation
[Step 3: Missing Handling]→ exclude / impute-mean / impute-median
[Step 4: Normalization] → side-by-side raw vs normalized + orientation icon (↑ atau ↓)
[Step 5: BWM Weighting] → tabel weight per indikator (with hyperlink to bwm_weights record)
[Step 6: Block Score] → weighted sum dengan formula visible: S_d = Σ w_i x_i / Σ w_i
[Step 7: Quadrant] → 2x2 matrix dengan posisi asset di-highlight
[Step 8: Recommendation] → list rekomendasi terpicu + severity
[Step 9: Export Result] → save to scores store + export PDF asset report
Tiap step expandable: “Show formula” / “Show source code” toggle untuk transparansi.
Audit trail panel di kanan:
- Data source ID
- scoring_method_version
- weight_id
- last_updated
- computed_by
13. EXPORT CENTER — Rancangan
Single page, 13 export options sebagai card:
┌─ CSV Exports ──────────────────────────────┐
│ Raw assets (all) [Download] │
│ Filtered assets (current) [Download] │
│ Yearly observations [Download] │
│ Interview summaries [Download] │
│ FGD summaries [Download] │
│ Field observations [Download] │
│ BWM weights [Download] │
│ Processed scores [Download] │
│ Audit trail [Download] │
└────────────────────────────────────────────┘
┌─ PDF Reports ──────────────────────────────┐
│ Asset diagnostic (pick id) [Generate] │
│ Province diagnostic (pick) [Generate] │
│ Methodology appendix [Generate] │
│ Data dictionary [Generate] │
└────────────────────────────────────────────┘
┌─ Bulk ─────────────────────────────────────┐
│ Full research data package as ZIP │
│ (Browser-side via JSZip; large) │
└────────────────────────────────────────────┘
PDF Asset Report struktur:
- Cover: judul + asset_name + asset_id + tanggal.
- Metadata box: scoring_method_version, data_revision_number, cut_off_date.
- Identifikasi aset (lokasi, jenis, sumber dana, CapEx, kapasitas).
- 22 indikator → tabel + bar chart.
- Skor: S_CAUSE, S_IMPACT, kebermanfaatan, keberfungsian, keberlanjutan.
- Matriks 2×2 dengan posisi aset.
- Trajectory chart 2015–2024 (jika data tersedia).
- Top-3 indikator merah.
- Rekomendasi.
- Notes & limitations (auto-include disclaimer dari
metadata.json). - Footer: DOI Zenodo (jika ada), URL dashboard, page numbers.
14. LOGIN/ADMIN GATE — Rancangan
Mode 1 (MVP 1, implemented): Client-side passcode gate.
- Passcode
infraimpact-2026(configurable diassets/js/admin/auth.js). - Hash SHA-256 disimpan di-source. Bukan keamanan; mencegah klik tidak sengaja.
- Session token disimpan di
sessionStorage(cleared on tab close). - Banner permanen: “⚠ Pseudo-login — bukan keamanan sebenarnya. Jangan publish data sensitif ke repo publik.”
- Role default:
researcher(full studio access).
Mode 2 (MVP 2, designed only): Supabase Auth.
- Email magic-link.
- Role-based via Supabase
user_metadata.role. - RLS policy per table per role (lihat §22).
15. DATA QUALITY DASHBOARD — Rancangan
Metric cards (12 metrik per brief Anda §M) + tabel:
┌─ Coverage ──────────────────────────────────┐
│ Assets total: 571 │
│ With coordinates: 571 (100%) │
│ Without construction_year: 0 │
│ Without operational_status: 0 │
└──────────────────────────────────────────────┘
┌─ Qualitative coding progress ───────────────┐
│ Interviews coded: 0 / target N │
│ FGDs validated: 0 / target M │
│ Documents verified: 0 / target K │
└──────────────────────────────────────────────┘
┌─ Score coverage ────────────────────────────┐
│ Assets with cause_score: 571 │
│ Assets with all 3 derived scores: ___ │
│ Method versions in use: v1.0 │
└──────────────────────────────────────────────┘
┌─ Quality flag distribution ─────────────────┐
│ [bar chart] complete · partial · estimated · │
│ missing · inconsistent · placeholder │
└──────────────────────────────────────────────┘
Tabel: aset yang data_quality_flag != complete → daftar dengan link langsung ke editor record.
16. PSEUDOCODE — IndexedDB Wrapper
const DB_NAME = 'infraimpact_research_db';
const DB_VERSION = 1;
const STORES = {
assets: { keyPath: 'asset_id', indexes: ['province_code', 'district_code', 'asset_type'] },
yearly_observations: { keyPath: 'observation_id', indexes: ['asset_id', 'year'] },
interviews: { keyPath: 'interview_id', indexes: ['asset_id', 'coding_status'] },
fgds: { keyPath: 'fgd_id', indexes: ['asset_id'] },
field_observations: { keyPath: 'observation_id', indexes: ['asset_id'] },
documents: { keyPath: 'document_id', indexes: ['asset_id', 'document_type'] },
bwm_experts: { keyPath: 'expert_id' },
bwm_inputs: { keyPath: 'input_id', indexes: ['expert_id', 'block'] },
bwm_weights: { keyPath: 'weight_id', indexes: ['valid_from'] },
indicators: { keyPath: 'indicator_id' },
scores: { keyPath: 'score_id', indexes: ['asset_id', 'year'] },
audit_logs: { keyPath: 'audit_id', indexes: ['timestamp', 'table_name'] },
settings: { keyPath: 'key' }
};
async function openDB() {
return new Promise((res, rej) => {
const req = indexedDB.open(DB_NAME, DB_VERSION);
req.onerror = () => rej(req.error);
req.onsuccess = () => res(req.result);
req.onupgradeneeded = (e) => {
const db = e.target.result;
for (const [name, cfg] of Object.entries(STORES)) {
if (!db.objectStoreNames.contains(name)) {
const s = db.createObjectStore(name, { keyPath: cfg.keyPath });
(cfg.indexes || []).forEach(idx => s.createIndex(idx, idx, { unique: false }));
}
}
};
});
}
async function put(store, record) {
const db = await openDB();
const tx = db.transaction(store, 'readwrite');
tx.objectStore(store).put({ ...record, updated_at: new Date().toISOString() });
return new Promise((r,j) => { tx.oncomplete = r; tx.onerror = () => j(tx.error); });
}
async function getAll(store, filterFn) {
const db = await openDB();
const tx = db.transaction(store, 'readonly');
const req = tx.objectStore(store).getAll();
return new Promise((res,rej) => {
req.onsuccess = () => res(filterFn ? req.result.filter(filterFn) : req.result);
req.onerror = () => rej(req.error);
});
}
// Plus: get(store, key), del(store, key), count(store), query-by-index, bulk-put
17. PSEUDOCODE — CSV Import/Export
// IMPORT
function importCSV(file, schema) {
return new Promise((resolve, reject) => {
Papa.parse(file, {
header: true, skipEmptyLines: true, dynamicTyping: true,
complete: (res) => {
const errors = [];
const valid = [];
res.data.forEach((row, i) => {
const e = validateAgainstSchema(row, schema);
if (e.length === 0) valid.push(row);
else errors.push({ row: i+2, errors: e });
});
resolve({ valid, errors, total: res.data.length });
},
error: reject
});
});
}
// EXPORT
function exportCSV(records, filename) {
const csv = Papa.unparse(records, { quotes: true });
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
18. PSEUDOCODE — PDF Export
// Asset diagnostic 1-page report
function exportAssetPDF(asset, observation, indicators, weights, recommendations) {
const { jsPDF } = window.jspdf;
const doc = new jsPDF({ format: 'a4', unit: 'mm' });
// Header
doc.setFontSize(16).text('INFRAIMPACT Asset Diagnostic Report', 15, 20);
doc.setFontSize(10).text(`${asset.asset_name} · ${asset.asset_id}`, 15, 28);
// Block A: Identification (autoTable plugin)
doc.autoTable({
head: [['Field', 'Value']],
body: [
['Type', asset.asset_type],
['Location', `${asset.district_name}, ${asset.province_name}`],
['Construction year', asset.construction_year],
['Funding', asset.funding_source],
['Design capacity', `${asset.design_capacity_m3_day} m³/day`],
// ...
],
startY: 35
});
// Block B: Scores + 2×2 chart (rendered via html2canvas or manual draw)
// Block C: Top-3 red indicators
// Block D: Recommendations
// Footer disclaimer auto-appended
doc.save(`asset_${asset.asset_id}_report.pdf`);
}
19. PSEUDOCODE — BWM Solver (Approximate)
function solveBWMApproximate(input) {
const { best, worst, criteria, bestToOthers, othersToWorst } = input;
const a_BW = bestToOthers[worst]; // pivot
const weights = {};
// For each criterion, compute relative weight
let denom = 0;
for (const c of criteria) {
if (c === best) continue;
denom += bestToOthers[c] / a_BW;
}
// Best's weight
weights[best] = 1 / (1 + denom);
for (const c of criteria) {
if (c === best) continue;
weights[c] = weights[best] * bestToOthers[c] / a_BW;
}
// Normalize (should already sum to 1; defensive)
const sum = Object.values(weights).reduce((a,b) => a+b, 0);
for (const c of criteria) weights[c] /= sum;
// Consistency Index (Rezaei 2015 Table)
const CI_TABLE = { 1:0, 2:0.44, 3:1.00, 4:1.63, 5:2.30, 6:3.00, 7:3.73, 8:4.47, 9:5.23 };
const CR = computeConsistencyRatio(input, weights, CI_TABLE);
return { weights, consistency_ratio: CR, solver_mode: 'approximate' };
}
20. PSEUDOCODE — INFRAIMPACT Calculation
(Sudah ada di assets/js/scoring.js; di Calc Studio kita expose tiap step ke UI.)
function computeINFRAIMPACT(observation, indicators, weights, thresholds, opts) {
const trace = { steps: [], inputs: {}, intermediate: {} };
// Step 1: raw input
trace.inputs = observation;
// Step 2: cleaning (flag outliers, mark missing)
const cleaned = cleanObservation(observation, indicators);
trace.steps.push({ name: 'cleaning', output: cleaned });
// Step 3: missing handling per `opts.missingPolicy` (exclude | mean | median)
// Step 4: normalization
const normalized = {};
for (const ind of indicators) {
normalized[ind.indicator_id] = normalize(cleaned[ind.observation_field], ind);
}
trace.steps.push({ name: 'normalization', output: normalized });
// Step 5-6: dimension + block scores
const dims = computeDimensions(normalized, indicators, weights);
const sCause = blockScore(dims.CAUSE, weights.dimension_weights);
const sImpact = blockScore(dims.IMPACT, weights.dimension_weights);
trace.steps.push({ name: 'block_scores', sCause, sImpact });
// Step 7: classify
const quadrant = classifyQuadrant(sCause, sImpact, thresholds.quadrant_threshold);
// Step 8: risk zone (with absolute override)
const zone = riskZone(sCause, sImpact, observation, thresholds);
// Step 9: recommendations
const recs = recommendations.filter(r => r.trigger_quadrant === quadrant);
return {
cause_score: sCause, impact_score: sImpact,
diagnostic_quadrant: quadrant, risk_zone: zone,
recommendations: recs, trace
};
}
21. ACCEPTANCE CRITERIA (MVP 1a)
/admin.htmlaccessible athttps://rezaprama.github.io/infraimpact/admin.html.- Passcode gate works; wrong passcode rejected; session persists until tab close.
- IndexedDB created with all 13 stores on first load.
- Seed dataset (571 aset + 1442 observations + indicators + weights + recommendations) auto-imported on first run.
- Raw Asset Viewer renders 571 aset paginated; search + filters work; column show/hide; CSV export.
- Click asset → drawer opens with all metadata.
- Calculation Studio shows 9 steps for any asset+year; trace visible; PDF export works.
- BWM Studio wizard completes 9 steps; weights saved to
bwm_weights; consistency ratio displayed. - CSV import for assets works with validation + error log.
- Interview/FGD/Field Obs/Document forms render from schema; save to IndexedDB; appear in list view.
- Data Quality dashboard shows 12 metrics + quality flag distribution chart.
- Export Center delivers: 9 CSV exports + 4 PDF exports.
- Audit log captures every write with timestamp + session_id + before/after.
- Backup → file download; Restore → file upload, validated against schema.
- All disclaimers visible: “Pseudo-login — bukan keamanan sebenarnya”, “Data simulasi”, “Bobot belum tervalidasi”.
22. SECURITY/PRIVACY WARNINGS (eksplisit, ditampilkan di UI)
Top banner di setiap halaman admin:
⚠ Mode pseudo-login. Login client-side ini bukan keamanan kriptografis — siapa pun yang membuka source code dapat melihat passcode. Jangan publish data sensitif (nama responden, transkrip penuh, koordinat presisi, dokumen internal Kementerian) ke repo publik. Untuk dataset penelitian sebenarnya, migrasi ke Supabase (lihat
docs/SUPABASE_MIGRATION.md).
PII guardrails sebelum save:
- Field nama responden → ditolak jika regex mendeteksi nama Indonesia umum.
- Field transkrip penuh → dikunci jika consent_status ≠
written_consent. - Koordinat presisi > 4 desimal → otomatis di-truncate untuk record dengan
privacy_level = public.
Backup file unencrypted — peringatkan user untuk store di drive aman.
23. STEP-BY-STEP IMPLEMENTATION PLAN
MVP 1a — DELIVERED in this commit
admin.htmlshellassets/css/admin.cssassets/js/admin/db.js(IndexedDB)assets/js/admin/auth.js(pseudo-login)assets/js/admin/audit.jsassets/js/admin/raw-viewer.jsassets/js/admin/calc-studio.jsassets/js/admin/bwm-studio.jsassets/js/admin/forms.js(engine + 4 schemas)assets/js/admin/csv-io.jsassets/js/admin/pdf-export.jsassets/js/admin/quality.jsassets/js/admin/studio-app.js(router + bootstrap)- CDN: PapaParse, jsPDF, jspdf-autotable
- Link “Research Data Studio” dari public dashboard header
MVP 1b — Next iteration (1–2 minggu kerja)
- Full form schemas (saat ini 4 schema dasar; perluas dengan helper text & validators)
- BWM exact LP solver (glpk.js)
- Education page: “Bagaimana INFRAIMPACT Bekerja?”
- Sample CSV templates for download
- Backup/Restore round-trip
- ZIP bundle export (JSZip)
- Sensitivity simulator di BWM studio
- Traceability chain UI (klik skor → drill ke evidence)
MVP 2 — Supabase (target: Q3 2026)
- Supabase project setup
- SQL schema deploy
- Supabase Auth integration
- Role-based RLS policies
- Migration script: IndexedDB → Supabase
- Real-time sync UI indicator
MVP 3 — Multi-user research workflow
- Enumerator mobile-friendly form views
- Supervisor validation queue
- Inter-coder agreement metrics
- Coding kappa statistics
MVP 4 — Public release + DOI
- Automated anonymization pipeline
- Zenodo DOI integration
- Replication package script
- Reproducibility manifest
24. FILE-BY-FILE CHANGE LOG (MVP 1a)
| File | Action | Purpose |
|---|---|---|
admin.html | NEW | Studio shell, sidebar, login gate |
assets/css/admin.css | NEW | Studio styling |
assets/js/admin/studio-app.js | NEW | Bootstrap + router + layout |
assets/js/admin/db.js | NEW | IndexedDB wrapper |
assets/js/admin/auth.js | NEW | Pseudo-login + role |
assets/js/admin/audit.js | NEW | Audit-log writer |
assets/js/admin/raw-viewer.js | NEW | Asset table view |
assets/js/admin/calc-studio.js | NEW | INFRAIMPACT walkthrough |
assets/js/admin/bwm-studio.js | NEW | BWM wizard |
assets/js/admin/forms.js | NEW | Form engine + 4 view registrations |
assets/js/admin/form-schemas.js | NEW | Interview/FGD/Obs/Doc schemas |
assets/js/admin/csv-io.js | NEW | PapaParse import/export |
assets/js/admin/pdf-export.js | NEW | jsPDF reports |
assets/js/admin/quality.js | NEW | Data quality dashboard |
index.html | MODIFIED | Add link to admin studio in header |
docs/RESEARCH_DATA_STUDIO_PLAN.md | NEW | this file |
docs/SUPABASE_MIGRATION.md | NEW | MVP 2 migration plan |
25. TESTING CHECKLIST
- Open
/admin.html→ passcode gate appears. - Enter wrong passcode → rejected with shake animation.
- Enter
infraimpact-2026→ studio loads. - Sidebar 10 tabs all clickable.
- Raw Viewer: 571 rows visible across pagination.
- Filter by province “Jawa Timur” → row count updates.
- Search “Surabaya” → filter applied.
- Click an asset → drawer with metadata visible.
- Calc Studio: select asset AST-0001 + year 2024 → 9 steps populated.
- BWM Studio: complete wizard for CAUSE block → weights saved.
- Interview form: save dummy record → appears in Interview list.
- CSV export “Raw assets” → file
infraimpact_assets_*.csvdownloads. - PDF export: asset diagnostic → 1-page PDF generated.
- Data Quality dashboard: metric cards populated.
- Audit log: any save creates entry.
- Backup → file downloads. Wipe IndexedDB → Restore → data returns.
- Close tab → reopen → passcode required again (sessionStorage).
- Open in incognito → empty IndexedDB; seed data re-imported.
26. DEPLOYMENT CHECKLIST (GitHub Pages)
- Copy semua file baru ke
D:\github\rezaprama.github.io\infraimpact\. - Pastikan
assets/js/admin/folder structure preserved. - Commit:
feat: add Research Data Studio (MVP 1a). - Push origin.
- Wait 30–90s for Pages deploy.
- Visit
https://rezaprama.github.io/infraimpact/admin.html. - Verify pseudo-login works.
- Verify 571 seed assets imported on first load.
- Hard-refresh (Ctrl+Shift+R) to clear cache.
27. PERTANYAAN KLARIFIKASI YANG MASIH ANDA PERLU JAWAB
- Anggota panel BWM: Berapa pakar? Disipliner mereka? (untuk kapasitas multi-expert aggregation.)
- Consent regime: Apakah Anda sudah pegang surat etik (Ethics Committee approval)? Format consent (verbal/tertulis) yang dipakai?
- Klasifikasi data: Apakah Direktorat Sanitasi sudah memberikan surat persetujuan publikasi? Jika belum, semua data tetap di-flag
internaldi mode Public. - Skema enumerasi: Apakah Anda sendiri yang mengisi semua form, atau ada enumerator tambahan? Ini menentukan apakah perlu Supabase (multi-user) atau IndexedDB cukup (single-user).
- Indikator perlu update? Apakah daftar 22 indikator di
data/indicators.jsonmasih final? Atau Anda akan menambah dimensi lain (mis. environmental externality)? - Threshold absolute override: Sekarang
effluent_compliance_rate < 0.50→ red. Apakah ada threshold lain dari PerMen LHK / SNI yang harus dipasang? - Bahasa dokumen output PDF: ID, EN, atau dual-language?
- Branding: Apakah saya boleh memasang logo UI + Kemen-PUPR di kop PDF? Atau hanya nama tesis Anda?
- DOI Zenodo: Mau saya siapkan integration sekarang atau setelah dataset riil masuk?
- Field deployment: Kapan Anda akan mulai input data riil ke studio ini? Itu menentukan urgensi migrasi Supabase.
Akhir master plan. Lanjut ke deliverable kode di file berikutnya.
