`). |
| `size` | Lebar visual elemen input / kolom (px). |
| `format` | Tipe handler data dari `CmsFormatField.php` (`text`, `textarea`, `number`, `select`, `radio`, `checkbox`, `datetime`, `upload_image`, `upload_file`, `geolocation`). |
| `fvalue` | Konfigurasi JSON metadata untuk limit karakter, date format, atau opsi radio/select. Contoh: `{"listing":{"cut":200},"insert":{}}` atau `{"property":{"rawdata":"Active;Disable|Active"}}`. |
| `rft` | *Relational Foreign Table* format pipe-delimited: `tabel_tujuan|kolom_value|kolom_label|klausa_where`. Contoh: `dts_ruas_saluran|id_ruas|nama_ruas|`. |
| `in_list` | `1` = Muncul sebagai kolom di tabel listing; `0` = Sembunyi dari listing. |
| `in_update` | 3-Digit Flag Visibilitas & Mutabilitas Form : • Digit 1 (`substr(in_update, 0, 1)`): `1` = Hidden di Form Insert/Add Data (`d-none`), `0` = Tampil. • Digit 2 (`substr(in_update, 1, 1)`): `1` = Hidden di Form Update/Edit Data, `0` = Tampil. • Digit 3 (`substr(in_update, 2, 1)`): `1` = Hidden di Halaman Detail, `0` = Tampil. • Readonly Flag (`substr(in_update, -1)`): Jika digit terakhir bernilai `1` pada tipe text, field dirender teks readonly + hidden input. |
| `default_insert` | Evaluasi PHP string (`eval()`) saat insert (contoh: `date('Y-m-d H:i:s')`). |
| `default_update` | Evaluasi PHP string (`eval()`) saat update. |
| `listing_php` | Cuplikan kode PHP dinamis yang dieksekusi via `eval()` pada setiap baris data di `CmsListing.php:337`. |
| `detail_php` | Cuplikan kode PHP dinamis yang dieksekusi saat membuka tampilan detail. |
---
C. Lifecycle CRUD pada Mode Listing (`Models/Cms/CmsUpdate.php` & `CmsDetail.php`)
Ketika user berinteraksi dengan tabel listing, request dialihkan oleh `CmsContent.php:100-145` melalui segmen URL `urinext()`:
Add Form (`/cms/{idnav}/insrt/add/{id_protab}/`) :
Ditangani oleh `CmsUpdate::addData()` (`CmsUpdate.php:130-236`).
Merender field sesuai format `CmsFormatField::ininsert()`.
Mengarahkan form action ke `POST /cms/{idnav}/recinsrt/{id_protab}`.
Insert Execution (`/cms/{idnav}/recinsrt/{id_protab}`) :
Ditangani oleh `CmsUpdate::recinsrt()` (`CmsUpdate.php:368-400`).
Setiap nilai POST dengan format `{id_protab}t_{field_name}` diekstrak dan disanitasi via `CmsFormatField::posvar_insert()`.
Dieksekusi via `$this->db->table($table)->insert($arrinsert)`.
Redirect kembali ke listing: `header("location: /cms/{idnav}")`.
Edit Form (`/cms/{idnav}/update/{id_record}/`) :
Ditangani oleh `CmsUpdate::setupdate()` (`CmsUpdate.php:239-352`).
Mengambil data eksisting via `outRow($table, [$id_key => $id_record])`.
Mengisi nilai form via `CmsFormatField::inupdate()` dan action ke `POST /cms/{idnav}/recupdate/{id_record}`.
Update Execution (`/cms/{idnav}/recupdate/{id_record}`) :
Ditangani oleh `CmsUpdate::recupdate()` (`CmsUpdate.php:402-440`).
Mengekstrak `{id_protab}t_{field_name}` via `CmsFormatField::posvar_update()`.
Menjalankan `$this->db->table($table)->update($arrUpdate, [$id_key => $id_update])`.
Delete Action (`/cms/{idnav}/delete/{id_record}/`) :
Ditangani oleh `CmsUpdate::setdelete()` (`CmsUpdate.php:442-530`).
Memeriksa apakah ada file/gambar terkait (`upload_image` / `upload_file`) untuk di-`unlink()` dari filesystem `imagebank/`.
Menghapus record: `DELETE FROM $tabel WHERE $id_key = $idDelete`.
Mengembalikan JSON `{valid: id}` jika request via AJAX atau redirect jika standard HTTP.
Detail View (`/cms/{idnav}/detail/{id_record}/`) :
Ditangani oleh `CmsDetail::home()` (`CmsDetail.php:21-116`).
Jika dipanggil via AJAX dengan suffix `/plain/1/`, mengembalikan respon JSON `{valid: 1, htm: $data}` untuk di-inject ke dalam Modal Bootstrap (`CmsDetail.php:113`).
---
3. PERMISI LEVEL & AUTENTIKASI (ACCESS CONTROL)
Sistem otentikasi dan autorisasi CMS SIPASI berpusat pada `Models/Cms/CmsLogIn.php` dan session CI4.
┌───────────────────────────────┐
│ POST /cms login (uid + ulock) │
└───────────────┬───────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
[ Tabel user_admin ] [ Tabel dts_user_petugas ]
│ │
├─ password_verify / md5 ├─ password_verify / md5
├─ level_admin = admin.level ├─ level_admin = petugas.level (def: 2)
└─ Set Session User └─ Set Session User
│ │
└───────────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ CI4 Session Storage │
│ - log_id, user_id, level │
│ - user_name, kode_di │
└─────────────┬─────────────┘
│
┌────────────────────────┴────────────────────────┐
▼ ▼
[ CmsNavigation::home() ] [ CmsContent::home() ]
Filter Query Menu Navigasi: Pengecekan Otorisasi Konten:
Level 1: WHERE user_level IN (1, 2) - outRow('cms_navigation', ...)
Level N: WHERE user_level = '$level' - Jika gagal: Blokir akses + pesan error
3.1 Session Data yang Ditetapkan Saat Login (`CmsLogIn.php:100-145`)
Setelah kredensial divalidasi, session diinisialisasi dengan struktur berikut:
php
$arrUser = array(
'log_id' => $user['id'], // Primary key ID user
'local' => $user['local'] ?? 1,
'kode_di' => !empty($user['area_di']) ? $user['area_di'] : (env('id_di') ?? 1),
'user_name' => $user['user_name'], // Nama lengkap user
'user_id' => $user['user_id'], // Username login
'level_admin' => $user['level_admin'], // Integer level hak akses
'jabatan' => $user['jabatan'], // Jabatan struktural
'web_id' => $user['web_id'] ?? 1,
'ip' => $_SERVER['REMOTE_ADDR'],
'his_log' => $user['his_log'] ?? '',
'demo' => $user['on_demo'] ?? '',
);
$this->session->set($arrUser);
3.2 Hierarki Level Pengguna
| Level (`level_admin`) | Peran / Deskripsi | Akses Menu `cms_navigation` |
|---|---|---|
| `1` | Super Administrator / Developer | Melihat seluruh menu dengan `user_level = '1'` ATAU `user_level = '2'`. Mendapatkan footer konfigurasi properti tabel (`getProtabFooter()`). |
| `2` | Operator / Admin Daerah Irigasi | Hanya melihat menu dengan `user_level = '2'` (Perencanaan, Pelaksanaan, Monitoring, Pengaturan). |
| `4` | Viewer / Publik Eksternal | Hanya melihat menu dengan `user_level = '4'` (Dashboard, Skema Publik, Data Ringkas). |
3.3 Titik Penyaringan & Pembatasan Otorisasi
Penyaringan Menu (`Models/Cms/CmsNavigation.php:23-28`) :
php
$level = $this->session->get('level_admin');
if ($level == '1') {
$query = $this->db->query("SELECT * FROM cms_navigation WHERE (user_level = '1' OR user_level = '2') AND sub_from_id = '0' ORDER BY lis");
} else {
$query = $this->db->query("SELECT * FROM cms_navigation WHERE user_level = '$level' AND sub_from_id = '0' ORDER BY lis");
}
Pencegahan Bypass URL Langsung (`Models/Cms/CmsContent.php:69-98`) :
Jika user mencoba mengakses ID navigasi yang tidak diizinkan untuk levelnya secara langsung (misal `/cms/41` oleh user level 4):
php
if ($level == '1') {
$proNav = outRow('cms_navigation', "id = '".$this->idnav."' AND (user_level = '1' OR user_level = '2')", '');
} else {
$proNav = outRow('cms_navigation', ['id' => $this->idnav, 'user_level' => $level], '');
}
if (empty($proNav['id'])) {
return '<div class="info my-4 mx-auto border w-50 p-4 text-center"> You do not have the authority to add to this data property <br><i>~ Please check in permission level ' . $level . ' navid ' . $this->idnav . ' ~</i></div>';
}
---
4. POLA ENDPOINT & AJAX ACTION (ROUTING & DATA EXCHANGE)
CMS SIPASI menggunakan arsitektur URL modular yang diparsing melalui helper URI khusus `urinext()`.
4.1 Mekanisme Chaining `urinext()`
Fungsi `urinext($var)` (`Helpers/bassic_helper.php:3-15`) bekerja dengan mencari posisi segmen `$var` dalam URL aktif, lalu mengembalikan nilai segmen persis setelahnya:
$$\text{URL}: \texttt{/cms/sip/skema\_jaringan/simpan\_giliran}$$
$$\begin{aligned}
\texttt{urinext('cms')} &\longrightarrow \mathbf{\text{'sip'}} \\
\texttt{urinext('sip')} &\longrightarrow \mathbf{\text{'skema\_jaringan'}} \\
\texttt{urinext('skema\_jaringan')} &\longrightarrow \mathbf{\text{'simpan\_giliran'}}
\end{aligned}$$
---
4.2 Pola Routing Modular di `Controllers/Cms.php:57-69`
php
public function body_content(){
$data = '';
if (urinext('cms') == 'mdl') {
// Memanggil Model pada namespace App\Models\Modul\Mdl_{nama}
$mdl = model('App\Models\Modul\Mdl_' . urinext('mdl'));
$data = $mdl->home();
} else if (urinext('cms') == 'sip') {
// Memanggil Model pada namespace App\Models\Sipasi\Mdl_{nama}
$mdl = model('App\Models\Sipasi\Mdl_' . urinext('sip'));
$data = $mdl->home();
} else {
// Default engine listing CMS
$data = $this->cmsContent->home();
}
return $data;
}
---
4.3 Studi Kasus Pola AJAX: `Mdl_skema_jaringan.php` (`Models/Sipasi/Mdl_skema_jaringan.php:38-125`)
[ Frontend Javascript AJAX ]
│
│ POST: /cms/sip/skema_jaringan/simpan_giliran
│ Data: { id, nama_giliran, total_durasi, data_json, ... }
▼
Controllers/Cms.php::body_content()
│
├─► urinext('cms') == 'sip'
└─► Instantiate: App\Models\Sipasi\Mdl_skema_jaringan -> home()
│
▼
Models/Sipasi/Mdl_skema_jaringan.php::home()
│
├─► urinext('skema_jaringan') == 'simpan_giliran'
└─► Eksekusi simpan_giliran()
│
├─ Validasi request method (POST)
├─ Validasi payload ($nama_giliran tidak kosong)
├─ Database Operation: $this->db->table('dd_giliran')->insert/update
└─ Return Respon JSON:
exit(json_encode(['valid' => 1, 'id' => $new_id, 'message' => '...']));
Standar Format Respon JSON CMS SIPASI:
json
{
"valid": 1,
"id": 105,
"message": "Data berhasil disimpan."
}
Respon Gagal / Validasi Eror :
json
{
"valid": 0,
"error": "Nama giliran tidak boleh kosong."
}
Respon Modal Pop-up (HTML Plain) :
json
{
"valid": 1,
"htm": "<div class='detail-view'>...content...</div>"
}
---
4.4 Generic Data Provider: `Mdl_db_ajax.php` (`Models/Modul/Mdl_db_ajax.php:21-99`)
Endpoint umum yang sering digunakan komponen frontend untuk query lookup instan:
URL : `POST /cms/mdl/db_ajax`
Parameter POST :
`table`: Nama tabel (dilindungi regex whitelist `/^[a-zA-Z0-9_]+$/`).
`fl`: Daftar kolom (misal: `id,name,id_key,last_update`).
`wh`: Klausa filter sanitasi (misal: `status='Active'`).
`opt`: Sorting/Limit tambahan (misal: `ORDER BY id DESC LIMIT 10`).
Respon Output : `{"table": "...", "list": [...], "ttl": N}`.
---
5. RESEP PEMBUATAN MODUL BARU: 'MONITORING TELEMETRI & CCTV'
Modul baru "Monitoring Telemetri & CCTV" memiliki 4 kebutuhan utama:
Realtime Telemetri : Data sensor debit/TMA/baterai terakhir per device (`dd_device_in_minute` JOIN `dts_device_node`).
Status Heartbeat : Indikator *Online / Offline* berdasarkan selisih waktu `last_update` terhadap interval `dtime` device.
Galeri Snapshot CCTV : Menampilkan foto kamera terbaru dari tabel `dd_device_image`.
Flatten Video .dav : Daftar arsip video `.dav` hasil flatten crawler storage.
5.1 Rekomendasi Arsitektur: Mode 'model'
> [!IMPORTANT]
> Rekomendasi Utama: Gunakan Mode 'model' (`view_mode = 'model'` dengan class `Sipasi\Mdl_monitoring_telemetri`).
> Alasan Teknis: Kebutuhan modul mencakup multi-tabel join, visualisasi grid/galeri CCTV, status heartbeat berkode warna realtime, auto-refresh interval 30 detik, serta playback/list video `.dav` yang tidak dapat dipenuhi secara fleksibel oleh form listing tabular statis.
---
5.2 Skrip SQL Registrasi Menu CMS (`cms_navigation`)
Jalankan query SQL berikut untuk mendaftarkan modul ke dalam navigasi CMS di bawah grup menu Monitoring (Induk ID `27`):
sql
-- 1. Ambil urutan lis terakhir pada sub_from_id = 27
-- 2. Insert menu 'Monitoring Telemetri & CCTV'
INSERT INTO `cms_navigation` (
`lang_code`,
`web_id`,
`position`,
`tabmenu`,
`sub_from_id`,
`lis`,
`user_level`,
`title`,
`sub_title`,
`id_protab`,
`view_mode`,
`val`,
`inhome`,
`lisinhome`,
`icon`,
`description`,
`hidden`
) VALUES (
'id',
'1',
'top',
NULL,
27, -- sub_from_id = 27 (Induk: Monitoring)
20, -- lis = urutan tampilan
2, -- user_level = 2 (Admin & SuperAdmin)
'Monitoring Telemetri & CCTV', -- Title menu
'Realtime Status, Sensor & CCTV Node', -- Subtitle
0, -- id_protab = 0 (karena mode model)
'model', -- view_mode = model
'Sipasi\\Mdl_monitoring_telemetri', -- val = Target Model CI4
NULL,
0,
'<i class=\"fa-duotone fa-tower-broadcast fa-lg\"></i>', -- Icon
'Modul monitoring terpadu telemetri, heartbeat status, dan visual CCTV',
0
);
---
5.3 Skeleton & Implementasi Backend Model CI4
Buat file baru di `app_sipasi/Models/Sipasi/Mdl_monitoring_telemetri.php`:
php
<?php
namespace App\Models\Sipasi;
use App\Controllers\BaseController;
helper('bassic');
class Mdl_monitoring_telemetri extends BaseController
{
protected $db;
protected $session;
protected $request;
protected $parser;
protected $id_di;
public function __construct()
{
$this->parser = service('parser');
$this->session = \Config\Services::session();
$this->request = \Config\Services::request();
$this->db = \Config\Database::connect();
$this->id_di = env('id_di') ?? 1;
}
/**
*/
public function home()
{
// AJAX Endpoints
if (urinext('monitoring_telemetri') === 'ajax_realtime') {
return $this->ajax_realtime();
}
if (urinext('monitoring_telemetri') === 'ajax_cctv') {
return $this->ajax_cctv();
}
if (urinext('monitoring_telemetri') === 'ajax_dav_videos') {
return $this->ajax_dav_videos();
}
// Tampilan antarmuka utama
return $this->render_dashboard();
}
/**
Render UI Utama Dashboard Monitoring
*/
public function render_dashboard()
{
$arrData = [
'BASE_URL' => base_url(),
'TITLE' => 'Monitoring Telemetri & CCTV',
'DEVICES_STATUS' => $this->get_devices_status(),
'CCTV_GALLERY' => $this->get_cctv_latest(),
'DAV_VIDEOS' => $this->get_dav_file_list(),
'DATE_NOW' => date('d F Y H:i:s'),
];
return $this->parser->setData($arrData)->render('Sipasi/Monitoring_telemetri');
}
/**
a & b) Query Telemetri Terakhir + Perhitungan Heartbeat Status
*/
public function get_devices_status()
{
// Query menggabungkan metadata node dengan data telemetri menit terakhir
$sql = "
SELECT
node.id,
node.name AS device_name,
node.id_key,
node.device_type,
node.dtime,
node.location,
node.last_update AS node_last_update,
minute.last_update AS telemetry_last_update,
minute.data_1 AS tma_level,
minute.data_2 AS debit_val,
minute.data_3 AS battery_volt,
minute.data_4 AS signal_rssi,
-- Perhitungan heartbeat: jika selisih waktu <= (dtime * 2.5) detik maka Online
CASE
WHEN node.last_update IS NULL THEN 'Offline'
WHEN TIMESTAMPDIFF(SECOND, node.last_update, NOW()) <= (COALESCE(node.dtime, 300) * 2.5) THEN 'Online'
ELSE 'Offline'
END AS heartbeat_status,
TIMESTAMPDIFF(SECOND, node.last_update, NOW()) AS last_seen_seconds
FROM dts_device_node node
LEFT JOIN (
-- Subquery record telemetri paling baru per id_key
SELECT m1.*
FROM dd_device_in_minute m1
INNER JOIN (
SELECT id_key, MAX(id) AS max_id
FROM dd_device_in_minute
GROUP BY id_key
) m2 ON m1.id = m2.max_id
) minute ON node.id_key = minute.id_key
WHERE node.status = 'Active'
ORDER BY node.name ASC
";
return $this->db->query($sql)->getResultArray();
}
/**
c) Query Snapshot / CCTV Image Terbaru
*/
public function get_cctv_latest()
{
$sql = "
SELECT
img.id,
img.id_key,
img.bg_bagi,
img.image,
img.time_image,
img.image_2,
img.time_image_2,
img.last_update,
node.name AS device_name,
node.location
FROM dd_device_image img
LEFT JOIN dts_device_node node ON img.id_key = node.id_key
ORDER BY img.last_update DESC
";
return $this->db->query($sql)->getResultArray();
}
/**
d) Scan Daftar File Video .dav Flatten
*/
public function get_dav_file_list($limit = 30)
{
$videoDir = FCPATH . 'cctv_storage/videos/';
$videoList = [];
if (is_dir($videoDir)) {
$files = glob($videoDir . '*.dav');
if ($files) {
// Urutkan file berdasarkan waktu modifikasi terbaru
usort($files, function ($a, $b) {
return filemtime($b) - filemtime($a);
});
$files = array_slice($files, 0, $limit);
foreach ($files as $file) {
$filename = basename($file);
$videoList[] = [
'filename' => $filename,
'filesize' => round(filesize($file) / (1024 * 1024), 2) . ' MB',
'filetime' => date('Y-m-d H:i:s', filemtime($file)),
'video_url' => base_url('cctv_storage/videos/' . $filename),
];
}
}
}
return $videoList;
}
/**
AJAX Endpoint: Polling Realtime JSON
*/
public function ajax_realtime()
{
$data = $this->get_devices_status();
exit(json_encode([
'valid' => 1,
'timestamp' => date('Y-m-d H:i:s'),
'devices' => $data,
]));
}
/**
AJAX Endpoint: CCTV Snapshot Gallery JSON
*/
public function ajax_cctv()
{
$data = $this->get_cctv_latest();
exit(json_encode([
'valid' => 1,
'images' => $data,
]));
}
/**
AJAX Endpoint: Video .dav List JSON
*/
public function ajax_dav_videos()
{
$data = $this->get_dav_file_list();
exit(json_encode([
'valid' => 1,
'videos' => $data,
]));
}
}
---
5.4 Alternatif: Konfigurasi SQL untuk Mode 'Listing'
Jika di masa mendatang diperlukan versi CRUD tabular murni untuk tabel monitoring, gunakan resep metadata berikut:
sql
-- 1. Insert ke cms_table_property
INSERT INTO `cms_table_property` (
`admin_level`,
`table_name`,
`title`,
`description`,
`custom_where`,
`order_by`,
`entry_number`,
`sending`,
`id_key`,
`action`,
`property`,
`plain_tpl`
) VALUES (
'1',
'dts_device_node',
'Daftar Node Telemetri & CCTV',
'Master data dan status node telemetri',
'status = \'Active\'',
'name',
25,
'asc',
'id',
';Detail;', -- Read-only listing dengan tombol Detail
'Search;;;;;Zebra',
'0'
);
SET @new_protab_id = LAST_INSERT_ID();
-- 2. Insert Field Property ke cms_listing_field
INSERT INTO `cms_listing_field` (`id_table_property`, `listing`, `field_name`, `label`, `size`, `format`, `fvalue`, `in_list`, `in_update`) VALUES
(@new_protab_id, 1, 'id', 'ID', 60, 'text', '{"listing":{"cut":10}}', 0, '111'),
(@new_protab_id, 2, 'name', 'Nama Node / Stasiun', 240, 'text', '{"listing":{"cut":50}}', 1, '000'),
(@new_protab_id, 3, 'id_key', 'Device ID Key', 180, 'text', '{"listing":{"cut":40}}', 1, '000'),
(@new_protab_id, 4, 'device_type', 'Tipe Node', 120, 'text', '{"listing":{"cut":20}}', 1, '000'),
(@new_protab_id, 5, 'location', 'Lokasi Geografis', 180, 'text', '{"listing":{"cut":40}}', 1, '000'),
(@new_protab_id, 6, 'dtime', 'Interval (dtk)', 80, 'number', '{"listing":{"precision":0}}', 1, '000'),
(@new_protab_id, 7, 'last_update', 'Heartbeat Terakhir', 160, 'datetime', '{"listing":{"dateformat":"d-m-Y H:i:s"}}', 1, '110'),
(@new_protab_id, 8, 'status', 'Status Node', 100, 'radio', '{"property":{"rawdata":"Active;Disable|Active"}}', 1, '000');
---
6. TABEL REFERENSI PATH & BUKTI KODE
| No | Modul / Komponen | File Path Aktual | Baris Bukti | Deskripsi Peran |
|---|---|---|---|---|
| 1 | Routing CMS | `sipasi_oransbari/app_sipasi/Config/Routes.php` | `L57-68` | Pendaftaran rute `$routes->add('/cms/(:any)', 'Cms::index/');` |
| 2 | Cms Controller | `sipasi_oransbari/app_sipasi/Controllers/Cms.php` | `L15-33` | Konstruktor otentikasi login & session guard |
| 3 | Cms Index & Layout | `sipasi_oransbari/app_sipasi/Controllers/Cms.php` | `L35-69` | Parsing placeholder `HEAD_NAVIGATION` & `BODY_CONTENT` |
| 4 | URI Chaining Helper | `sipasi_oransbari/app_sipasi/Helpers/bassic_helper.php` | `L3-15` | Definisi fungsi `urinext($var)` |
| 5 | Navigation Generator | `sipasi_oransbari/app_sipasi/Models/Cms/CmsNavigation.php` | `L20-116` | Query menu bertingkat, level user, dan active state |
| 6 | Autentikasi & Level | `sipasi_oransbari/app_sipasi/Models/Cms/CmsLogIn.php` | `L24-162` | Verifikasi pass/bcrypt, mapping level_admin ke session |
| 7 | Content Dispatcher | `sipasi_oransbari/app_sipasi/Models/Cms/CmsContent.php` | `L61-178` | Pemisahan eksekusi `view_mode == 'listing'` vs `'model'` |
| 8 | Listing Engine | `sipasi_oransbari/app_sipasi/Models/Cms/CmsListing.php` | `L28-392` | Query dinamis, formatting cell, dan eksekusi `eval()` |
| 9 | CRUD Mutator Engine | `sipasi_oransbari/app_sipasi/Models/Cms/CmsUpdate.php` | `L130-530` | Handler form add, update, insert, delete, upload |
| 10 | Field Formatter | `sipasi_oransbari/app_sipasi/Models/Cms/CmsFormatField.php` | `L20-400` | Format tipe data, lookups `rft`, dan parser `in_update` |
| 11 | AJAX Pattern Contoh | `sipasi_oransbari/app_sipasi/Models/Sipasi/Mdl_skema_jaringan.php` | `L38-125` | Pola endpoint AJAX sub-modul (`simpan_giliran`) |
| 12 | Generic Data Provider | `sipasi_oransbari/app_sipasi/Models/Modul/Mdl_db_ajax.php` | `L21-99` | Handler generic database ajax dengan validasi whitelist |
| 13 | Main Page Template | `sipasi_oransbari/app_sipasi/Views/Cms/main_page.php` | `L25-36` | Posisi tag `{ !HEAD_NAVIGATION! }` dan `{ !BODY_CONTENT! }` |
| 14 | Navbar View | `sipasi_oransbari/app_sipasi/Views/Cms/headNavigation.php` | `L1-33` | HTML markup navigasi Bootstrap dengan `{ !LIST_NAV! }` |
| 15 | Skema Navigasi DB | `db_dump/DB_SIPASI_Oransbari.sql` | `L1223-1288` | Definisi DDL tabel `cms_navigation` & data eksisting |
| 16 | Skema Protab DB | `db_dump/DB_SIPASI_Oransbari.sql` | `L1294-1331` | Definisi DDL tabel `cms_table_property` |
| 17 | Skema Field DB | `db_dump/DB_SIPASI_Oransbari.sql` | `L42-64` | Definisi DDL tabel `cms_listing_field` |
| 18 | Skema Device Telemetri | `db_dump/DB_SIPASI_Oransbari.sql` | `L1756-1780` | Struktur tabel `dd_device_in_minute` |
| 19 | Skema Snapshot CCTV | `db_dump/DB_SIPASI_Oransbari.sql` | `L1711-1727` | Struktur tabel `dd_device_image` |
| 20 | Skema Master Device | `db_dump/DB_SIPASI_Oransbari.sql` | `L2280-2307` | Struktur tabel `dts_device_node` |
---
*Dokumen ini disusun sebagai panduan standar pengembangan modul internal CMS SIPASI tanpa mengubah kode sistem eksisting.*