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.
74 lines
2.9 KiB
Ruby
74 lines
2.9 KiB
Ruby
# 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 (Po–Pá 8–16). Zavolejte prosím zítra v 8:00. Děkujeme."
|
||
end
|
||
end
|