Update 34 files

- /nfc/README.md
- /nfc/clean_old_logs.sh
- /nfc/clockin.py
- /nfc/emptycards.py
- /nfc/sync_clocks.sh
- /piservices/Dockerfile
- /piservices/Portainer-lamp-stack.env
- /piservices/Portainer-lamp-stack.yaml
- /piservices/README.md
- /piservices/avahi-alias.service
- /piservices/clockpy.service
- /piservices/emptycards.service
- /piservices/nfc-sync.service
- /piservices/test.sh
- /www/404
- /www/get-table-data.php
- /www/habiwan copy.png
- /www/habiwan.png
- /www/index.php
- /www/log_swipe.php
- /www/manage_data.php
- /www/manifest.json
- /www/names.csv
- /www/network_audit.csv
- /www/network-tracker.html
- /www/nfc_debug.log
- /www/scanner.html
- /www/staff_names.php
- /www/times.csv
- /www/check_update.php
- /www/dev.php
- /www/favicon.png
- /www/JsBarcode.all.min.js
- /www/README.md
This commit is contained in:
jpd committed 2026-07-24 14:37:45 +01:00
1 parent 10824ab851
commit a201d5d7ac
34 files changed
+9453

No files matched your search

+534
View File
@@ -0,0 +1,534 @@
<?php
// Start a secure session
session_start();
// 1. CHOOSE YOUR MANAGEMENT PASSWORD HERE:
define('STAFF_PASSWORD', '1111');
define('TIMEOUT_SECONDS', 86400); // Inactivity threshold
// Handle Explicit Logout
if (isset($_GET['logout'])) {
unset($_SESSION['authenticated']);
unset($_SESSION['last_activity']);
header("Location: " . $_SERVER['PHP_SELF']);
exit;
}
// Handle Login Form Submission
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['x_secure_token'])) {
if ($_POST['x_secure_token'] === STAFF_PASSWORD) {
$_SESSION['authenticated'] = true;
$_SESSION['last_activity'] = time();
header("Location: " . $_SERVER['PHP_SELF']);
exit;
} else {
$login_error = "Incorrect password access denied.";
}
}
// Server-Side Inactivity Check
if (isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true) {
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > TIMEOUT_SECONDS)) {
unset($_SESSION['authenticated']);
unset($_SESSION['last_activity']);
header("Location: " . $_SERVER['PHP_SELF'] . "?reason=timeout");
exit;
}
$_SESSION['last_activity'] = time();
}
if (isset($_GET['reason']) && $_GET['reason'] === 'timeout') {
unset($_SESSION['authenticated']);
unset($_SESSION['last_activity']);
}
// If user is not authenticated, show the login gate
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
$display_msg = "Staff Access";
if (isset($_GET['reason']) && $_GET['reason'] === 'timeout') {
$login_error = "Logged out due to inactivity.";
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Staff Login</title>
<link rel="icon" href="favicon.png" type="image/png">
<style>
html {
-webkit-text-size-adjust: 100%;
touch-action: manipulation; /* Improves touch response */
}
/* Prevent iOS zoom on input focus */
input[type="text"],
input[type="date"],
input[type="password"] {
font-size: 16px !important;
}
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', sans-serif; }
/* 1. Change body to flex-column */
body {
background: linear-gradient(180deg, #1c3344, #102a33, #000000);
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center; /* Keeps it centered horizontally */
}
/* 2. Push login box to the center of available space */
.login-box {
margin: auto;
background: rgba(255, 255, 255, 0.95);
padding: 40px 30px;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
width: 100%;
max-width: 380px;
text-align: center;
}
h2 { margin-bottom: 25px; color: #0d47a1; font-size: 1.8rem; font-weight: 600; }
input[type="password"] { width: 100%; padding: 15px; margin-bottom: 20px; border: 1px solid #ccc; border-radius: 8px; font-size: 1rem; transition: all 0.3s; }
input[type="password"]:focus { outline: none; border-color: #1976d2; box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.15); }
button { width: 100%; padding: 15px; background-color: #1976d2; color: white; border: none; border-radius: 8px; font-size: 1.1rem; font-weight: 600; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
button:hover { background-color: #1565c0; transform: translateY(-2px); box-shadow: 0 6px 15px rgba(25, 118, 210, 0.3); }
.error { background: #ffebee; color: #c62828; padding: 12px; border-radius: 8px; font-weight: bold; margin-bottom: 20px; font-size: 0.9rem; border: 1px solid #ffcdd2; }
/* 3. Footer stays at the bottom */
.footer { text-align: center; padding-bottom: 20px; color: rgba(255, 255, 255, 0.7); font-size: 0.9rem; }
/* Responsive adjustments for screens smaller than 600px */
@media (max-width: 600px) {
header h1 {
/* Scales between 1.5rem and 2.8rem depending on screen width */
font-size: clamp(1.5rem, 5vw, 2.8rem);
margin-bottom: 15px;
text-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
header p {
font-size: 1rem;
}
.employee-name {
font-size: 0.95rem;
}
.time-badge {
font-size: 0.8rem;
padding: 4px 10px;
}
.search-box {
max-width: 100%;
}
.controls {
flex-direction: column;
}
}
</style>
</head>
<body>
<img src="favicon.png" alt="PiClock">
<div class="login-box">
<h2><?= htmlspecialchars($display_msg) ?></h2>
<?php if (isset($login_error)): ?>
<div class="error"><?= htmlspecialchars($login_error) ?></div>
<?php endif; ?>
<form method="POST" autocomplete="off" onsubmit="requestFS()">
<input type="password" name="x_secure_token" placeholder="Enter Password" autocomplete="new-password" required autofocus>
<button type="submit">Log In</button>
</form>
</div>
<div class="footer">
'PiClock' NFC Timestamp Viewer <span style="display: inline-block; transform: rotateY(180deg);">&copy;</span> 2025-<?php echo date('Y');?>
<br>made with &hearts; by F.Javier "<a href="mailto:habiwan@me.com" style="color: #fff;">habiwan</a>" Puig Diaz
<br><img src="habiwan.png" alt="habiwan">
</div>
</body>
</html>
<?php
exit;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>PiClock Dashboard</title>
<link rel="icon" href="favicon.png" type="image/png">
<style>
html {
-webkit-text-size-adjust: 100%;
touch-action: manipulation; /* Improves touch response */
}
/* Prevent iOS zoom on input focus */
input[type="text"],
input[type="date"],
input[type="password"] {
font-size: 16px !important;
}
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', sans-serif; }
body { background: linear-gradient(180deg, #1c3344, #102a33, #000000); min-height: 100dvh; padding: 30px 20px; color: #fff; }
.container { max-width: 1200px; margin: 0 auto; }
header { text-align: center; padding: 5px 0; margin-bottom: 1px; }
header h1 {
/* Scales between 1.5rem and 2.8rem depending on screen width */
font-size: clamp(1.5rem, 5vw, 2.8rem);
margin-bottom: 15px;
text-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
header p { font-size: 1.2rem; opacity: 0.9; }
.controls { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; flex-wrap: wrap; gap: 20px; }
.search-box { flex: 1; min-width: 150px; max-width: 250px; }
.search-box input { width: 100%; padding: 14px 20px; border-radius: 50px; border: 1px solid rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.15); color: white; font-size: 1rem; }
.search-box input:focus { outline: none; background: rgba(255, 255, 255, 0.25); border-color: #64b5f6; }
.date-filters { display: flex; gap: 15px; align-items: center; flex-wrap: wrap; }
.date-group { display: flex; align-items: center; gap: 8px; }
.date-group label { font-size: 0.9rem; color: #bbdefb; font-weight: 500; }
.date-group input[type="date"] { padding: 11px 15px; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.15); color: white; font-size: 0.95rem; font-family: inherit; color-scheme: dark; }
.date-group input[type="date"]:focus { outline: none; background: rgba(255, 255, 255, 0.25); border-color: #64b5f6; }
.timeline-container { display: flex; flex-direction: column; gap: 25px; margin-bottom: 20px; }
.day-card { background: rgba(255, 255, 255, 0.95); border-radius: 12px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
.day-header { display: flex; justify-content: space-between; align-items: center; padding: 15px 25px; background: #1976d2; color: white; }
.day-title { font-size: 1.2rem; font-weight: 600; text-shadow: 0 1px 2px rgba(0,0,0,0.2); }
.day-meta { display: flex; gap: 10px; align-items: center; }
.day-meta .year, .day-meta .week { padding: 4px 10px; border-radius: 4px; font-size: 0.85rem; background: rgba(255, 255, 255, 0.2); color: white; font-weight: 500; }
.day-content { display: flex; flex-direction: column; }
.employee-row { display: flex; justify-content: space-between; align-items: center; padding: 15px 25px; border-bottom: 1px solid #e0e0e0; gap: 20px; }
.employee-row:last-child { border-bottom: none; }
.employee-row:hover { background-color: #f5f9ff; }
.employee-info { display: flex; align-items: center; gap: 15px; }
.avatar { width: 40px; height: 40px; border-radius: 50%; background: linear-gradient(45deg, #2196f3, #64b5f6); display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 1.1rem; }
.employee-name { font-weight: 600; font-size: 1.05rem; color: #263238; }
.card-id-sub { font-size: 0.85rem; color: #666; margin-top: 2px; }
.badge-unknown { background-color: #ffebee; color: #c62828; padding: 4px 10px; border-radius: 12px; font-size: 0.85rem; font-weight: bold; border: 1px solid #ffcdd2; display: inline-block; }
code { background-color: #f0f1f4; padding: 2px 6px; border-radius: 4px; font-family: monospace; color: #1976d2; font-size: 0.85rem; }
.punch-times { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; max-width: 60%; }
.time-badge { background: #e3f2fd; color: #0d47a1; padding: 6px 14px; border-radius: 30px; font-size: 0.9rem; font-weight: 600; border: 1px solid #bbdefb; box-shadow: 0 2px 4px rgba(0,0,0,0.04); }
.table-header { display: flex; justify-content: space-between; padding: 15px 25px; background: #0d47a1; color: white; font-weight: 500; border-radius: 12px; margin-bottom: 15px; position: relative; z-index: 2; }
.table-header a, .table-header a:link, .table-header a:visited { color: #ffffffff !important; text-decoration: none; cursor: default; }
.footer { text-align: center; margin-top: 40px; color: rgba(255, 255, 255, 0.7); font-size: 0.9rem; }
/* ADDED: Visual Cues for Interactive filtering elements */
.day-title, .year, .week, .employee-name, .badge-unknown {
cursor: pointer;
transition: opacity 0.2s ease;
}
.day-title:hover, .year:hover, .week:hover, .employee-name:hover, .badge-unknown:hover {
opacity: 0.6;
}
/* Responsive adjustments for screens smaller than 600px */
@media (max-width: 600px) {
/* 1. Reclaim wasted screen space by reducing container padding */
body {
padding: 15px 10px;
}
/* 2. Header and Search adjustments */
header h1 {
font-size: clamp(1.5rem, 6vw, 2.2rem) !important;
margin-bottom: 10px;
}
header p { font-size: 1rem; }
.search-box { max-width: 100%; }
.controls { flex-direction: column; align-items: stretch; }
.date-filters { justify-content: space-between; }
/* 3. Stack the Table Header */
.table-header {
flex-direction: column;
text-align: center;
padding: 12px;
gap: 10px;
}
/* 4. Stack the Day Header so dates don't wrap aggressively */
.day-header {
flex-direction: column;
align-items: flex-start;
padding: 12px 15px;
gap: 8px;
}
.day-title { font-size: 1.1rem; }
/* 5. Stack the Employee Rows so time badges drop to a new line */
.employee-row {
flex-direction: column;
align-items: flex-start;
padding: 15px;
gap: 12px;
}
.employee-info { width: 100%; }
/* 6. Allow time punches to take full width and align left under the name */
.punch-times {
max-width: 100%;
justify-content: flex-start;
padding-left: 55px; /* Indents them nicely under the avatar */
}
.time-badge {
font-size: 0.85rem;
padding: 5px 12px;
}
.employee-name { font-size: 1rem; }
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>'PiClock' NFC System</h1>
<p>custom made Punchcard-System replacement</p>
</header>
<div class="controls">
<div class="search-box">
<input type="text" id="searchInput" placeholder="🔎 Search">
</div>
<div class="date-filters">
<div class="date-group">
<label for="startDate">From:</label>
<input type="date" id="startDate">
</div>
<div class="date-group">
<label for="endDate">To:</label>
<input type="date" id="endDate">
</div>
</div>
</div>
<div class="table-header">
<div><a href="/staff_names.php" target="_blank">Grouped Shift Logs</a></div>
<div id="recordCounter">Loading NFC Card swipes...</div>
</div>
<div class="timeline-container" id="live-data">
</div>
<div class="footer">
'PiClock' NFC Timestamp Viewer <span style="display: inline-block; transform: rotateY(180deg);">&copy;</span> 2025-<?php echo date('Y');?>
<br>made with &hearts; by F.Javier "<a href="mailto:habiwan@me.com" style="color: #fff;">habiwan</a>" Puig Diaz
<br><img src="habiwan.png" alt="habiwan">
</div>
<script>
// Safety net to re-enable fullscreen if lost
window.addEventListener('load', function() {
document.body.addEventListener('click', function() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => {});
}
}, { once: true });
});
// --- INTERACTIVE CLICK LISTENERS ---
document.getElementById('live-data').addEventListener('click', function(e) {
const searchInput = document.getElementById('searchInput');
const startDateInput = document.getElementById('startDate');
const endDateInput = document.getElementById('endDate');
// 1. Employee Name or ID or Unknown Badge
const clickedName = e.target.closest('.employee-name') || e.target.closest('.badge-unknown');
if (clickedName) {
let textToSearch = clickedName.textContent.trim();
// Toggle off if already searching this exact text
if (searchInput.value === textToSearch) {
searchInput.value = '';
} else {
searchInput.value = textToSearch;
}
filterDashboard();
}
// 2. Day Title
const clickedDay = e.target.closest('.day-title');
if (clickedDay) {
const d = clickedDay.closest('.day-card').dataset.date;
// Toggle off if already filtered to this exact day
if (startDateInput.value === d && endDateInput.value === d) {
startDateInput.value = '';
endDateInput.value = '';
} else {
startDateInput.value = d;
endDateInput.value = d;
}
filterDashboard();
}
// 3. Year
const clickedYear = e.target.closest('.year');
if (clickedYear) {
const y = clickedYear.dataset.year;
const startY = y + '-01-01';
const endY = y + '-12-31';
// Toggle off if already filtered to this exact year
if (startDateInput.value === startY && endDateInput.value === endY) {
startDateInput.value = '';
endDateInput.value = '';
} else {
startDateInput.value = startY;
endDateInput.value = endY;
}
filterDashboard();
}
// 4. Week
const clickedWeek = e.target.closest('.week');
if (clickedWeek) {
const startWk = clickedWeek.dataset.start;
const endWk = clickedWeek.dataset.end;
// Toggle off if already filtered to this exact week
if (startDateInput.value === startWk && endDateInput.value === endWk) {
startDateInput.value = '';
endDateInput.value = '';
} else {
startDateInput.value = startWk;
endDateInput.value = endWk;
}
filterDashboard();
}
});
// --- FILTER LOGIC ---
function filterDashboard() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase().trim();
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
const dayCards = document.querySelectorAll('.day-card');
let totalVisiblePunches = 0;
dayCards.forEach(card => {
const dayDate = card.getAttribute('data-date');
let dateMatches = true;
if (startDate && dayDate < startDate) dateMatches = false;
if (endDate && dayDate > endDate) dateMatches = false;
if (!dateMatches) {
card.style.display = 'none';
return;
}
let visibleEmployeesInDay = 0;
const rows = card.querySelectorAll('.employee-row');
rows.forEach(row => {
const empName = row.getAttribute('data-name');
// Updated search logic
const textMatches = empName.includes(searchTerm) ||
(searchTerm === 'unknown card' && row.querySelector('.badge-unknown'));
if (textMatches) {
row.style.display = '';
visibleEmployeesInDay++;
totalVisiblePunches += row.querySelectorAll('.time-badge').length;
} else {
row.style.display = 'none';
}
});
if (visibleEmployeesInDay > 0) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
document.getElementById('recordCounter').textContent = 'Total: ' + totalVisiblePunches + ' swipes';
}
document.getElementById('searchInput').addEventListener('input', filterDashboard);
document.getElementById('startDate').addEventListener('change', filterDashboard);
document.getElementById('endDate').addEventListener('change', filterDashboard);
// --- LIVE UPDATE LOGIC ---
let currentVersion = '';
// Fetches the HTML from the backend and updates the container
async function fetchTableData() {
try {
// Ensure this points to the new PHP script you created in the first step
const response = await fetch('get-table-data.php?t=' + Date.now());
if (!response.ok) return;
const htmlRows = await response.text();
// Inject the new HTML
document.getElementById('live-data').innerHTML = htmlRows;
// Re-run the filter immediately so new data obeys active search/dates
filterDashboard();
} catch (error) {
console.error('Error fetching table data:', error);
}
}
// Checks the database for new row counts
async function checkServerForUpdates() {
try {
const response = await fetch('check_update.php?t=' + Date.now());
if (!response.ok) {
console.warn('Update background check failed with status:', response.status);
return;
}
const latestVersion = await response.text();
// Initial load scenario
if (!currentVersion) {
currentVersion = latestVersion;
fetchTableData(); // Fetch the data for the first time
return;
}
// If a change happened in the MySQL Database
if (latestVersion !== currentVersion) {
console.log('Database change detected! Fetching new data...');
currentVersion = latestVersion;
fetchTableData();
}
} catch (error) {
console.error('Error checking for updates:', error);
}
}
// Check for updates every 3000 milliseconds (3 seconds)
checkServerForUpdates();
setInterval(checkServerForUpdates, 3000);
</script>
<script>
// Pass the PHP timeout value into JavaScript to log out after inactivity
const TIMEOUT_DURATION = <?php echo TIMEOUT_SECONDS; ?> * 1000;
let logoutTimer;
function resetTimer() {
clearTimeout(logoutTimer);
logoutTimer = setTimeout(logoutUser, TIMEOUT_DURATION);
}
function logoutUser() {
window.location.href = "?logout=1";
}
// Attach listeners
window.onload = resetTimer;
window.onmousemove = resetTimer;
window.onmousedown = resetTimer;
window.ontouchstart = resetTimer;
window.onclick = resetTimer;
window.onkeydown = resetTimer;
</script>
</body>
</html>