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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user