feat: temperature unit preference and slot timezone display for briefing

- Add °C/°F toggle in briefing settings; persisted in briefing_config.temp_unit
- briefing_pipeline reads temp_unit and converts Open-Meteo Celsius values
  before passing them to the LLM (both full compilation and slot injection)
- Scheduled Slots section now shows each UTC slot time converted to the
  user's browser local time, plus a line confirming which timezone the
  browser is using

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 08:29:40 -04:00
parent d2605287f7
commit 6e57ce4555
3 changed files with 101 additions and 13 deletions
+2
View File
@@ -328,6 +328,7 @@ export interface BriefingConfig {
work_days: number[];
slots: BriefingSlots;
notifications: boolean;
temp_unit: 'C' | 'F';
}
export interface BriefingFeed {
@@ -360,6 +361,7 @@ const DEFAULT_BRIEFING_CONFIG: BriefingConfig = {
work_days: [1, 2, 3, 4, 5],
slots: { compilation: true, morning: true, midday: false, afternoon: false },
notifications: true,
temp_unit: 'C',
};
export async function getBriefingConfig(): Promise<BriefingConfig> {
+67 -7
View File
@@ -173,6 +173,18 @@ function toggleWorkDay(day: number) {
days.sort();
}
/** Convert a UTC hour (023) to the browser's local time string, e.g. "6:00 am (UTC 4:00)" */
function utcSlotToLocal(utcHour: number): string {
const now = new Date();
const utcMs = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), utcHour, 0, 0);
const local = new Date(utcMs);
const localStr = local.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
const utcStr = utcHour === 0 ? '12:00 am' : utcHour < 12
? `${utcHour}:00 am`
: utcHour === 12 ? '12:00 pm' : `${utcHour - 12}:00 pm`;
return `${localStr} (UTC ${utcStr})`;
}
async function saveBriefingSettings() {
briefingSaving.value = true;
briefingSaved.value = false;
@@ -1316,6 +1328,22 @@ function formatUserDate(iso: string): string {
</label>
<p class="field-hint">Look up weather for locations in today's calendar events.</p>
</div>
<div class="field" style="margin-top: 1rem">
<label class="field-label">Temperature unit</label>
<div class="briefing-unit-toggle">
<button
type="button"
:class="['briefing-unit-btn', { active: briefingConfig.temp_unit === 'C' }]"
@click="briefingConfig.temp_unit = 'C'"
>°C</button>
<button
type="button"
:class="['briefing-unit-btn', { active: briefingConfig.temp_unit === 'F' }]"
@click="briefingConfig.temp_unit = 'F'"
>°F</button>
</div>
</div>
</section>
<!-- Work schedule -->
@@ -1336,27 +1364,33 @@ function formatUserDate(iso: string): string {
<!-- Slots -->
<section class="settings-section full-width">
<h2>Scheduled Slots</h2>
<p class="section-desc">Each active slot will post an update into your briefing conversation.</p>
<p class="section-desc">
Each active slot posts an update into your briefing conversation.
Slots run on the server in UTC — your local equivalent is shown below.
</p>
<div class="briefing-slot-list">
<div
v-for="[key, label, time] in ([
['compilation', 'Morning briefing', '4:00 am'],
['morning', 'Office check-in', '8:00 am'],
['midday', 'Midday update', '12:00 pm'],
['afternoon', 'End of day', '4:00 pm'],
v-for="[key, label, utcHour] in ([
['compilation', 'Morning briefing', 4],
['morning', 'Office check-in', 8],
['midday', 'Midday update', 12],
['afternoon', 'End of day', 16],
] as const)"
:key="key"
class="briefing-slot-row"
>
<div class="briefing-slot-info">
<span class="briefing-slot-label">{{ label }}</span>
<span class="briefing-slot-time">{{ time }}</span>
<span class="briefing-slot-time">{{ utcSlotToLocal(utcHour) }}</span>
</div>
<label>
<input type="checkbox" v-model="briefingConfig.slots[key]" />
</label>
</div>
</div>
<p class="field-hint" style="margin-top: 0.5rem">
Your browser's local timezone: <strong>{{ Intl.DateTimeFormat().resolvedOptions().timeZone }}</strong>
</p>
</section>
<!-- RSS Feeds -->
@@ -2697,6 +2731,32 @@ function formatUserDate(iso: string): string {
flex-wrap: wrap;
margin-top: 0.5rem;
}
.briefing-unit-toggle {
display: flex;
gap: 0;
border: 1px solid var(--color-border);
border-radius: 8px;
overflow: hidden;
width: fit-content;
margin-top: 0.25rem;
}
.briefing-unit-btn {
padding: 0.35rem 1rem;
background: var(--color-bg-card);
color: var(--color-text-muted);
font-size: 0.85rem;
cursor: pointer;
border: none;
font-family: inherit;
transition: all 0.15s;
}
.briefing-unit-btn:first-child {
border-right: 1px solid var(--color-border);
}
.briefing-unit-btn.active {
background: var(--color-primary);
color: #fff;
}
.briefing-day-btn {
padding: 0.35rem 0.65rem;
border: 1px solid var(--color-border);