feat(cctv): embed live CCTV feeds as card layout with MJPEG stream preview

- Replaced table layout with responsive card grid (3 columns)
- Each station shows embedded live MJPEG feed with fallback for unavailable streams
- Admin edit button moved to card header
- Open Full Screen link in card footer
- Added cctv.tck.com.my to CSP img-src and connect-src
This commit is contained in:
root
2026-05-30 23:48:31 +08:00
parent 659400bbad
commit 63dd4e72e0
2 changed files with 50 additions and 43 deletions

View File

@@ -9,7 +9,7 @@ server {
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://unpkg.com https://cdnjs.cloudflare.com https://code.jquery.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://unpkg.com https://fonts.googleapis.com; img-src 'self' data: https://tile.openstreetmap.org https://*.tile.openstreetmap.org; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net https://unpkg.com; connect-src 'self' https://tile.openstreetmap.org https://*.tile.openstreetmap.org https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com;" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://unpkg.com https://cdnjs.cloudflare.com https://code.jquery.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://unpkg.com https://fonts.googleapis.com; img-src 'self' data: https://tile.openstreetmap.org https://*.tile.openstreetmap.org https://cctv.tck.com.my; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net https://unpkg.com; connect-src 'self' https://tile.openstreetmap.org https://*.tile.openstreetmap.org https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com https://cctv.tck.com.my;" always;
charset utf-8;

View File

@@ -17,58 +17,65 @@
</div>
@endif
<div class="table-responsive">
<table class="table table-bordered" id="tblwl">
<thead class="table-light">
<tr>
<th scope="col">@lang('messages.station')</th>
<th scope="col">@lang('messages.district')</th>
<th scope="col">@lang('messages.link')</th>
@if (Auth::check() && Auth::user()->access_level == 1)
<th scope="col">Edit</th>
@endif
</tr>
</thead>
<tbody>
@foreach ($stationdata as $row)
<tr id="row-{{ $row->stationid }}">
<td>{{ $row->name }}</td>
<td>{{ $row->district }}</td>
<td>
<span id="link-display-{{ $row->stationid }}">
<a href="{{ $row->cctv_link }}" target="_blank" rel="noopener noreferrer">{{ $row->cctv_link }}</a>
</span>
<span id="link-edit-{{ $row->stationid }}" style="display:none;">
<form id="edit-form-{{ $row->stationid }}" method="POST" action="{{ route('cctv.update', $row->stationid) }}" class="d-flex gap-2 align-items-center" style="display:inline-flex;">
@csrf
<input type="text" name="cctv_link" value="{{ $row->cctv_link }}" class="form-control form-control-sm" style="min-width:250px;">
<button type="submit" class="btn btn-success btn-sm"><i class='bx bx-check'></i></button>
<button type="button" class="btn btn-secondary btn-sm" onclick="cancelEdit('{{ $row->stationid }}')"><i class='bx bx-x'></i></button>
</form>
</span>
</td>
<div class="row">
@foreach ($stationdata as $row)
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong>{{ $row->name }}</strong>
<br><small class="text-muted">{{ $row->district }}</small>
</div>
@if (Auth::check() && Auth::user()->access_level == 1)
<td>
<button class="btn btn-outline-primary btn-sm" onclick="toggleEdit('{{ $row->stationid }}')"><i class='bx bx-edit'></i></button>
</td>
<button class="btn btn-outline-primary btn-sm" onclick="toggleEdit('{{ $row->stationid }}')"><i class='bx bx-edit'></i></button>
@endif
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="card-body p-2 text-center bg-dark" id="feed-display-{{ $row->stationid }}">
@if($row->cctv_link)
<img src="{{ $row->cctv_link }}" alt="{{ $row->name }}" class="img-fluid w-100" style="max-height:300px; object-fit:contain;" onerror="this.style.display='none'; this.parentElement.innerHTML='<div class=\'text-white-50 py-5\'><i class=\'bx bx-camera-off\' style=\'font-size:3rem;\'></i><p class=\'mt-2 mb-0\'>Feed unavailable</p></div>';">
@else
<div class="text-white-50 py-5">
<i class='bx bx-camera-off' style='font-size:3rem;'></i>
<p class="mt-2 mb-0">No CCTV link configured</p>
</div>
@endif
</div>
<div class="card-body p-2 bg-dark" id="feed-edit-{{ $row->stationid }}" style="display:none;">
<form id="edit-form-{{ $row->stationid }}" method="POST" action="{{ route('cctv.update', $row->stationid) }}" class="d-flex gap-2 align-items-center p-2">
@csrf
<input type="text" name="cctv_link" value="{{ $row->cctv_link }}" class="form-control form-control-sm" style="min-width:250px;">
<button type="submit" class="btn btn-success btn-sm"><i class='bx bx-check'></i></button>
<button type="button" class="btn btn-secondary btn-sm" onclick="cancelEdit('{{ $row->stationid }}')"><i class='bx bx-x'></i></button>
</form>
</div>
<div class="card-footer text-center">
@if($row->cctv_link)
<a href="{{ $row->cctv_link }}" target="_blank" rel="noopener noreferrer" class="btn btn-sm btn-outline-primary w-100">
<i class='bx bx-link-external'></i> Open Full Screen
</a>
@endif
</div>
</div>
</div>
@endforeach
</div>
@if(empty($stationdata))
<div class="alert alert-light text-center mt-4" role="alert">
No CCTV stations configured.
</div>
@endif
</div>
</section>
<script>
function toggleEdit(stationid) {
document.getElementById('link-display-' + stationid).style.display = 'none';
document.getElementById('link-edit-' + stationid).style.display = 'inline';
document.getElementById('feed-display-' + stationid).style.display = 'none';
document.getElementById('feed-edit-' + stationid).style.display = 'block';
}
function cancelEdit(stationid) {
document.getElementById('link-display-' + stationid).style.display = 'inline';
document.getElementById('link-edit-' + stationid).style.display = 'none';
document.getElementById('feed-display-' + stationid).style.display = 'block';
document.getElementById('feed-edit-' + stationid).style.display = 'none';
}
</script>
@endsection