Compare commits

...

3 Commits

Author SHA1 Message Date
Flatlogic Bot
1b3f6ab743 v3 2026-04-05 11:53:02 +00:00
Flatlogic Bot
cc09913585 v2 2026-04-05 11:27:52 +00:00
Flatlogic Bot
8be8405504 v1 unk 2026-04-05 09:55:47 +00:00
8 changed files with 3969 additions and 551 deletions

20
api/audit-log.php Normal file
View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../app/channel_data.php';
function audit_respond(int $status, array $payload): void
{
http_response_code($status);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
try {
channel_app_bootstrap();
audit_respond(200, ['success' => true, 'data' => audit_log_list($_GET)]);
} catch (Throwable $e) {
audit_respond(500, ['success' => false, 'error' => 'Unable to load audit log.']);
}

74
api/channels.php Normal file
View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../app/channel_data.php';
function respond(int $status, array $payload): void
{
http_response_code($status);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
channel_app_bootstrap();
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$userId = channel_current_user_id();
try {
if ($method === 'GET') {
if (isset($_GET['stats'])) {
respond(200, ['success' => true, 'data' => channel_stats()]);
}
if (isset($_GET['history_for'])) {
respond(200, ['success' => true, 'data' => channel_history((int) $_GET['history_for'])]);
}
if (isset($_GET['id'])) {
$channel = channel_get((int) $_GET['id']);
if (!$channel) {
respond(404, ['success' => false, 'error' => 'Channel not found.']);
}
respond(200, ['success' => true, 'data' => $channel, 'meta' => [
'editable_fields' => CHANNEL_EDITABLE_FIELDS,
'locked_fields' => CHANNEL_LOCKED_FIELDS,
'options' => channel_distinct_options(CHANNEL_FILTER_OPTION_FIELDS),
]]);
}
respond(200, ['success' => true, 'data' => channel_list($_GET)]);
}
$rawBody = file_get_contents('php://input');
$payload = $rawBody ? json_decode($rawBody, true) : [];
if (!is_array($payload)) {
$payload = [];
}
if ($method === 'PATCH') {
if (isset($_GET['bulk'])) {
$result = channel_bulk_patch($payload['ids'] ?? [], $payload['fields'] ?? [], $userId);
respond(200, ['success' => true, 'message' => 'Bulk update applied.', 'data' => $result]);
}
if (!isset($_GET['id'])) {
respond(400, ['success' => false, 'error' => 'Missing channel id.']);
}
$result = channel_patch((int) $_GET['id'], $payload['fields'] ?? $payload, $userId);
respond(200, ['success' => true, 'message' => 'Channel updated.', 'data' => $result]);
}
respond(405, ['success' => false, 'error' => 'Method not allowed.']);
} catch (RuntimeException $e) {
if (str_starts_with($e->getMessage(), 'FORBIDDEN_FIELD:')) {
respond(403, ['success' => false, 'error' => 'Attempted to modify a locked system field.']);
}
respond(500, ['success' => false, 'error' => $e->getMessage() ?: 'Unexpected runtime error.']);
} catch (InvalidArgumentException $e) {
respond(422, ['success' => false, 'error' => $e->getMessage()]);
} catch (Throwable $e) {
respond(500, ['success' => false, 'error' => 'Unexpected server error.']);
}

1574
app/channel_data.php Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

14
healthz.php Normal file
View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/app/channel_data.php';
try {
channel_app_bootstrap();
$count = (int) db()->query('SELECT COUNT(*) FROM channels')->fetchColumn();
echo json_encode(['ok' => true, 'channels' => $count, 'time' => gmdate('c')], JSON_UNESCAPED_SLASHES);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode(['ok' => false], JSON_UNESCAPED_SLASHES);
}

600
index.php
View File

@ -1,150 +1,488 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC'); @date_default_timezone_set('UTC');
require_once __DIR__ . '/app/channel_data.php';
channel_app_bootstrap();
$phpVersion = PHP_VERSION; $projectName = $_SERVER['PROJECT_NAME'] ?? 'SignalDesk TV';
$now = date('Y-m-d H:i:s'); $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'TV channels data management workspace for controlled edits, analytics, and auditability.';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?> ?>
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($projectName) ?> · TV Channels Data Management</title>
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>">
<meta name="author" content="Flatlogic AI">
<?php if ($projectDescription): ?> <?php if ($projectDescription): ?>
<!-- Meta description --> <meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>">
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' /> <meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>">
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?> <?php endif; ?>
<?php if ($projectImageUrl): ?> <?php if ($projectImageUrl): ?>
<!-- Open Graph image --> <meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>">
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" /> <meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>">
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?> <?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
:root { <link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head> </head>
<body> <body>
<main> <div class="app-shell">
<div class="card"> <header class="border-bottom bg-white sticky-top app-header">
<h1>Analyzing your requirements and generating your website…</h1> <nav class="navbar navbar-expand-lg navbar-light py-2">
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes"> <div class="container-fluid px-3 px-lg-4">
<span class="sr-only">Loading…</span> <a class="navbar-brand d-flex align-items-center gap-2" href="#workspace">
<span class="brand-mark">SD</span>
<span>
<span class="d-block brand-title">SignalDesk TV</span>
<span class="d-block brand-subtitle">Controlled channel operations</span>
</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
<li class="nav-item"><a class="nav-link" href="#workspace">Workspace</a></li>
<li class="nav-item"><a class="nav-link" href="#analytics-panel">Analytics</a></li>
<li class="nav-item"><a class="nav-link" href="#audit-panel">Audit log</a></li>
<li class="nav-item"><a class="nav-link" href="healthz.php" target="_blank" rel="noopener">Health</a></li>
</ul>
</div> </div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div> </div>
</nav>
</header>
<main class="container-fluid px-3 px-lg-4 py-4">
<section class="hero-panel mb-4" id="workspace">
<div class="card panel-card">
<div class="card-body p-4 p-lg-5">
<div class="d-flex flex-wrap gap-2 align-items-center mb-3">
<span class="eyebrow">Channel workspace</span>
<span class="status-chip">Fast search · Technical filters · Version history</span>
</div>
<h1 class="hero-title">Find channels faster, filter every technical field, and spot the latest edit instantly.</h1>
<p class="hero-copy mb-0">Use the quick search bar on top for anything specific, then narrow results with the detailed filters on the left.</p>
</div>
</div>
</section>
<section class="row g-4">
<aside class="col-12 col-xl-3">
<div class="card panel-card sticky-xl-top filters-card">
<div class="card-body p-3 p-lg-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h2 class="section-title mb-1">Filters</h2>
<p class="text-muted small mb-0">Use quick search on top for free text, then apply exact filters here.</p>
</div>
<button class="btn btn-sm btn-outline-secondary" id="resetFiltersBtn">Reset</button>
</div>
<form id="filtersForm" class="vstack gap-4" autocomplete="off">
<input id="searchInput" name="search" type="hidden" value="">
<div>
<div class="section-subtitle mb-2">Main filters</div>
<div class="row g-3">
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="satRefFilter">Satellite</label>
<select id="satRefFilter" name="sat_ref" class="form-select filter-select" data-default-option="All satellites"><option value="">All satellites</option></select>
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="countryFilter">Country ISO</label>
<select id="countryFilter" name="country_iso" class="form-select filter-select" data-default-option="All countries"><option value="">All countries</option></select>
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="countryClientFilter">Country client</label>
<select id="countryClientFilter" name="country_client" class="form-select filter-select" data-default-option="All client countries"><option value="">All client countries</option></select>
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="genreFilter">Genre</label>
<select id="genreFilter" name="genre" class="form-select filter-select" data-default-option="All genres"><option value="">All genres</option></select>
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="typeFilter">Type</label>
<select id="typeFilter" name="type" class="form-select">
<option value="">All types</option>
<option value="free">free</option>
<option value="payed">payed</option>
</select>
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="resolutionFilter">Resolution</label>
<select id="resolutionFilter" name="resolution" class="form-select filter-select" data-default-option="All resolutions"><option value="">All resolutions</option></select>
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="languageFilter">Language</label>
<select id="languageFilter" name="langue" class="form-select filter-select" data-default-option="All languages"><option value="">All languages</option></select>
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="regionFilter">Region</label>
<select id="regionFilter" name="region" class="form-select filter-select" data-default-option="All regions"><option value="">All regions</option></select>
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="groupFilter">Group</label>
<select id="groupFilter" name="groupe" class="form-select filter-select" data-default-option="All groups"><option value="">All groups</option></select>
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="activeFilter">Active</label>
<select id="activeFilter" name="active" class="form-select">
<option value="">Default logic</option>
<option value="1">1</option>
<option value="0">0</option>
</select>
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="manualFilter">Manual flag</label>
<select id="manualFilter" name="manually_edited" class="form-select">
<option value="">All</option>
<option value="1">Edited</option>
<option value="0">Not edited</option>
</select>
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="idtypeFilter">Id type</label>
<select id="idtypeFilter" name="idtype" class="form-select">
<option value="">All</option>
<option value="1">Known (1)</option>
<option value="2">Unknown (2)</option>
</select>
</div>
</div>
</div>
<div>
<div class="section-subtitle mb-2">Technical filters</div>
<div class="row g-3">
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="tpInfoFilter">sat_tpinfo</label>
<select id="tpInfoFilter" name="sat_tpinfo" class="form-select filter-select" data-default-option="All transponders"><option value="">All transponders</option></select>
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="sixIdFilter">six_id</label>
<input id="sixIdFilter" name="six_id" class="form-control" type="text" placeholder="Exact match">
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="sidFilter">sid</label>
<input id="sidFilter" name="sid" class="form-control" type="text" placeholder="Exact match">
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="onidFilter">onid</label>
<input id="onidFilter" name="onid" class="form-control" type="text" placeholder="Exact match">
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="tidFilter">tid</label>
<input id="tidFilter" name="tid" class="form-control" type="text" placeholder="Exact match">
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="clientChannelFilter">sat_ch_client_upper</label>
<select id="clientChannelFilter" name="sat_ch_client_upper" class="form-select filter-select" data-default-option="All client channels"><option value="">All client channels</option></select>
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="clientBouquetFilter">sat_client</label>
<select id="clientBouquetFilter" name="sat_client" class="form-select filter-select" data-default-option="All bouquets"><option value="">All bouquets</option></select>
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="countingFilter">counting</label>
<input id="countingFilter" name="counting" class="form-control" type="text" placeholder="Exact match">
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="lastviewFilter">lastview</label>
<input id="lastviewFilter" name="lastview" class="form-control" type="text" placeholder="Exact match">
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="satInFilter">sat_in</label>
<input id="satInFilter" name="sat_in" class="form-control" type="date">
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="satOutFilter">sat_out</label>
<input id="satOutFilter" name="sat_out" class="form-control" type="date">
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="satUpdateFilter">sat_update</label>
<input id="satUpdateFilter" name="sat_update" class="form-control" type="date">
</div>
<div class="col-sm-6 col-xl-12">
<label class="form-label" for="manualEditedAtFilter">manually_edited_at</label>
<input id="manualEditedAtFilter" name="manually_edited_at" class="form-control" type="date">
</div>
</div>
</div>
<button class="btn btn-dark" type="submit">Apply filters</button>
</form>
</div>
</div>
</aside>
<div class="col-12 col-xl-9">
<div class="card panel-card mb-4">
<div class="card-body p-3 p-lg-4">
<div class="d-flex flex-column gap-3">
<div class="d-flex flex-column flex-lg-row align-items-lg-center justify-content-between gap-3">
<div>
<h2 class="section-title mb-1">Channels</h2>
<p class="text-muted small mb-0">Search on top, then narrow the dataset with detailed business and technical filters.</p>
</div>
<div class="d-flex flex-wrap gap-2 align-items-center">
<button class="btn btn-outline-secondary btn-sm" id="selectPageBtn">Select page</button>
<button class="btn btn-outline-secondary btn-sm" id="saveCurrentViewBtn">Save visible page</button>
<button class="btn btn-dark btn-sm" id="bulkEditBtn" data-bs-toggle="modal" data-bs-target="#bulkEditModal" disabled>Bulk edit (0)</button>
</div>
</div>
<form id="topSearchForm" class="dataset-search-bar" autocomplete="off">
<div class="dataset-search-grid">
<div class="dataset-search-input-wrap">
<label class="visually-hidden" for="topSearchInput">Quick search</label>
<input id="topSearchInput" class="form-control dataset-search-input" type="search" placeholder="Search channel, satellite, transponder, client, or system id…">
</div>
<button class="btn btn-outline-secondary advanced-filter-trigger" type="button" id="advancedFiltersBtn" data-bs-toggle="modal" data-bs-target="#advancedFilterModal">
Advanced filter
<span class="advanced-filter-count d-none" id="advancedFilterCount">0</span>
</button>
<button class="btn btn-dark" type="submit">Search</button>
<button class="btn btn-outline-secondary" type="button" id="clearSearchBtn">Clear</button>
</div>
</form>
<div class="toolbar-strip">
<div class="toolbar-left">
<span class="legend legend-manual"><span class="legend-swatch"></span> Manually edited</span>
<span class="legend legend-last-change"><span class="legend-swatch"></span> Last edited field</span>
<span class="legend legend-selected"><span class="legend-swatch"></span> Selected row</span>
</div>
<div class="toolbar-right small text-muted">
<span id="resultCounter">Loading channels…</span>
</div>
</div>
<div id="activeFilterChips" class="chip-row"></div>
<div id="selectionBar" class="selection-bar d-none" aria-live="polite">
<div><strong id="selectionCount">0</strong> row(s) selected</div>
<div class="d-flex gap-2 flex-wrap">
<button class="btn btn-sm btn-dark" id="selectionBulkBtn" data-bs-toggle="modal" data-bs-target="#bulkEditModal">Bulk edit selected</button>
<button class="btn btn-sm btn-outline-secondary" id="clearSelectionBtn">Clear selection</button>
</div>
</div>
<ul class="nav nav-tabs section-tabs" id="workspaceTabs" role="tablist">
<li class="nav-item" role="presentation"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#channels-panel" type="button" role="tab">Dataset</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#analytics-panel" type="button" role="tab">Analytics</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#history-panel" type="button" role="tab">History</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#audit-panel" type="button" role="tab">Audit log</button></li>
</ul>
<div class="tab-content pt-3">
<div class="tab-pane fade show active" id="channels-panel" role="tabpanel">
<div class="table-responsive channel-table-wrap">
<table class="table align-middle channel-table mb-0">
<thead>
<tr>
<th class="sticky-col checkbox-col"><input class="form-check-input" type="checkbox" id="selectAllPageCheckbox" aria-label="Select all rows on current page"></th>
<th data-sort="upper_ch_ref">upper_ch_ref</th>
<th data-sort="country_iso">country_iso</th>
<th data-sort="country_client">country_client</th>
<th data-sort="sat_ref">sat_ref</th>
<th data-sort="sat_tpinfo">sat_tpinfo</th>
<th data-sort="genre">genre</th>
<th data-sort="type">type</th>
<th data-sort="resolution">resolution</th>
<th data-sort="langue">langue</th>
<th data-sort="region">region</th>
<th data-sort="groupe">groupe</th>
<th data-sort="active">active</th>
<th data-sort="idtype">idtype</th>
<th class="sys-col" data-sort="six_id">six_id</th>
<th class="sys-col" data-sort="sid">sid</th>
<th class="sys-col" data-sort="onid">onid</th>
<th class="sys-col" data-sort="tid">tid</th>
<th data-sort="sat_in">sat_in</th>
<th data-sort="sat_out">sat_out</th>
<th data-sort="sat_update">sat_update</th>
<th class="sys-col" data-sort="sat_ch_client_upper">sat_ch_client_upper</th>
<th class="sys-col" data-sort="sat_client">sat_client</th>
<th class="sys-col" data-sort="counting">counting</th>
<th class="sys-col" data-sort="lastview">lastview</th>
<th data-sort="manually_edited_at">manually_edited_at</th>
</tr>
</thead>
<tbody id="channelsTableBody">
<tr><td colspan="26" class="empty-state-cell">Loading dataset…</td></tr>
</tbody>
</table>
</div>
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 pt-3">
<div class="small text-muted" id="pageMeta">Page 1 of 1</div>
<div class="pagination-controls d-flex gap-2">
<button class="btn btn-outline-secondary btn-sm" id="prevPageBtn">Previous</button>
<button class="btn btn-outline-secondary btn-sm" id="nextPageBtn">Next</button>
</div>
</div>
</div>
<div class="tab-pane fade" id="analytics-panel" role="tabpanel">
<div class="row g-3 mb-4" id="analyticsOverview"></div>
<div class="row g-3" id="analyticsBreakdowns"></div>
</div>
<div class="tab-pane fade" id="history-panel" role="tabpanel">
<div class="history-header d-flex flex-column flex-lg-row justify-content-between gap-3 mb-3">
<div>
<h3 class="section-title mb-1">Version history</h3>
<p class="small text-muted mb-0" id="historyLead">Click a row in Dataset to load its previous and current versions.</p>
</div>
<div class="d-flex gap-2 align-items-start align-items-lg-center">
<button class="btn btn-outline-secondary btn-sm" id="refreshHistoryBtn">Refresh history</button>
</div>
</div>
<div class="row g-3 mb-3" id="historySummary">
<div class="col-12"><div class="analytics-card"><div class="small text-muted">No channel selected yet.</div></div></div>
</div>
<div id="historyTimeline" class="history-stack">
<div class="analytics-card"><div class="small text-muted">History will appear here after you select a channel row.</div></div>
</div>
</div>
<div class="tab-pane fade" id="audit-panel" role="tabpanel">
<div class="row g-3 mb-3">
<div class="col-md-4"><label class="form-label" for="auditUserFilter">User</label><select id="auditUserFilter" class="form-select"><option value="">All users</option></select></div>
<div class="col-md-4"><label class="form-label" for="auditFieldFilter">Field</label><select id="auditFieldFilter" class="form-select"><option value="">All fields</option></select></div>
<div class="col-md-2"><label class="form-label" for="auditDateFrom">From</label><input id="auditDateFrom" class="form-control" type="date"></div>
<div class="col-md-2"><label class="form-label" for="auditDateTo">To</label><input id="auditDateTo" class="form-control" type="date"></div>
</div>
<div class="d-flex justify-content-between align-items-center mb-3">
<p class="small text-muted mb-0">Audit entries show each field-level change with old new value.</p>
<button class="btn btn-outline-secondary btn-sm" id="applyAuditFiltersBtn">Refresh log</button>
</div>
<div class="table-responsive audit-table-wrap">
<table class="table align-middle audit-table mb-0">
<thead>
<tr>
<th>When</th>
<th>User</th>
<th>Channel</th>
<th>Field</th>
<th>Change</th>
</tr>
</thead>
<tbody id="auditTableBody">
<tr><td colspan="5" class="empty-state-cell">Load the audit trail…</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</main> </main>
<footer> </div>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer> <div class="offcanvas offcanvas-end detail-offcanvas" tabindex="-1" id="detailPanel" aria-labelledby="detailPanelLabel">
<div class="offcanvas-header border-bottom">
<div>
<h2 class="offcanvas-title h5 mb-1" id="detailPanelLabel">Channel detail</h2>
<p class="small text-muted mb-0">Editable metadata + locked system fields.</p>
</div>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body p-0">
<form id="detailForm" class="detail-form">
<div class="detail-loading px-4 py-5 text-muted" id="detailLoadingState">Select a row to inspect and update it.</div>
<input type="hidden" id="detailChannelId">
<div id="detailEditableSection" class="d-none">
<div class="detail-section px-4 py-4 border-bottom">
<div class="section-subtitle">Editable fields</div>
<div id="detailLastChangeNotice" class="detail-last-change-note d-none"></div>
<div class="row g-3" id="editableFieldsContainer"></div>
</div>
<div class="detail-section px-4 py-4 border-bottom bg-light-subtle">
<div class="section-subtitle">Locked system fields</div>
<div class="row g-3" id="lockedFieldsContainer"></div>
</div>
<div class="detail-actions p-4 d-flex gap-2 justify-content-end">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="offcanvas">Cancel</button>
<button type="submit" class="btn btn-dark">Save changes</button>
</div>
</div>
</form>
</div>
</div>
<div class="modal fade" id="bulkEditModal" tabindex="-1" aria-labelledby="bulkEditModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header border-bottom">
<div>
<h2 class="modal-title h5 mb-1" id="bulkEditModalLabel">Bulk edit selected rows</h2>
<p class="small text-muted mb-0">Only checked fields will be applied to the selected channels. This includes <code>sat_out</code>.</p>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="bulkEditForm">
<div class="modal-body">
<div class="alert alert-light border small mb-4">This applies changes row by row, closes the previous version with <code>date_out</code>, creates a new current row, and writes one audit entry per modified field, per channel.</div>
<div class="row g-3" id="bulkEditFieldsContainer"></div>
</div>
<div class="modal-footer border-top d-flex justify-content-between">
<div class="small text-muted"><span id="bulkSelectionSummary">0</span> row(s) currently selected</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-dark">Apply to selected rows</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="advancedFilterModal" tabindex="-1" aria-labelledby="advancedFilterModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header border-bottom">
<div>
<h2 class="modal-title h5 mb-1" id="advancedFilterModalLabel">Advanced filter builder</h2>
<p class="small text-muted mb-0">Build a deeper search like <code>sid contains 123</code> or <code>Frequency / Transponder equals 11001</code>. Every rule is combined together.</p>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-light border small mb-4">Use this when the normal dropdown filters are too strict. You can mix contains, equals, does not contain, and not equal rules.</div>
<div id="advancedFiltersContainer" class="advanced-filters-stack"></div>
<button type="button" class="btn btn-outline-secondary mt-3" id="addAdvancedFilterBtn">Add rule</button>
</div>
<div class="modal-footer border-top d-flex justify-content-between gap-2 flex-wrap">
<button type="button" class="btn btn-outline-secondary" id="clearAdvancedFiltersBtn">Clear rules</button>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-dark" id="applyAdvancedFiltersBtn">Apply advanced filters</button>
</div>
</div>
</div>
</div>
</div>
<div class="toast-container position-fixed bottom-0 end-0 p-3" id="toastContainer"></div>
<script>
window.tvChannelApp = {
apiBase: 'api/channels.php',
auditApi: 'api/audit-log.php',
detailFields: {
editable: <?= json_encode(CHANNEL_EDITABLE_FIELDS, JSON_UNESCAPED_SLASHES) ?>,
locked: <?= json_encode(CHANNEL_LOCKED_FIELDS, JSON_UNESCAPED_SLASHES) ?>
},
currentUser: <?= json_encode(channel_current_user_id(), JSON_UNESCAPED_SLASHES) ?>
};
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="assets/js/main.js?v=<?= time() ?>"></script>
</body> </body>
</html> </html>