From d8599657f4a911fb7eec259a41c9b6d94ebfc169 Mon Sep 17 00:00:00 2001 From: db123 Date: Wed, 24 Jun 2026 11:21:54 +0330 Subject: [PATCH] refactor: export full Gregorian range, add timezone fallback, cleanup rate limit, configurable schema path - GenerateMonthlyReport now takes explicit Gregorian start/end range, computed via monthGregorianRange, so Jalali/Hijri months spanning two Gregorian months are fully covered in exports. - Added loadLocation helper (UTC fallback on error) and replaced all 11 ignored-error call sites in handlers + export. - Added periodicCleanup goroutine in NewHandler to prevent unbounded growth of the rateLimit map. - schemaPath is now read from SCHEMA_PATH env var (init), defaulting to /app/db/schema.sql. - Migrated sendDayView to use sendOrEdit helper, removing duplicated send/edit branching. - Removed dead code: userYearMonthToGregorian (unused), the old startOffset function (replaced). - Updated README with calendar docs, new env vars, full project tree. - Updated .env.example and docker-compose.yml with SCHEMA_PATH. - Simplified Makefile: added fmt, tidy, check, run-dev targets; removed broken Make-glob prerequisite pattern. --- .env.example | 1 + Makefile | 24 +++++--- README.md | 109 +++++++++++++++++++++++++---------- docker-compose.yml | 1 + internal/bot/dateutil.go | 28 +++++---- internal/bot/export.go | 24 ++------ internal/bot/handlers.go | 119 +++++++++++++++++++++++---------------- internal/db/store.go | 9 ++- 8 files changed, 193 insertions(+), 122 deletions(-) diff --git a/.env.example b/.env.example index a2bf9c5..d410649 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,7 @@ BOT_TOKEN=YOUR_TELEGRAM_BOT_TOKEN HTTP_PROXY= HTTPS_PROXY= DB_PATH=db.sqlite3 +SCHEMA_PATH= BOT_WEBHOOK_URL= BOT_LISTEN=:8080 BOT_TLS_CERT= diff --git a/Makefile b/Makefile index 3bbc709..9e4eab9 100644 --- a/Makefile +++ b/Makefile @@ -1,24 +1,33 @@ -.PHONY: build run test clean docker docker-run lint vet - BINARY=worktime-bot BUILD_DIR=.build -build: $(BUILD_DIR)/$(BINARY) +.PHONY: all build run test clean fmt tidy vet check docker docker-run -$(BUILD_DIR)/$(BINARY): cmd/bot/*.go internal/**/*.go go.mod go.sum - mkdir -p $(BUILD_DIR) +all: build test check + +build: go build -o $(BUILD_DIR)/$(BINARY) ./cmd/bot/ run: build $(BUILD_DIR)/$(BINARY) +run-dev: + go run ./cmd/bot/ + test: go test ./... -lint: vet +fmt: + go fmt ./... + +tidy: + go mod tidy + vet: go vet ./... +check: fmt vet + clean: rm -rf $(BUILD_DIR) @@ -27,6 +36,3 @@ docker: docker-run: docker docker compose up -d - -.PHONY: all -all: build test lint diff --git a/README.md b/README.md index b87117f..ecd1db7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # WorkTime Bot -A single-user Telegram time-tracking bot. Clock in/out, toggle remote/onsite, mark day off, get daily reports, and export monthly Excel reports. +A single-user Telegram time-tracking bot with **multi-calendar support** (Gregorian, Jalali/Persian, Hijri). Clock in/out, toggle remote/onsite, mark day off, get daily reports at a configurable time, browse history by month, and export monthly `.xlsx` reports. ## Quick Start @@ -10,43 +10,69 @@ cp .env.example .env docker compose up -d ``` -### Environment Variables +## Environment Variables -| Variable | Purpose | -| ------------------- | ----------------------------------------------------------- | -| `BOT_TOKEN` | Telegram Bot API token (required) | -| `BOT_ALLOWED_USERS` | Comma-separated Telegram user IDs (empty = allow all) | -| `HTTPS_PROXY` | Outbound proxy for Telegram API (e.g. `http://proxy:10808`) | -| `DB_PATH` | Path to SQLite database (default: `db.sqlite3`) | -| `BOT_WEBHOOK_URL` | Set for webhook mode (omit for polling) | +| Variable | Default | Purpose | +| ------------------- | -------------------- | ----------------------------------------------------- | +| `BOT_TOKEN` | — | Telegram Bot API token **(required)** | +| `BOT_ALLOWED_USERS` | (all) | Comma-separated Telegram user IDs (empty = allow all) | +| `HTTP_PROXY` | — | Outbound HTTP proxy for Telegram API | +| `HTTPS_PROXY` | — | Outbound HTTPS proxy for Telegram API | +| `DB_PATH` | `db.sqlite3` | Path to SQLite database file | +| `SCHEMA_PATH` | `/app/db/schema.sql` | Path to SQL schema file | +| `BOT_WEBHOOK_URL` | — | Set for webhook mode (omit for long-polling) | +| `BOT_LISTEN` | `:8080` | Listen address for webhook mode | +| `BOT_TLS_CERT` | — | TLS certificate path (webhook HTTPS) | +| `BOT_TLS_KEY` | — | TLS key path (webhook HTTPS) | + +## Calendar Support + +Users can choose between three calendars via `/settings`: + +- **Gregorian** — standard international calendar +- **Jalali** (Persian/Solar Hijri) — official calendar of Iran and Afghanistan +- **Hijri** (Islamic lunar) — used in many Muslim-majority countries + +The history view, export month picker, and date displays all adapt to the user's chosen calendar. The `/export` and `/edit` commands accept dates in the user's calendar. Internally, all dates are stored as Gregorian. ## Usage -**Persistent reply keyboard** at the bottom: `Clock In` / `Clock Out` +**Persistent reply keyboard** at the bottom of the chat: `Clock In` / `Clock Out` **Inline menu** on `/start`: -- Clock In / Clock Out -- Report / Export -- Toggle Remote/Onsite / Day Off +| Button | Action | +| -------------------- | --------------------------------- | +| Clock In / Clock Out | Record work event | +| Report | Today's work & break summary | +| Export | Pick a month → download `.xlsx` | +| History | Browse past days in calendar view | +| Toggle Remote/Onsite | Switch work type | +| Day Off | Mark today as day off | +| Settings | Timezone, accent, calendar type | ### Commands -| Command | Action | -| -------------------------- | ---------------------------- | -| `/start` | Show inline menu | -| `Clock In` or `/clockin` | Record clock-in | -| `Clock Out` or `/clockout` | Record clock-out | -| `/remote` | Toggle remote/onsite mode | -| `/report` | Today's work & break summary | -| `/export` | Monthly `.xlsx` report | -| `/dayoff` | Toggle today as day off | +| Command | Action | +| --------------------- | ------------------------------------------------------- | +| `/start` | Show inline menu | +| `/clockin` | Record clock-in | +| `/clockout` | Record clock-out | +| `/remote` | Toggle remote/onsite | +| `/report` | Today's work & break summary | +| `/export` | Export this month (optional `YYYY-MM` in your calendar) | +| `/dayoff` | Toggle today as day off | +| `/edit YYYY-MM-DD` | View/edit events for a specific date | +| `/settimezone ` | Set timezone (e.g. `Asia/Tehran`) | +| `/settings` | Open settings menu | -Daily report is sent automatically at 23:00. +## Daily Report + +A daily summary is sent automatically at the user's configured report time (default `23:00`). The scheduler checks every 30 seconds. ## How Break Time Works -The gap between a clock-out and the next clock-in is counted as break: +The gap between a clock-out and the next clock-in is counted as break. Gaps shorter than the **min break threshold** (default 5 minutes) are absorbed into work time. ``` 09:00 Clock In → work starts @@ -56,20 +82,43 @@ The gap between a clock-out and the next clock-in is counted as break: total: 7h work, 1h break ``` +## Export + +The `/export` command opens a month picker. Select any month in your calendar and download an `.xlsx` file with columns: Date, Clock In, Clock Out, Type, Work, Break. + +Four color themes are available in settings: **Ocean**, **Beach**, **Rose**, **Catppuccin**. + ## Project Structure ``` -├── cmd/bot/ main.go, webhook.go +├── cmd/bot/ +│ ├── main.go Entrypoint, polling/webhook, scheduler +│ └── webhook.go Webhook mode with TLS support ├── internal/ -│ ├── bot/ handlers, export, totals -│ └── db/ SQLite store -├── db/schema.sql Database schema +│ ├── bot/ +│ │ ├── handlers.go Message/callback routing, views, menu builders +│ │ ├── totals.go Break/work computation state machine +│ │ ├── export.go Excel (.xlsx) report generation +│ │ └── dateutil.go Gregorian/Jalali/Hijri conversions & calendars +│ └── db/ +│ └── store.go SQLite store with auto-migration +├── db/ +│ └── schema.sql Database schema +├── .gitea/workflows/ +│ └── ci.yml Gitea Actions CI ├── Dockerfile -└── docker-compose.yml +├── docker-compose.yml +├── Makefile +├── .env.example +└── README.md ``` -## Build +## Build & Run ```bash +# Local (requires Go 1.26+ and CGo-free SQLite) +make run + +# Docker docker compose up -d --build ``` diff --git a/docker-compose.yml b/docker-compose.yml index 0f1b259..2d8bac8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: - HTTP_PROXY=${HTTP_PROXY-} - HTTPS_PROXY=${HTTPS_PROXY-} - DB_PATH=/data/db.sqlite3 + - SCHEMA_PATH=${SCHEMA_PATH:-/app/db/schema.sql} - TZ=Asia/Tehran volumes: - worktime_data:/data diff --git a/internal/bot/dateutil.go b/internal/bot/dateutil.go index 2d50d31..1919802 100644 --- a/internal/bot/dateutil.go +++ b/internal/bot/dateutil.go @@ -1,6 +1,9 @@ package bot -import "fmt" +import ( + "fmt" + "time" +) var gregMonthDays = []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} var jalaliMonthDays = []int{31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29} @@ -303,20 +306,6 @@ func userDateToGregorian(dateStr, cal string) string { return fmt.Sprintf("%04d-%02d-%02d", y, m, d) } -// userYearMonthToGregorian converts a (year, month) in the user's calendar -// to the corresponding Gregorian (year, month). The day is assumed to be 1. -func userYearMonthToGregorian(year, month int, cal string) (int, int) { - switch cal { - case "jalali": - gy, gm, _ := jalaliToGregorian(year, month, 1) - return gy, gm - case "hijri": - gy, gm, _ := hijriToGregorian(year, month, 1) - return gy, gm - } - return year, month -} - // monthGregorianRange returns the Gregorian start and end date strings // for a given calendar month (cal=gregorian|jalali|hijri). func monthGregorianRange(cal string, year, month int) (start, end string) { @@ -340,3 +329,12 @@ func monthGregorianRange(cal string, year, month int) (start, end string) { } return } + +// loadLocation loads a timezone, falling back to UTC on error. +func loadLocation(tz string) *time.Location { + loc, err := time.LoadLocation(tz) + if err != nil { + return time.UTC + } + return loc +} diff --git a/internal/bot/export.go b/internal/bot/export.go index 1aaf372..5466779 100644 --- a/internal/bot/export.go +++ b/internal/bot/export.go @@ -22,7 +22,7 @@ var themes = map[string]themeColors{ "catppuccin": {headerFill: "B48DED", border: "D9D0E8", totalFill: "F0E8FC"}, } -func GenerateMonthlyReport(store *db.Store, userID int64, timezone, accent, calendar string, year int, month time.Month) ([]byte, error) { +func GenerateMonthlyReport(store *db.Store, userID int64, timezone, accent, calendar string, calYear, calMonth int, start, end time.Time) ([]byte, error) { theme, ok := themes[accent] if !ok { theme = themes["ocean"] @@ -76,16 +76,7 @@ func GenerateMonthlyReport(store *db.Store, userID int64, timezone, accent, cale Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"}, }) - var jy, jm int - switch calendar { - case "jalali": - jy, jm, _ = gregorianToJalali(year, int(month), 1) - case "hijri": - jy, jm, _ = gregorianToHijri(year, int(month), 1) - default: - jy, jm = year, int(month) - } - monthName := formatMonthTitle(calendar, jy, jm) + monthName := formatMonthTitle(calendar, calYear, calMonth) f.SetCellValue(sheet, "A1", fmt.Sprintf("Work Time Report - %s", monthName)) f.MergeCell(sheet, "A1", "F1") f.SetCellStyle(sheet, "A1", "F1", titleStyle) @@ -106,16 +97,11 @@ func GenerateMonthlyReport(store *db.Store, userID int64, timezone, accent, cale f.SetColWidth(sheet, "E", "E", 10) f.SetColWidth(sheet, "F", "F", 10) - loc, err := time.LoadLocation(timezone) - if err != nil { - loc = time.UTC - } + loc := loadLocation(timezone) row := 3 - firstDay := time.Date(year, month, 1, 0, 0, 0, 0, loc) - lastDay := firstDay.AddDate(0, 1, -1) - userDays, err := store.GetUserDaysInRange(userID, firstDay.Format("2006-01-02"), lastDay.Format("2006-01-02")) + userDays, err := store.GetUserDaysInRange(userID, start.Format("2006-01-02"), end.Format("2006-01-02")) if err != nil { return nil, err } @@ -127,7 +113,7 @@ func GenerateMonthlyReport(store *db.Store, userID int64, timezone, accent, cale var totalWork, totalBreak int64 - for d := firstDay; !d.After(lastDay); d = d.AddDate(0, 0, 1) { + for d := start; !d.After(end); d = d.AddDate(0, 0, 1) { dateStr := d.Format("2006-01-02") displayDate := formatDateForCalendar(dateStr, calendar) diff --git a/internal/bot/handlers.go b/internal/bot/handlers.go index de79b48..c698f26 100644 --- a/internal/bot/handlers.go +++ b/internal/bot/handlers.go @@ -25,12 +25,28 @@ type Handler struct { } func NewHandler(bot *tgbotapi.BotAPI, store *db.Store, allowed map[int64]bool) *Handler { - return &Handler{ + h := &Handler{ Bot: bot, DB: store, AllowedUsers: allowed, lastMsgTime: make(map[int64]time.Time), } + go h.periodicCleanup() + return h +} + +func (h *Handler) periodicCleanup() { + for { + time.Sleep(10 * time.Second) + h.mu.Lock() + cutoff := time.Now().Add(-rateLimitInterval * 2) + for uid, t := range h.lastMsgTime { + if t.Before(cutoff) { + delete(h.lastMsgTime, uid) + } + } + h.mu.Unlock() + } } type actionFunc func(int64) (string, error) @@ -222,7 +238,7 @@ func (h *Handler) clockIn(chatID int64) (string, error) { } if last != nil && last.EventType == "in" { - loc, _ := time.LoadLocation(user.Timezone) + loc := loadLocation(user.Timezone) now := time.Now().In(loc) lastTime := time.Unix(last.OccurredAt, 0).In(loc) if lastTime.Format("2006-01-02") == now.Format("2006-01-02") { @@ -230,7 +246,7 @@ func (h *Handler) clockIn(chatID int64) (string, error) { } } - loc, _ := time.LoadLocation(user.Timezone) + loc := loadLocation(user.Timezone) now := time.Now().In(loc) wt := day.CurrentWorkTypeID if err := h.DB.CreateEvent(user.ID, day.ID, "in", &wt, now.Unix(), ""); err != nil { @@ -254,7 +270,7 @@ func (h *Handler) clockOut(chatID int64) (string, error) { if last.EventType == "out" { return "", errors.New("Already clocked out") } - loc, _ := time.LoadLocation(user.Timezone) + loc := loadLocation(user.Timezone) now := time.Now().In(loc) if err := h.DB.CreateEvent(user.ID, day.ID, "out", nil, now.Unix(), ""); err != nil { return "", err @@ -726,7 +742,7 @@ func (h *Handler) handleEditMsg(msg *tgbotapi.Message) { h.sendText(msg.Chat.ID, "Error loading profile") return } - loc, _ := time.LoadLocation(user.Timezone) + loc := loadLocation(user.Timezone) date = userDateToGregorian(date, user.Calendar) h.sendDayView(msg.Chat.ID, 0, user, date, loc, true) } @@ -737,7 +753,7 @@ func (h *Handler) handleHistoryCallback(chatID int64, msgID int, callbackID, dat if err != nil { return } - loc, _ := time.LoadLocation(user.Timezone) + loc := loadLocation(user.Timezone) if data == "history" { h.editCalendar(chatID, msgID, 0, 0) @@ -816,7 +832,7 @@ func (h *Handler) editCalendar(chatID int64, msgID int, year, month int) { if err != nil { return } - loc, _ := time.LoadLocation(user.Timezone) + loc := loadLocation(user.Timezone) now := time.Now().In(loc) if year == 0 || month == 0 { @@ -919,15 +935,7 @@ func (h *Handler) sendDayView(chatID int64, msgID int, user *db.User, date strin tgbotapi.NewInlineKeyboardButtonData("Back to Calendar", "history"), ), ) - if isNewMsg || msgID == 0 { - reply := tgbotapi.NewMessage(chatID, text) - reply.ReplyMarkup = &kb - h.Bot.Send(reply) - } else { - edit := tgbotapi.NewEditMessageText(chatID, msgID, text) - edit.ReplyMarkup = &kb - h.Bot.Send(edit) - } + h.sendOrEdit(chatID, msgID, text, &kb) return } @@ -963,15 +971,7 @@ func (h *Handler) sendDayView(chatID int64, msgID int, user *db.User, date strin )) kb := tgbotapi.NewInlineKeyboardMarkup(kbRows...) - if isNewMsg || msgID == 0 { - reply := tgbotapi.NewMessage(chatID, text) - reply.ReplyMarkup = &kb - h.Bot.Send(reply) - } else { - edit := tgbotapi.NewEditMessageText(chatID, msgID, text) - edit.ReplyMarkup = &kb - h.Bot.Send(edit) - } + h.sendOrEdit(chatID, msgID, text, &kb) } func (h *Handler) editEventType(chatID int64, msgID int, user *db.User, eventID int64, loc *time.Location) { @@ -1184,25 +1184,32 @@ func (h *Handler) handleExport(msg *tgbotapi.Message) { h.sendText(msg.Chat.ID, "Error loading profile") return } - loc, _ := time.LoadLocation(user.Timezone) + loc := loadLocation(user.Timezone) now := time.Now().In(loc) + // Default to current month in user's calendar + cy, cm := now.Year(), int(now.Month()) + switch user.Calendar { + case "jalali": + cy, cm, _ = gregorianToJalali(cy, cm, now.Day()) + case "hijri": + cy, cm, _ = gregorianToHijri(cy, cm, now.Day()) + } + // Parse optional YYYY-MM argument (in user's calendar) - gy, gm := now.Year(), int(now.Month()) if len(msg.Text) > 7 { - if n, _ := fmt.Sscanf(msg.Text[7:], "%d-%d", &gy, &gm); n == 2 { - if gy < 0 || gm < 1 || gm > 12 { + if n, _ := fmt.Sscanf(msg.Text[7:], "%d-%d", &cy, &cm); n == 2 { + if cy < 0 || cm < 1 || cm > 12 { h.sendText(msg.Chat.ID, "Invalid date. Use: /export YYYY-MM") return } - gy, gm = userYearMonthToGregorian(gy, gm, user.Calendar) } } - h.sendExportDirect(msg.Chat.ID, user, gy, gm) + h.sendExportDirect(msg.Chat.ID, user, cy, cm) } -func (h *Handler) sendExportDirect(chatID int64, user *db.User, gy, gm int) { +func (h *Handler) sendExportDirect(chatID int64, user *db.User, calYear, calMonth int) { // Check if clocked in last, err := h.DB.GetLastEvent(user.ID) if err == nil && last != nil && last.EventType == "in" { @@ -1210,8 +1217,20 @@ func (h *Handler) sendExportDirect(chatID int64, user *db.User, gy, gm int) { return } - slog.Info("export", "user_id", user.ID, "year", gy, "month", gm) - data, err := GenerateMonthlyReport(h.DB, user.ID, user.Timezone, user.ExportAccent, user.Calendar, gy, time.Month(gm)) + loc := loadLocation(user.Timezone) + startStr, endStr := monthGregorianRange(user.Calendar, calYear, calMonth) + start, err := time.Parse("2006-01-02", startStr) + if err != nil { + start = time.Date(calYear, time.Month(calMonth), 1, 0, 0, 0, 0, loc) + } + end, err := time.Parse("2006-01-02", endStr) + if err != nil { + startOfMonth := time.Date(calYear, time.Month(calMonth), 1, 0, 0, 0, 0, loc) + end = startOfMonth.AddDate(0, 1, -1) + } + + slog.Info("export", "user_id", user.ID, "cal", user.Calendar, "cal_y", calYear, "cal_m", calMonth, "from", startStr, "to", endStr) + data, err := GenerateMonthlyReport(h.DB, user.ID, user.Timezone, user.ExportAccent, user.Calendar, calYear, calMonth, start, end) if err != nil { slog.Error("generate report", "error", err) h.sendText(chatID, "Error generating report") @@ -1219,7 +1238,7 @@ func (h *Handler) sendExportDirect(chatID int64, user *db.User, gy, gm int) { } slog.Info("report generated", "bytes", len(data)) doc := tgbotapi.NewDocument(chatID, tgbotapi.FileBytes{ - Name: fmt.Sprintf("worktime_%s.xlsx", fmt.Sprintf("%04d_%02d", gy, gm)), + Name: fmt.Sprintf("worktime_%s.xlsx", fmt.Sprintf("%04d_%02d", calYear, calMonth)), Bytes: data, }) if _, err := h.Bot.Send(doc); err != nil { @@ -1233,7 +1252,7 @@ func (h *Handler) exportCallback(chatID int64, msgID int, callbackID string) { if err != nil { return } - loc, _ := time.LoadLocation(user.Timezone) + loc := loadLocation(user.Timezone) now := time.Now().In(loc) cy, cm := now.Year(), int(now.Month()) switch user.Calendar { @@ -1313,16 +1332,20 @@ func (h *Handler) handleExportCallback(chatID int64, msgID int, callbackID, data h.Bot.Send(edit) return } - // Convert from user's calendar to Gregorian for export - gy, gm := y, m - switch user.Calendar { - case "jalali": - gy, gm, _ = jalaliToGregorian(y, m, 1) - case "hijri": - gy, gm, _ = hijriToGregorian(y, m, 1) + // Convert from user's calendar to full Gregorian range + startStr, endStr := monthGregorianRange(user.Calendar, y, m) + start, err := time.Parse("2006-01-02", startStr) + if err != nil { + h.sendText(chatID, "Error processing date") + return } - slog.Info("export", "user_id", user.ID, "year", gy, "month", gm, "cal", user.Calendar, "cal_y", y, "cal_m", m) - data, err := GenerateMonthlyReport(h.DB, user.ID, user.Timezone, user.ExportAccent, user.Calendar, gy, time.Month(gm)) + end, err := time.Parse("2006-01-02", endStr) + if err != nil { + h.sendText(chatID, "Error processing date") + return + } + slog.Info("export", "user_id", user.ID, "cal", user.Calendar, "cal_y", y, "cal_m", m, "from", startStr, "to", endStr) + data, err := GenerateMonthlyReport(h.DB, user.ID, user.Timezone, user.ExportAccent, user.Calendar, y, m, start, end) if err != nil { slog.Error("generate report", "error", err) edit := tgbotapi.NewEditMessageText(chatID, msgID, "Error generating report") @@ -1337,7 +1360,7 @@ func (h *Handler) handleExportCallback(chatID int64, msgID int, callbackID, data edit.ReplyMarkup = &kb h.Bot.Send(edit) doc := tgbotapi.NewDocument(chatID, tgbotapi.FileBytes{ - Name: fmt.Sprintf("worktime_%s.xlsx", fmt.Sprintf("%04d_%02d", gy, gm)), + Name: fmt.Sprintf("worktime_%s.xlsx", fmt.Sprintf("%04d_%02d", y, m)), Bytes: data, }) if _, err := h.Bot.Send(doc); err != nil { @@ -1367,7 +1390,7 @@ func (h *Handler) buildStatusText(chatID int64) string { last, err := h.DB.GetLastEvent(user.ID) if err == nil && last != nil && last.EventType == "in" { - loc, _ := time.LoadLocation(user.Timezone) + loc := loadLocation(user.Timezone) t := time.Unix(last.OccurredAt, 0).In(loc).Format("15:04") text += fmt.Sprintf("\nStatus: working since %s", t) } else { @@ -1376,7 +1399,7 @@ func (h *Handler) buildStatusText(chatID int64) string { events, err := h.DB.EventsForDayByDayID(day.ID) if err == nil && len(events) > 0 { - loc, _ := time.LoadLocation(user.Timezone) + loc := loadLocation(user.Timezone) now := time.Now().In(loc) y, m, d := now.Date() dayStart := time.Date(y, m, d, 0, 0, 0, 0, loc).Unix() diff --git a/internal/db/store.go b/internal/db/store.go index 57316f2..6fe909f 100644 --- a/internal/db/store.go +++ b/internal/db/store.go @@ -11,7 +11,14 @@ import ( const driverName = "sqlite" -var schemaPath = "/app/db/schema.sql" +var schemaPath string + +func init() { + schemaPath = os.Getenv("SCHEMA_PATH") + if schemaPath == "" { + schemaPath = "/app/db/schema.sql" + } +} type Store struct { db *sql.DB