Restructure
New Journal entries
Colour tabs update with wind
This commit is contained in:
fraxle
2026-08-31 15:12:45 +01:00
parent 47818fba33
commit 71860b9dd9
40 changed files with 3430 additions and 443 deletions
+18
View File
@@ -0,0 +1,18 @@
<?php
// POST { id } -> deletes an entry the caller owns.
require __DIR__ . '/../lib/bootstrap.php';
require_method('POST');
$user = require_user();
$body = json_body();
$id = (int)($body['id'] ?? 0);
if ($id <= 0) json_error('Invalid id.');
$stmt = db()->prepare('DELETE FROM journal_entries WHERE id = ? AND user_id = ?');
$stmt->execute([$id, $user['id']]);
if ($stmt->rowCount() === 0) {
json_error('Entry not found.', 404);
}
json_out(['ok' => true]);
+29
View File
@@ -0,0 +1,29 @@
<?php
// GET -> { ok:true, entries: [{ id, date, endDate, note, profile, solarEnv, reading, locationName, lat, lon, createdAt }, ...] }, newest first.
require __DIR__ . '/../lib/bootstrap.php';
$user = require_user();
$stmt = db()->prepare(
'SELECT id, entry_date, end_date, note, profile, solar_env, reading, location_name, lat, lon, created_at
FROM journal_entries
WHERE user_id = ?
ORDER BY entry_date DESC, id DESC'
);
$stmt->execute([$user['id']]);
$entries = array_map(fn($r) => [
'id' => (int)$r['id'],
'date' => $r['entry_date'],
'endDate' => $r['end_date'],
'note' => $r['note'],
'profile' => $r['profile'],
'solarEnv' => $r['solar_env'],
'reading' => $r['reading'] !== null ? (float)$r['reading'] : null,
'locationName' => $r['location_name'],
'lat' => $r['lat'] !== null ? (float)$r['lat'] : null,
'lon' => $r['lon'] !== null ? (float)$r['lon'] : null,
'createdAt' => $r['created_at'],
], $stmt->fetchAll());
json_out(['ok' => true, 'entries' => $entries]);
+102
View File
@@ -0,0 +1,102 @@
<?php
// POST { date, endDate?, note, id?, profile?, solarEnv?, reading?, locationName?, lat?, lon? }
// -> creates a new entry, or updates one the caller owns when id is given.
// { ok:true, id }.
//
// endDate is optional - a note can span a period (e.g. a holiday week)
// rather than a single day. Omitted/equal-to-date means a single-day entry.
//
// profile/solarEnv/reading/locationName/lat/lon are an optional attached
// "reading" - a computed felt-temperature number the browser derives
// client-side from the live 14-day forecast (same compute.js the main app
// uses, same peak-of-day rule the day tabs use, taken across the whole
// range for a multi-day entry). The server never touches the physics; it
// just stores whatever number the browser already computed, the same way
// it stores the note text. profile AND solarEnv are both stored because
// both change the number - the dashboard filters its entry list and graph
// down to whichever combination is currently selected, so a Desert reading
// never turns up mixed into a Beach trend.
require __DIR__ . '/../lib/bootstrap.php';
require_method('POST');
$user = require_user();
$body = json_body();
$date = (string)($body['date'] ?? '');
$note = trim((string)($body['note'] ?? ''));
$id = isset($body['id']) ? (int)$body['id'] : null;
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
json_error('Invalid date.');
}
$endDate = null;
if (isset($body['endDate']) && $body['endDate'] !== null && $body['endDate'] !== '') {
$endDate = (string)$body['endDate'];
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
json_error('Invalid end date.');
}
if ($endDate < $date) json_error('End date must be on or after the start date.');
if ($endDate === $date) $endDate = null; // same day - store as a single-day entry
}
if ($note === '') {
json_error('Note cannot be empty.');
}
if (strlen($note) > 4000) {
json_error('Note is too long.');
}
$profile = null;
$solarEnv = null;
$reading = null;
$locationName = null;
$lat = null;
$lon = null;
if (isset($body['reading']) && $body['reading'] !== null && $body['reading'] !== '') {
if (!is_numeric($body['reading'])) json_error('Invalid reading.');
$reading = round((float)$body['reading'], 1);
if ($reading < -100 || $reading > 150) json_error('Reading out of range.');
$profile = (string)($body['profile'] ?? '');
if (!in_array($profile, ['basic', 'home', 'vehicle', 'pets'], true)) {
json_error('Invalid profile.');
}
$solarEnv = (string)($body['solarEnv'] ?? '');
if (!in_array($solarEnv, ['open', 'urban', 'beach', 'river', 'forest', 'openwater', 'alpine', 'desert'], true)) {
json_error('Invalid solar model.');
}
$locationName = trim((string)($body['locationName'] ?? ''));
if ($locationName === '' || strlen($locationName) > 255) json_error('Invalid location.');
if (!is_numeric($body['lat'] ?? null) || !is_numeric($body['lon'] ?? null)) {
json_error('Invalid coordinates.');
}
$lat = round((float)$body['lat'], 5);
$lon = round((float)$body['lon'], 5);
if ($lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) json_error('Invalid coordinates.');
}
if ($id) {
// Ownership enforced in the WHERE clause, not a prior SELECT.
$stmt = db()->prepare(
'UPDATE journal_entries
SET entry_date = ?, end_date = ?, note = ?, profile = ?, solar_env = ?, reading = ?, location_name = ?, lat = ?, lon = ?
WHERE id = ? AND user_id = ?'
);
$stmt->execute([$date, $endDate, $note, $profile, $solarEnv, $reading, $locationName, $lat, $lon, $id, $user['id']]);
if ($stmt->rowCount() === 0) {
json_error('Entry not found.', 404);
}
json_out(['ok' => true, 'id' => $id]);
}
$stmt = db()->prepare(
'INSERT INTO journal_entries (user_id, entry_date, end_date, note, profile, solar_env, reading, location_name, lat, lon)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
);
$stmt->execute([$user['id'], $date, $endDate, $note, $profile, $solarEnv, $reading, $locationName, $lat, $lon]);
json_out(['ok' => true, 'id' => (int)db()->lastInsertId()]);
+21
View File
@@ -0,0 +1,21 @@
<?php
// POST { email, password } -> signs the user in.
require __DIR__ . '/../lib/bootstrap.php';
require_method('POST');
$body = json_body();
$email = strtolower(trim($body['email'] ?? ''));
$password = (string)($body['password'] ?? '');
$stmt = db()->prepare('SELECT id, password_hash FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();
// Same message whether the email is unknown or the password is wrong -
// don't let a login form confirm which emails have accounts.
if (!$user || !password_verify($password, $user['password_hash'])) {
json_error('Incorrect email or password.', 401);
}
issue_session((int)$user['id']);
json_out(['ok' => true, 'email' => $email]);
+6
View File
@@ -0,0 +1,6 @@
<?php
require __DIR__ . '/../lib/bootstrap.php';
require_method('POST');
clear_session();
json_out(['ok' => true]);
+7
View File
@@ -0,0 +1,7 @@
<?php
// GET -> { ok:true, user: { email } } if signed in, else { ok:true, user:null }.
// Always 200 - this is a status check, not an auth gate.
require __DIR__ . '/../lib/bootstrap.php';
$user = current_user();
json_out(['ok' => true, 'user' => $user ? ['email' => $user['email']] : null]);
+30
View File
@@ -0,0 +1,30 @@
<?php
// POST { email, password } -> creates an account, signs the user in.
require __DIR__ . '/../lib/bootstrap.php';
require_method('POST');
$body = json_body();
$email = strtolower(trim($body['email'] ?? ''));
$password = (string)($body['password'] ?? '');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
json_error('Enter a valid email address.');
}
if (strlen($password) < 8) {
json_error('Password must be at least 8 characters.');
}
$stmt = db()->prepare('SELECT id FROM users WHERE email = ?');
$stmt->execute([$email]);
if ($stmt->fetch()) {
json_error('An account with that email already exists.', 409);
}
$hash = password_hash($password, PASSWORD_ARGON2ID);
$stmt = db()->prepare(
'INSERT INTO users (public_id, email, password_hash) VALUES (?, ?, ?)'
);
$stmt->execute([new_public_id(), $email, $hash]);
issue_session((int)db()->lastInsertId());
json_out(['ok' => true, 'email' => $email]);