From b0fb9d0c5462eed7832a8452513f31f2b499dba1 Mon Sep 17 00:00:00 2001 From: Cosmo Date: Thu, 23 Apr 2026 06:13:16 +0000 Subject: [PATCH] =?UTF-8?q?fix:=204=20bugs=20=E2=80=94=20MSK=20today=20eve?= =?UTF-8?q?nts,=20settings=20scroll,=20note=20dates,=20persistent=20notes?= =?UTF-8?q?=20volume?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - calendar API: today/week ranges use Moscow time (UTC+3) instead of UTC — previously today events did not appear until 03:00 MSK - settings tab: add -webkit-overflow-scrolling: touch + touchAction pan-y for tablet scroll - NotesTab: add date picker (pinDate) in editor header + date badge in list - home: pinnedNotes now filters by pinDate (today or future), falls back to latest - notes/auth: storage moved from /tmp to /data (falls back to /tmp if /data missing) - deploy workflow: mount /opt/digital-home/smart-home-tablet-data:/data so notes survive redeploys --- .gitea/workflows/deploy.yml | 1 + app/api/auth/route.ts | 3 +- app/api/calendar/route.ts | 17 +++++-- app/api/notes/route.ts | 3 +- app/page.tsx | 15 +++++-- components/NotesTab.tsx | 90 +++++++++++++++++++++++++++---------- 6 files changed, 97 insertions(+), 32 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 99f2133..8121007 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -29,5 +29,6 @@ jobs: --label traefik.http.routers.tablet.tls.certresolver=letsencrypt \ --label traefik.http.services.tablet.loadbalancer.server.port=3000 \ --env-file /opt/digital-home/tablet.env \ + -v /opt/digital-home/smart-home-tablet-data:/data \ smart-home-tablet:latest echo 'Deploy done' diff --git a/app/api/auth/route.ts b/app/api/auth/route.ts index b10d284..e4a37b9 100644 --- a/app/api/auth/route.ts +++ b/app/api/auth/route.ts @@ -4,7 +4,8 @@ import * as fs from 'fs' import * as path from 'path' const SECRET = process.env.APP_SECRET || 'smart-home-default-secret-change-me' -const CONFIG_PATH = '/tmp/tablet-config.json' +const DATA_DIR = fs.existsSync('/data') ? '/data' : '/tmp' +const CONFIG_PATH = `${DATA_DIR}/tablet-config.json` function loadConfig(): { pin: string } { try { diff --git a/app/api/calendar/route.ts b/app/api/calendar/route.ts index 2d6f325..f6c8e3f 100644 --- a/app/api/calendar/route.ts +++ b/app/api/calendar/route.ts @@ -43,12 +43,21 @@ export async function GET(req: Request) { let timeMin: string let timeMax: string + // Moscow boundaries (UTC+3, no DST). 00:00 MSK = 21:00 UTC previous day. + const MSK_OFFSET_MS = 3 * 3600 * 1000 + const mskNow = new Date(now.getTime() + MSK_OFFSET_MS) + const my = mskNow.getUTCFullYear() + const mm = mskNow.getUTCMonth() + const md = mskNow.getUTCDate() + const mskMidnight = (y: number, m: number, d: number) => + new Date(Date.UTC(y, m, d) - MSK_OFFSET_MS).toISOString() + if (range === 'today') { - timeMin = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString() - timeMax = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).toISOString() + timeMin = mskMidnight(my, mm, md) + timeMax = mskMidnight(my, mm, md + 1) } else if (range === 'week') { - timeMin = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString() - timeMax = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 7).toISOString() + timeMin = mskMidnight(my, mm, md) + timeMax = mskMidnight(my, mm, md + 7) } else { // month — support year/month query params const targetYear = parseInt(searchParams.get('year') || String(now.getFullYear())) diff --git a/app/api/notes/route.ts b/app/api/notes/route.ts index 04d08e4..aff85e8 100644 --- a/app/api/notes/route.ts +++ b/app/api/notes/route.ts @@ -2,7 +2,8 @@ export const dynamic = 'force-dynamic' import { NextResponse } from 'next/server' import * as fs from 'fs' -const NOTES_PATH = '/tmp/tablet-notes.json' +const DATA_DIR = fs.existsSync('/data') ? '/data' : '/tmp' +const NOTES_PATH = `${DATA_DIR}/tablet-notes.json` interface Note { id: string diff --git a/app/page.tsx b/app/page.tsx index ab1fd13..804b661 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -394,10 +394,19 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S }) .catch(() => {}) - // Notes + // Notes — pinned (pinDate today or future), fallback to latest fetch('/api/notes') .then(r => r.json()) - .then(d => setPinnedNotes((d.notes || []).slice(0, 3))) + .then(d => { + const all = d.notes || [] + const today = new Date(); today.setHours(0, 0, 0, 0) + const pinned = all.filter((n: any) => { + if (!n.pinDate) return false + const pd = new Date(n.pinDate); pd.setHours(0, 0, 0, 0) + return pd.getTime() >= today.getTime() + }) + setPinnedNotes((pinned.length ? pinned : all).slice(0, 3)) + }) .catch(() => {}) }, []) @@ -650,7 +659,7 @@ function SettingsTab({ city, onCityChange, onLogout, theme, onThemeChange }: { c const currentCity = CITIES.find(c => c.id === city) || CITIES[0] return ( -
+

Настройки

{/* City selector */} diff --git a/components/NotesTab.tsx b/components/NotesTab.tsx index 40fbe7f..4f8b8b5 100644 --- a/components/NotesTab.tsx +++ b/components/NotesTab.tsx @@ -1,6 +1,6 @@ 'use client' import { useState, useEffect, useCallback } from 'react' -import { Plus, X, Trash2, ShoppingCart, FileText, Check, Circle } from 'lucide-react' +import { Plus, X, Trash2, ShoppingCart, FileText, Check, Circle, Calendar as CalendarIcon } from 'lucide-react' interface NoteItem { id: string @@ -15,6 +15,7 @@ interface Note { items?: NoteItem[] text?: string color: string + pinDate: string | null createdAt: string updatedAt: string } @@ -157,6 +158,12 @@ export default function NotesTab() { {note.title}
+ {note.pinDate && ( +
+ + {new Date(note.pinDate).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' })} +
+ )} {note.type === 'shopping' && totalCount > 0 && (
- { - const newTitle = e.target.value - setActiveNote(prev => prev ? { ...prev, title: newTitle } : null) - }} - onBlur={() => updateNote(activeNote.id, { title: activeNote.title })} - style={{ - background: 'transparent', border: 'none', outline: 'none', - fontSize: 18, fontWeight: 700, color: 'var(--text-primary)', - fontFamily: 'inherit', flex: 1, minWidth: 0, - }} - /> - +
+ { + const newTitle = e.target.value + setActiveNote(prev => prev ? { ...prev, title: newTitle } : null) + }} + onBlur={() => updateNote(activeNote.id, { title: activeNote.title })} + style={{ + background: 'transparent', border: 'none', outline: 'none', + fontSize: 18, fontWeight: 700, color: 'var(--text-primary)', + fontFamily: 'inherit', flex: 1, minWidth: 0, + }} + /> + +
+
+ + { + const v = e.target.value || null + setActiveNote(prev => prev ? { ...prev, pinDate: v } : null) + updateNote(activeNote.id, { pinDate: v }) + }} + style={{ + background: 'rgba(255,255,255,0.04)', + border: '1px solid rgba(255,255,255,0.07)', + borderRadius: 10, padding: '6px 10px', + color: 'var(--text-primary)', fontSize: 13, + outline: 'none', fontFamily: 'inherit', + colorScheme: 'dark' as any, + }} + /> + {activeNote.pinDate && ( + + )} +
{/* Content */}