Tracking
Refreshed cached files
This commit is contained in:
fraxle
2026-06-07 10:50:42 +01:00
parent 1a69994833
commit a9d53eff46
7 changed files with 328 additions and 5 deletions
+1 -1
View File
@@ -23,7 +23,7 @@
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<link rel="stylesheet" href="./assets/about.css">
<link rel="stylesheet" href="./assets/about.css?v=15a">
<!-- Structured data for AEO / AI search -->
<script type="application/ld+json">
+13
View File
@@ -36,6 +36,16 @@ import { useColumnPopup } from './useColumnPopup.js';
import { useTableScroll } from './useTableScroll.js';
import { getActiveEvents, getLensEvent } from '../events.js';
function track(event, profile) {
try {
fetch('track.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(profile ? { event, profile } : { event }),
}).catch(() => {});
} catch (e) { /* ignore */ }
}
export function useAppState() {
// ── 1. LOCATION ──────────────────────────────────────────────────────
@@ -66,6 +76,8 @@ export function useAppState() {
const { forecast, airQuality, loading, error, now, fetchedAt, liveElev } = useForecast(location, isPro);
useEffect(() => { track('visit'); }, []);
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState([]);
const [searching, setSearching] = useState(false);
@@ -150,6 +162,7 @@ export function useAppState() {
const toggleCol = (col) => setVisibleCols(prev => ({ ...prev, [col]: !prev[col] }));
const activateProfile = (key) => {
track('profile', key);
const profile = FILTER_PROFILES[key];
try { localStorage.setItem('sunscope_profile', key); } catch (e) { /* ignore */ }
setActiveProfile(key);
+1 -1
View File
@@ -224,7 +224,7 @@ async function build() {
// 8. Copy static files
console.log(' Copying static files...');
for (const f of ['robots.txt', 'sitemap.xml', 'og-image.png', 'favicon.svg']) {
for (const f of ['robots.txt', 'sitemap.xml', 'og-image.png', 'favicon.svg', 'restore.php', 'track.php', 'stats.php']) {
const src = path.join(ROOT, f);
if (fs.existsSync(src)) {
copyFile(src, path.join(DIST, f));
+1 -1
View File
@@ -17,7 +17,7 @@
<meta name="twitter:description" content="Plain answers to the most common questions about outdoor comfort, UV safety, car heat, air quality, pollen, and how SunScope works." />
<meta name="twitter:image" content="https://sunscope.net/og-image.png" />
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<link rel="stylesheet" href="./assets/about.css">
<link rel="stylesheet" href="./assets/about.css?v=15a">
<script type="application/ld+json">
{
"@context": "https://schema.org",
+2 -2
View File
@@ -70,10 +70,10 @@
<link rel="preconnect" href="https://api.open-meteo.com" crossorigin />
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<link rel="stylesheet" href="./assets/sunscope.css?v=15g">
<link rel="stylesheet" href="./assets/sunscope.css?v=15h">
</head>
<body>
<div id="root"></div>
<script type="module" src="./assets/js/main.js?v=15b"></script>
<script type="module" src="./assets/js/main.js?v=15c"></script>
</body>
</html>
+238
View File
@@ -0,0 +1,238 @@
<?php
// ─── SunScope — Tracking Stats ────────────────────────────────────────────────
$data_file = __DIR__ . '/data/tracking.json';
$tracking = [];
if (file_exists($data_file)) {
$tracking = json_decode(file_get_contents($data_file), true) ?? [];
}
// Sort days newest first
krsort($tracking);
// Collect all profile keys seen across all days, in preferred display order
$profile_order = ['basic','home','vehicle','farming','outdoors','alltemps','showall','custom'];
$all_profiles = [];
foreach ($tracking as $day => $data) {
foreach (array_keys($data['profiles'] ?? []) as $p) {
$all_profiles[$p] = true;
}
}
// Sort: preferred order first, then any extras alphabetically
$ordered = [];
foreach ($profile_order as $p) {
if (isset($all_profiles[$p])) $ordered[] = $p;
}
foreach (array_keys($all_profiles) as $p) {
if (!in_array($p, $ordered, true)) $ordered[] = $p;
}
$profile_labels = [
'basic' => 'Basic',
'home' => 'Home',
'vehicle' => 'Vehicle',
'farming' => 'Farming',
'outdoors' => 'Places',
'alltemps' => 'Temps',
'showall' => 'Show All',
'custom' => 'Custom',
];
$total_visits = array_sum(array_column($tracking, 'visits'));
$profile_totals = [];
foreach ($ordered as $p) {
$profile_totals[$p] = array_sum(array_map(fn($d) => $d['profiles'][$p] ?? 0, $tracking));
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SunScope — Stats</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>☀️</text></svg>" />
<link href="https://fonts.bunny.net/css2?family=Fraunces:ital,wght@0,900;1,300&family=Manrope:wght@400;600&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: Manrope, sans-serif;
background: #f5edd6;
color: #1e1208;
padding: 2rem;
min-height: 100vh;
}
.header {
display: flex;
align-items: baseline;
gap: 1rem;
margin-bottom: 2rem;
}
.logo {
font-family: Fraunces, serif;
font-size: 1.8rem;
line-height: 1;
letter-spacing: -0.02em;
}
.logo-sun { color: #c8922a; font-weight: 900; font-style: normal; }
.logo-scope { color: #1e1208; font-weight: 300; font-style: italic; }
.subtitle {
font-family: Fraunces, serif;
font-style: italic;
color: #7a5c2a;
font-size: 0.9rem;
}
.summary {
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
margin-bottom: 2rem;
}
.pill {
background: #fffcf2;
border: 1.5px solid #c9b08a;
border-radius: 6px;
padding: 0.75rem 1.25rem;
}
.pill-label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #7a5c2a;
font-weight: 600;
}
.pill-value {
font-family: 'JetBrains Mono', monospace;
font-size: 1.5rem;
font-weight: 600;
color: #c8922a;
}
.table-wrap {
overflow-x: auto;
}
table {
border-collapse: collapse;
width: 100%;
min-width: 500px;
background: #fffcf2;
border: 1.5px solid #c9b08a;
border-radius: 6px;
overflow: hidden;
font-size: 0.875rem;
}
thead th {
background: #1e1208;
color: #f5edd6;
padding: 0.6rem 0.9rem;
text-align: left;
font-family: 'JetBrains Mono', monospace;
font-size: 0.72rem;
letter-spacing: 0.04em;
white-space: nowrap;
}
thead th.num { text-align: right; }
tbody tr:nth-child(even) { background: #fdf8ec; }
tbody tr:hover { background: #faefd8; }
td {
padding: 0.55rem 0.9rem;
border-top: 1px solid #e8dcc4;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
white-space: nowrap;
}
td.num { text-align: right; }
td.zero { color: #c9b08a; }
tfoot td {
background: #f5edd6;
border-top: 2px solid #c9b08a;
font-weight: 600;
color: #7a5c2a;
}
.empty {
background: #fffcf2;
border: 1.5px solid #c9b08a;
border-radius: 6px;
padding: 2rem;
text-align: center;
color: #7a5c2a;
font-style: italic;
}
.back {
margin-top: 1.5rem;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.back a { color: #b09870; text-decoration: none; }
.back a:hover { color: #c8922a; }
</style>
</head>
<body>
<div class="header">
<div class="logo">
<span class="logo-sun">SUN</span><span class="logo-scope">Scope</span>
</div>
<div class="subtitle">Usage stats</div>
</div>
<div class="summary">
<div class="pill">
<div class="pill-label">Total visits</div>
<div class="pill-value"><?= number_format($total_visits) ?></div>
</div>
<div class="pill">
<div class="pill-label">Days tracked</div>
<div class="pill-value"><?= count($tracking) ?></div>
</div>
<?php foreach ($ordered as $p): ?>
<div class="pill">
<div class="pill-label"><?= htmlspecialchars($profile_labels[$p] ?? $p) ?> clicks</div>
<div class="pill-value"><?= number_format($profile_totals[$p]) ?></div>
</div>
<?php endforeach; ?>
</div>
<?php if (empty($tracking)): ?>
<div class="empty">No tracking data yet.</div>
<?php else: ?>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Date</th>
<th class="num">Visits</th>
<?php foreach ($ordered as $p): ?>
<th class="num"><?= htmlspecialchars($profile_labels[$p] ?? $p) ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php foreach ($tracking as $day => $data): ?>
<tr>
<td><?= htmlspecialchars($day) ?></td>
<td class="num <?= $data['visits'] === 0 ? 'zero' : '' ?>"><?= $data['visits'] ?></td>
<?php foreach ($ordered as $p): $n = $data['profiles'][$p] ?? 0; ?>
<td class="num <?= $n === 0 ? 'zero' : '' ?>"><?= $n === 0 ? '—' : $n ?></td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr>
<td>Total</td>
<td class="num"><?= number_format($total_visits) ?></td>
<?php foreach ($ordered as $p): ?>
<td class="num"><?= number_format($profile_totals[$p]) ?></td>
<?php endforeach; ?>
</tr>
</tfoot>
</table>
</div>
<?php endif; ?>
<div class="back"><a href="/">← Back to SunScope</a></div>
</body>
</html>
+72
View File
@@ -0,0 +1,72 @@
<?php
// ─── SunScope — Rudimentary tracking ─────────────────────────────────────────
// Accepts POST { event: "visit" | "profile", profile?: string }
// Writes daily tallies to data/tracking.json as { "YYYY-MM-DD": { visits, profiles: { key: n } } }
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
exit;
}
$body = json_decode(file_get_contents('php://input'), true);
$event = $body['event'] ?? '';
$profile = $body['profile'] ?? '';
if (!in_array($event, ['visit', 'profile'], true)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid event']);
exit;
}
if ($event === 'profile' && $profile === '') {
http_response_code(400);
echo json_encode(['error' => 'Missing profile']);
exit;
}
$data_dir = __DIR__ . '/data';
$data_file = $data_dir . '/tracking.json';
if (!is_dir($data_dir)) {
mkdir($data_dir, 0755, true);
// Block direct web access to the data folder
file_put_contents($data_dir . '/.htaccess', "Deny from all\n");
}
$fp = fopen($data_file, 'c+');
if (!$fp) {
http_response_code(500);
echo json_encode(['error' => 'Could not open tracking file']);
exit;
}
flock($fp, LOCK_EX);
$size = fstat($fp)['size'];
$tracking = [];
if ($size > 0) {
$tracking = json_decode(fread($fp, $size), true) ?? [];
}
$day = date('Y-m-d');
if (!isset($tracking[$day])) {
$tracking[$day] = ['visits' => 0, 'profiles' => []];
}
if ($event === 'visit') {
$tracking[$day]['visits']++;
} else {
$tracking[$day]['profiles'][$profile] = ($tracking[$day]['profiles'][$profile] ?? 0) + 1;
}
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, json_encode($tracking, JSON_PRETTY_PRINT));
flock($fp, LOCK_UN);
fclose($fp);
echo json_encode(['ok' => true]);