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 di geo/. 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 oleh file:// — wajib via http:// atau https://.
  • 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:

TahapStackAudiensStatus
MVP 1 (now)GitHub Pages + IndexedDB + pseudo-loginReza sendiri, demo akademikSebagian terbangun (MVP 1a)
MVP 2GitHub Pages + Supabase Postgres + Supabase AuthReza + 2–3 enumeratorPlan tersedia (§22)
MVP 3+ Role-based RLS, supervisor validation, real-time syncPembimbing + tim risetPlan tersedia
MVP 4Public release dengan anonimisasi otomatis + DOI ZenodoReviewer Q1 + publikPlan 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)

FieldTypeReqValidationPrivacy
asset_idstring PKY^AST-\d{4,}$public
asset_namestringY3–120 charspublic
asset_typeenumYIPLT | IPAL_komunal | IPAL_kawasan | TPA | TPST | SPALDTpublic
province_codestring(2)YBPSpublic
province_namestringYpublic
district_codestring(4)YBPSpublic
district_namestringYpublic
latitudefloatN[-11, 6]restricted
longitudefloatN[95, 141]restricted
construction_yearintY[2000, current_year]public
handover_yearintN≥ construction_yearpublic
funding_sourceenumYAPBN | APBD_I | APBD_II | Hibah | PHLN | Mixedpublic
capex_value_idrbigintN≥ 0internal
design_capacity_m3_dayfloatY> 0public
operator_entitystringN0–200internal
data_quality_flagenumYcomplete | partial | estimated | missing | inconsistent | placeholder | confidentialpublic
data_sourcestringYpublic
last_updateddateYISOpublic
notesstringN0–2000internal

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_score
  • recommendation_ids (array of FK ke recommendations.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)

FieldTypeReqPrivacy 
interview_idstring PKY (INT-YYYY-NNNN) 
interview_datedateYinternal 
locationstringYinternal 
province_code, district_codeFKYpublic (denorm) 
asset_idFKNpublic 
interviewerstringYrestricted 
respondent_codestringYconfidential (real name NEVER stored) 
respondent_roleenumYUPTD | Dinas_PUPR | Bappeda | operator | masyarakat | konsultan | kontraktor | regulator_nasional | akademisi | NGOinternal
institution_typeenumYcentral_govt | provincial | local | BUMD | KSM | NGO | private | academicinternal
consent_statusenumYwritten_consent | verbal_consent | no_consent_recordedconfidential
anonymity_levelenumYnamed (never used) | role_only | anonymized | fully_redactedinternal
interview_modeenumYin_person | video_call | phone | async_textinternal
duration_minutesintN≥ 0internal
transcript_summarytextY50–5000 charsinternal
full_transcript_optionaltextN0–100k charsconfidential (never publish)
key_quotesarray of {quote, timestamp_min, redacted}Ninternal
observed_problemtextNinternal
cause_tagsarrayNenum (13 tags)public
impact_tagsarrayNenum (10 tags)public
evidence_strengthenumYlow | medium | highpublic
linked_indicatorsarrayNFK ke indicatorspublic
linked_documentsarrayNFK ke documentspublic
follow_up_requiredboolNinternal
researcher_memotextNinternal
created_at, updated_atdatetimeYinternal
created_bystringYuser session id / roleinternal
coding_statusenumYdraft | coded | validated | rejectedinternal

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 dengan respondent_code anonim)
  • 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_status enum: unverified | verified_self | verified_cross | disputed
  • privacy_level enum: public | internal | restricted | confidential

4.7 bwm_experts (primary: expert_id)

FieldType
expert_idstring PK
expert_codestring (anonymous label, mis. “PANEL-A-E01”)
affiliation_typeenum: academic, government, practitioner, NGO
years_experienceint
domain_expertisearray (institusi, regulasi, finansial, layanan, teknis)
consent_statusenum
created_atdatetime

4.8 bwm_inputs (primary: input_id)

FieldTypeNotes
input_idstring PK 
expert_idFK 
blockenum: CAUSE | IMPACT 
levelenum: block | dimension | indicator 
best_criterionstringid kriteria yang dipilih “terbaik”
worst_criterionstringid kriteria yang dipilih “terburuk”
best_to_othersobject{criterion_id: score 1-9}
others_to_worstobject{criterion_id: score 1-9}
created_atdatetime 

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 ke bwm_inputs)
  • consistency_ratio (float)
  • xi_star (float — BWM optimality)
  • solver_mode enum: exact_lp | approximate | aggregated_panel
  • valid_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_zone
  • recommendation_ids
  • scoring_inputs (full JSON trace untuk traceability)
  • derived_from_observation_id, derived_from_weight_id
  • computed_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_recordedfull_transcript_optional dikunci.
  • 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 di assets/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.html accessible at https://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.html shell
  • assets/css/admin.css
  • assets/js/admin/db.js (IndexedDB)
  • assets/js/admin/auth.js (pseudo-login)
  • assets/js/admin/audit.js
  • assets/js/admin/raw-viewer.js
  • assets/js/admin/calc-studio.js
  • assets/js/admin/bwm-studio.js
  • assets/js/admin/forms.js (engine + 4 schemas)
  • assets/js/admin/csv-io.js
  • assets/js/admin/pdf-export.js
  • assets/js/admin/quality.js
  • assets/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)

FileActionPurpose
admin.htmlNEWStudio shell, sidebar, login gate
assets/css/admin.cssNEWStudio styling
assets/js/admin/studio-app.jsNEWBootstrap + router + layout
assets/js/admin/db.jsNEWIndexedDB wrapper
assets/js/admin/auth.jsNEWPseudo-login + role
assets/js/admin/audit.jsNEWAudit-log writer
assets/js/admin/raw-viewer.jsNEWAsset table view
assets/js/admin/calc-studio.jsNEWINFRAIMPACT walkthrough
assets/js/admin/bwm-studio.jsNEWBWM wizard
assets/js/admin/forms.jsNEWForm engine + 4 view registrations
assets/js/admin/form-schemas.jsNEWInterview/FGD/Obs/Doc schemas
assets/js/admin/csv-io.jsNEWPapaParse import/export
assets/js/admin/pdf-export.jsNEWjsPDF reports
assets/js/admin/quality.jsNEWData quality dashboard
index.htmlMODIFIEDAdd link to admin studio in header
docs/RESEARCH_DATA_STUDIO_PLAN.mdNEWthis file
docs/SUPABASE_MIGRATION.mdNEWMVP 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_*.csv downloads.
  • 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

  1. Anggota panel BWM: Berapa pakar? Disipliner mereka? (untuk kapasitas multi-expert aggregation.)
  2. Consent regime: Apakah Anda sudah pegang surat etik (Ethics Committee approval)? Format consent (verbal/tertulis) yang dipakai?
  3. Klasifikasi data: Apakah Direktorat Sanitasi sudah memberikan surat persetujuan publikasi? Jika belum, semua data tetap di-flag internal di mode Public.
  4. 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).
  5. Indikator perlu update? Apakah daftar 22 indikator di data/indicators.json masih final? Atau Anda akan menambah dimensi lain (mis. environmental externality)?
  6. Threshold absolute override: Sekarang effluent_compliance_rate < 0.50 → red. Apakah ada threshold lain dari PerMen LHK / SNI yang harus dipasang?
  7. Bahasa dokumen output PDF: ID, EN, atau dual-language?
  8. Branding: Apakah saya boleh memasang logo UI + Kemen-PUPR di kop PDF? Atau hanya nama tesis Anda?
  9. DOI Zenodo: Mau saya siapkan integration sekarang atau setelah dataset riil masuk?
  10. 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.