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()]);