Prd/components/backend/lib/helpdesk/business_hours.rb
Lucy Doupalů be9f14ce34 Helpdesk - operator console + patched GrapheneOS Dialer for call handling
A small helpdesk system: an office Pixel running a patched GrapheneOS Dialer
answers technician calls, records both call legs as separate channels, and a
Ruby backend transcribes them through Whisper and files an AI summary against
the caller.

Squashed to a single commit for sharing. No credentials are included; secrets
live outside the repo in /etc/helpdesk/env on the server or a gitignored
.claude/env.local locally. See .claude/env.local.example for the shape.

Start at README.md, then docs/architecture.md.
2026-07-27 18:50:32 +02:00

74 lines
2.9 KiB
Ruby
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# frozen_string_literal: true
# Business-hours decision: Mon-Fri 08:00-16:00 Europe/Prague, excluding Czech public holidays.
# This is the authoritative backend check (the device's util/BusinessHours.java is only a local
# reflex when the network is down). Pure functions over a supplied local Time — deterministic + testable.
# Note: prague_now uses a TZ-env trick for DST; a tz library would be sturdier if this grows.
require "date"
module Helpdesk
module BusinessHours
OPEN_HOUR = 8
CLOSE_HOUR = 16 # closed at 16:00 sharp
module_function
# `local` must be a Time already in Europe/Prague.
def open?(local)
return false if weekend?(local)
return false if holiday?(local.to_date)
local.hour >= OPEN_HOUR && local.hour < CLOSE_HOUR
end
def weekend?(local) = [0, 6].include?(local.wday) # Sun=0, Sat=6
# Czech public holidays (fixed) + movable Good Friday & Easter Monday.
def holiday?(date)
y = date.year
fixed = [[1, 1], [5, 1], [5, 8], [7, 5], [7, 6], [9, 28], [10, 28], [11, 17], [12, 24], [12, 25], [12, 26]]
return true if fixed.include?([date.month, date.day])
easter = easter_date(y)
date == (easter - 2) || date == (easter + 1) # Good Friday, Easter Monday
end
# Anonymous Gregorian algorithm.
def easter_date(y)
a = y % 19; b = y / 100; c = y % 100; d = b / 4; e = b % 4
f = (b + 8) / 25; g = (b - f + 1) / 3
h = (19 * a + b - d - g + 15) % 30
i = c / 4; k = c % 4
l = (32 + 2 * e + 2 * i - h - k) % 7
m = (a + 11 * h + 22 * l) / 451
month = (h + l - 7 * m + 114) / 31
day = ((h + l - 7 * m + 114) % 31) + 1
Date.new(y, month, day)
end
# Prague "now" without a tz gem AND without mutating the process-global ENV["TZ"] (the old
# `ENV["TZ"]=` trick raced across WEBrick request threads: concurrent /status polls could
# permanently leak TZ, and libc setenv isn't thread-safe against getenv in other threads). Instead
# apply the Europe/Prague UTC offset (CET/CEST) directly to a UTC clock — pure, thread-safe.
def prague_now
utc = Time.now.utc
utc.getlocal(prague_offset(utc))
end
# "+02:00" during EU summer time (CEST), else "+01:00" (CET). EU DST runs from 01:00 UTC on the
# last Sunday of March to 01:00 UTC on the last Sunday of October.
def prague_offset(utc)
y = utc.year
dst_start = Time.utc(y, 3, last_sunday(y, 3).day, 1)
dst_end = Time.utc(y, 10, last_sunday(y, 10).day, 1)
(utc >= dst_start && utc < dst_end) ? "+02:00" : "+01:00"
end
def last_sunday(y, month)
d = Date.new(y, month, -1) # last day of the month
d - d.wday # step back to Sunday (Date#wday: Sunday=0)
end
# Out-of-hours SMS text (Czech).
OUT_OF_HOURS_SMS = "Mimo pracovní dobu (PoPá 816). Zavolejte prosím zítra v 8:00. Děkujeme."
end
end