From b536281d12930e30a9e84fe16e2f4ffe500f83ef Mon Sep 17 00:00:00 2001 From: irvin <7606712+Osyna@users.noreply.github.com> Date: Mon, 10 Aug 2026 01:17:28 +0200 Subject: [PATCH 1/3] Automated off-site DB backups: daily cron -> private pinna-db-backups repo --- docs/BACKUPS.md | 9 +++++++++ scripts/db-backup-push.sh | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100755 scripts/db-backup-push.sh diff --git a/docs/BACKUPS.md b/docs/BACKUPS.md index 009837f..1a4011b 100644 --- a/docs/BACKUPS.md +++ b/docs/BACKUPS.md @@ -30,3 +30,12 @@ gunzip -c backups/pinna-XXXX.sql.gz | psql "$DATABASE_URL" - Never point `prisma migrate diff/dev --shadow-database-url` at a real database — Prisma **resets** shadow databases. Use a throwaway local container instead. - Take a manual backup before any schema change: `./scripts/db-backup.sh`. + +## Current automated setup (active since 2026-02) + +- `scripts/db-backup-push.sh` runs **daily at 03:00** via cron on the dev machine: + dump → `backups/` (local, keeps 30) → `~/Backups/pinna` → pushed to the + **private** repo `Osyna/pinna-db-backups` (keeps 60 snapshots off-site). +- Verified restore path: `gunzip -c pinna-*.sql.gz | psql "$DATABASE_URL"`. +- Recommended upgrade when available: S3 destination in Dokploy for + server-side scheduled backups independent of the dev machine. diff --git a/scripts/db-backup-push.sh b/scripts/db-backup-push.sh new file mode 100755 index 0000000..79cbce3 --- /dev/null +++ b/scripts/db-backup-push.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Dump the Pinna DB and push the snapshot to the PRIVATE off-site repo +# (github.com/Osyna/pinna-db-backups). Intended for cron: +# 0 3 * * * /home/irvin/Projects/Pinna/scripts/db-backup-push.sh >> /home/irvin/Backups/pinna/cron.log 2>&1 +set -euo pipefail +cd "$(dirname "$0")/.." + +./scripts/db-backup.sh + +DEST="$HOME/Backups/pinna" +mkdir -p "$DEST" +cp backups/pinna-*.sql.gz "$DEST"/ 2>/dev/null || true + +cd "$DEST" +# keep the 60 newest off-site +ls -1t pinna-*.sql.gz 2>/dev/null | tail -n +61 | xargs -r rm -- +git add -A +git -c user.name=pinna-backup -c user.email=7606712+Osyna@users.noreply.github.com \ + commit -q -m "DB backup $(date +%F-%H%M)" || exit 0 # nothing new +git push -q origin main +echo "$(date -Is) backup pushed" From c3995a21b8dbb4ae1d4046793505cb0e9a19be0d Mon Sep 17 00:00:00 2001 From: irvin <7606712+Osyna@users.noreply.github.com> Date: Mon, 10 Aug 2026 01:23:34 +0200 Subject: [PATCH 2/3] Nearby search results: typed icon markers with dotted 'not saved' ring - Nearby OSM results render as the same white icon discs as saved places (amenity -> icon+color mapping: restaurant/bar/pub/cafe/ fast-food/bakery/nightclub/ice-cream...), but with a DOTTED ring in the type color to signal 'found nearby, not saved yet' - drawMarkerImage gains a dashed option; images generated lazily via styleimagemissing (nearby-marker| prefix), sized 30px vs 34px saved - nearby-circles layer switched circle -> symbol (same id, handlers untouched); stale circle-color updater removed - Nearby popup shows a dashed round icon chip in the type color instead of the amber dot --- src/categoryIcons.js | 45 +++++++++++++++++++++++++++++++++----- src/components/MapView.vue | 39 ++++++++++++++++++--------------- src/styles/main.scss | 7 ++++++ 3 files changed, 68 insertions(+), 23 deletions(-) diff --git a/src/categoryIcons.js b/src/categoryIcons.js index 59bb499..837b931 100644 --- a/src/categoryIcons.js +++ b/src/categoryIcons.js @@ -32,6 +32,30 @@ export function categoryIconSvg(iconName, { size = 15, strokeWidth = 2.6 } = {}) return `` } +/* OSM amenity -> icon + color (same palette as saved categories) */ +export const AMENITY_STYLE = { + restaurant: { icon: 'utensils', color: '#FF3B30' }, + bar: { icon: 'glass', color: '#AF52DE' }, + pub: { icon: 'glass', color: '#AF52DE' }, + biergarten: { icon: 'glass', color: '#AF52DE' }, + cafe: { icon: 'coffee', color: '#FF9500' }, + fast_food: { icon: 'burger', color: '#FFCC00' }, + food_court: { icon: 'burger', color: '#FFCC00' }, + ice_cream: { icon: 'brunch', color: '#FF2D55' }, + bakery: { icon: 'bread', color: '#A0522D' }, + nightclub: { icon: 'music', color: '#5856D6' }, +} + +export function amenityStyle(amenity) { + return AMENITY_STYLE[amenity] || { icon: 'pin', color: '#8E8E93' } +} + +/* MapLibre image id for an unsaved nearby place (dashed ring) */ +export function nearbyImageId(amenity) { + const { icon, color } = amenityStyle(amenity) + return `nearby-marker|${icon}|${color}` +} + /* Stable MapLibre image id for a category (icon + color are both encoded, so custom categories that share an icon but differ in color still get their own image). */ @@ -39,9 +63,11 @@ export function markerImageId(cat) { return `cat-marker|${cat?.icon || 'pin'}|${cat?.color || '#8E8E93'}` } -/* Draw a cartoon map marker: white disc, ink outline, colored icon. +/* Draw a cartoon map marker: white disc, soft ring, colored icon. + `dashed: true` renders the ring dotted — used for places that are + found nearby but NOT saved yet. Returns ImageData + pixelRatio, ready for map.addImage(). */ -export function drawMarkerImage(iconName, color, { size = 34, pixelRatio = 2 } = {}) { +export function drawMarkerImage(iconName, color, { size = 34, pixelRatio = 2, dashed = false } = {}) { const px = size * pixelRatio const canvas = document.createElement('canvas') canvas.width = px @@ -60,9 +86,18 @@ export function drawMarkerImage(iconName, color, { size = 34, pixelRatio = 2 } = ctx.shadowColor = 'transparent' ctx.shadowBlur = 0 ctx.shadowOffsetY = 0 - ctx.lineWidth = 1.6 * pixelRatio - ctx.strokeStyle = 'rgba(46, 33, 64, 0.30)' - ctx.stroke() + if (dashed) { + // dotted ring in the type color: "found nearby, not saved yet" + ctx.setLineDash([3.2 * pixelRatio, 2.6 * pixelRatio]) + ctx.lineWidth = 1.9 * pixelRatio + ctx.strokeStyle = color + ctx.stroke() + ctx.setLineDash([]) + } else { + ctx.lineWidth = 1.6 * pixelRatio + ctx.strokeStyle = 'rgba(46, 33, 64, 0.30)' + ctx.stroke() + } // Icon, centered (24-unit viewBox) const span = size * 0.56 * pixelRatio diff --git a/src/components/MapView.vue b/src/components/MapView.vue index 6d39bdb..63ce840 100644 --- a/src/components/MapView.vue +++ b/src/components/MapView.vue @@ -13,7 +13,7 @@ import { useUserLocation } from '../composables/useUserLocation' import { useTheme } from '../composables/useTheme' import { hapticTap } from '../composables/useHaptics' import { ACCENT } from '../theme' -import { iconPathFor, categoryIconSvg, markerImageId, drawMarkerImage } from '../categoryIcons' +import { iconPathFor, categoryIconSvg, markerImageId, drawMarkerImage, amenityStyle, nearbyImageId } from '../categoryIcons' const props = defineProps({ splashFinished: Boolean @@ -275,19 +275,18 @@ function setupCustomLayers() { paint: { 'text-color': '#ffffff' }, }) - // Nearby individual circles (exclude clusters) + // Nearby individual markers (exclude clusters): dashed-ring icon discs — + // same type colors as saved places, dotted outline = "not saved yet" map.addLayer({ id: 'nearby-circles', - type: 'circle', + type: 'symbol', source: 'nearby', filter: ['!', ['has', 'point_count']], - paint: { - 'circle-radius': 6, - 'circle-color': nearbyColor, - 'circle-stroke-width': 1.5, - 'circle-stroke-color': 'rgba(255, 255, 255, 0.7)', - 'circle-opacity': 0.8, - 'circle-stroke-opacity': 0.8, + layout: { + 'icon-image': ['get', 'iconId'], + 'icon-size': ['interpolate', ['linear'], ['zoom'], 10, 0.8, 15, 1], + 'icon-allow-overlap': true, + 'icon-ignore-placement': true, }, }) @@ -443,7 +442,7 @@ function setupClickHandlers() { const html = `