Compare commits

..

1 Commits

Author SHA1 Message Date
81b32ed576 fix: add --host to admin dashboard dev server
Bind Vite to all interfaces so port forwarding works in
Ona environments.

Co-authored-by: Ona <no-reply@ona.com>
2026-03-24 22:10:09 +00:00
10 changed files with 55 additions and 226 deletions

View File

@@ -11,7 +11,7 @@
"dockerfile": "Dockerfile" "dockerfile": "Dockerfile"
}, },
"postCreateCommand": "bun install", "postCreateCommand": "bun install",
"postStartCommand": "./scripts/setup-git.sh && ./scripts/setup-nvim.sh && ./scripts/setup-tailscale.sh", "postStartCommand": "./scripts/setup-git.sh && ./scripts/setup-nvim.sh",
// Features add additional features to your environment. See https://containers.dev/features // Features add additional features to your environment. See https://containers.dev/features
// Beware: features are not supported on all platforms and may have unintended side-effects. // Beware: features are not supported on all platforms and may have unintended side-effects.
"features": { "features": {

View File

@@ -25,7 +25,7 @@ services:
- manual - manual
commands: commands:
start: | start: |
gitpod --context environment environment port open 3000 --name "Aelis Backend" --protocol http gitpod --context environment environment port open 3000 --name "Aelis Backend" --protocol https
cd apps/aelis-backend && bun run dev cd apps/aelis-backend && bun run dev
admin-dashboard: admin-dashboard:
@@ -35,5 +35,5 @@ services:
- manual - manual
commands: commands:
start: | start: |
gitpod --context environment environment port open 5174 --name "Admin Dashboard" --protocol http gitpod --context environment environment port open 5174 --name "Admin Dashboard" --protocol https
cd apps/admin-dashboard && bun run dev --host cd apps/admin-dashboard && bun run dev --host

View File

@@ -46,7 +46,7 @@ export function createLlmClient(config: LlmClientConfig): LlmClient {
type: "json_schema" as const, type: "json_schema" as const,
jsonSchema: { jsonSchema: {
name: "enhancement_result", name: "enhancement_result",
strict: false, strict: true,
schema: enhancementResultJsonSchema, schema: enhancementResultJsonSchema,
}, },
}, },

View File

@@ -166,12 +166,11 @@ describe("schema sync", () => {
expect(parseEnhancementResult(JSON.stringify(bad))).toBeNull() expect(parseEnhancementResult(JSON.stringify(bad))).toBeNull()
// JSON Schema only allows string or null for slot values // JSON Schema only allows string or null for slot values
const slotValueSchema = const slotValueTypes =
enhancementResultJsonSchema.properties.slotFills.additionalProperties enhancementResultJsonSchema.properties.slotFills.additionalProperties
.additionalProperties .additionalProperties.type
expect(slotValueSchema.anyOf).toEqual([ expect(slotValueTypes).toContain("string")
{ type: "string" }, expect(slotValueTypes).toContain("null")
{ type: "null" }, expect(slotValueTypes).not.toContain("number")
])
}) })
}) })

View File

@@ -31,7 +31,7 @@ export const enhancementResultJsonSchema = {
additionalProperties: { additionalProperties: {
type: "object", type: "object",
additionalProperties: { additionalProperties: {
anyOf: [{ type: "string" }, { type: "null" }], type: ["string", "null"],
}, },
}, },
}, },

View File

@@ -32,7 +32,7 @@ export interface CurrentWeatherFeedItem extends FeedItem<
CurrentWeatherData CurrentWeatherData
> {} > {}
export type HourlyWeatherEntry = { export type HourlyWeatherData = {
forecastTime: Date forecastTime: Date
conditionCode: ConditionCode conditionCode: ConditionCode
daylight: boolean daylight: boolean
@@ -48,16 +48,12 @@ export type HourlyWeatherEntry = {
windSpeed: number windSpeed: number
} }
export type HourlyWeatherData = {
hours: HourlyWeatherEntry[]
}
export interface HourlyWeatherFeedItem extends FeedItem< export interface HourlyWeatherFeedItem extends FeedItem<
typeof WeatherFeedItemType.Hourly, typeof WeatherFeedItemType.Hourly,
HourlyWeatherData HourlyWeatherData
> {} > {}
export type DailyWeatherEntry = { export type DailyWeatherData = {
forecastDate: Date forecastDate: Date
conditionCode: ConditionCode conditionCode: ConditionCode
maxUvIndex: number maxUvIndex: number
@@ -71,10 +67,6 @@ export type DailyWeatherEntry = {
temperatureMin: number temperatureMin: number
} }
export type DailyWeatherData = {
days: DailyWeatherEntry[]
}
export interface DailyWeatherFeedItem extends FeedItem< export interface DailyWeatherFeedItem extends FeedItem<
typeof WeatherFeedItemType.Daily, typeof WeatherFeedItemType.Daily,
DailyWeatherData DailyWeatherData

View File

@@ -8,10 +8,8 @@ export {
type CurrentWeatherData, type CurrentWeatherData,
type HourlyWeatherFeedItem, type HourlyWeatherFeedItem,
type HourlyWeatherData, type HourlyWeatherData,
type HourlyWeatherEntry,
type DailyWeatherFeedItem, type DailyWeatherFeedItem,
type DailyWeatherData, type DailyWeatherData,
type DailyWeatherEntry,
type WeatherAlertFeedItem, type WeatherAlertFeedItem,
type WeatherAlertData, type WeatherAlertData,
} from "./feed-items" } from "./feed-items"

View File

@@ -4,10 +4,10 @@ import { Context } from "@aelis/core"
import { LocationKey } from "@aelis/source-location" import { LocationKey } from "@aelis/source-location"
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import type { WeatherKitClient, WeatherKitResponse, HourlyForecast, DailyForecast } from "./weatherkit" import type { WeatherKitClient, WeatherKitResponse } from "./weatherkit"
import fixture from "../fixtures/san-francisco.json" import fixture from "../fixtures/san-francisco.json"
import { WeatherFeedItemType, type DailyWeatherData, type HourlyWeatherData } from "./feed-items" import { WeatherFeedItemType } from "./feed-items"
import { WeatherKey, type Weather } from "./weather-context" import { WeatherKey, type Weather } from "./weather-context"
import { WeatherSource, Units } from "./weather-source" import { WeatherSource, Units } from "./weather-source"
@@ -131,125 +131,8 @@ describe("WeatherSource", () => {
const hourlyItems = items.filter((i) => i.type === WeatherFeedItemType.Hourly) const hourlyItems = items.filter((i) => i.type === WeatherFeedItemType.Hourly)
const dailyItems = items.filter((i) => i.type === WeatherFeedItemType.Daily) const dailyItems = items.filter((i) => i.type === WeatherFeedItemType.Daily)
expect(hourlyItems.length).toBe(1) expect(hourlyItems.length).toBe(3)
expect((hourlyItems[0]!.data as HourlyWeatherData).hours.length).toBe(3) expect(dailyItems.length).toBe(2)
expect(dailyItems.length).toBe(1)
expect((dailyItems[0]!.data as DailyWeatherData).days.length).toBe(2)
})
test("produces a single hourly item with hours array", async () => {
const source = new WeatherSource({ client: mockClient })
const context = createMockContext({ lat: 37.7749, lng: -122.4194 })
const items = await source.fetchItems(context)
const hourlyItems = items.filter((i) => i.type === WeatherFeedItemType.Hourly)
expect(hourlyItems.length).toBe(1)
const hourlyData = hourlyItems[0]!.data as HourlyWeatherData
expect(Array.isArray(hourlyData.hours)).toBe(true)
expect(hourlyData.hours.length).toBeGreaterThan(0)
expect(hourlyData.hours.length).toBeLessThanOrEqual(12)
})
test("averages urgency across hours with mixed conditions", async () => {
const mildHour: HourlyForecast = {
forecastStart: "2026-01-17T01:00:00Z",
conditionCode: "Clear",
daylight: true,
humidity: 0.5,
precipitationAmount: 0,
precipitationChance: 0,
precipitationType: "clear",
pressure: 1013,
snowfallIntensity: 0,
temperature: 20,
temperatureApparent: 20,
temperatureDewPoint: 10,
uvIndex: 3,
visibility: 20000,
windDirection: 180,
windGust: 10,
windSpeed: 5,
}
const severeHour: HourlyForecast = {
...mildHour,
forecastStart: "2026-01-17T02:00:00Z",
conditionCode: "SevereThunderstorm",
}
const mixedResponse: WeatherKitResponse = {
forecastHourly: { hours: [mildHour, severeHour] },
}
const source = new WeatherSource({ client: createMockClient(mixedResponse) })
const context = createMockContext({ lat: 37.7749, lng: -122.4194 })
const items = await source.fetchItems(context)
const hourlyItem = items.find((i) => i.type === WeatherFeedItemType.Hourly)
expect(hourlyItem).toBeDefined()
// Mild urgency = 0.3, severe urgency = 0.6, average = 0.45
expect(hourlyItem!.signals!.urgency).toBeCloseTo(0.45, 5)
// Worst-case: SevereThunderstorm → Imminent
expect(hourlyItem!.signals!.timeRelevance).toBe("imminent")
})
test("produces a single daily item with days array", async () => {
const source = new WeatherSource({ client: mockClient })
const context = createMockContext({ lat: 37.7749, lng: -122.4194 })
const items = await source.fetchItems(context)
const dailyItems = items.filter((i) => i.type === WeatherFeedItemType.Daily)
expect(dailyItems.length).toBe(1)
const dailyData = dailyItems[0]!.data as DailyWeatherData
expect(Array.isArray(dailyData.days)).toBe(true)
expect(dailyData.days.length).toBeGreaterThan(0)
expect(dailyData.days.length).toBeLessThanOrEqual(7)
})
test("averages urgency across days with mixed conditions", async () => {
const mildDay: DailyForecast = {
forecastStart: "2026-01-17T00:00:00Z",
forecastEnd: "2026-01-18T00:00:00Z",
conditionCode: "Clear",
maxUvIndex: 3,
moonPhase: "firstQuarter",
precipitationAmount: 0,
precipitationChance: 0,
precipitationType: "clear",
snowfallAmount: 0,
sunrise: "2026-01-17T07:00:00Z",
sunriseCivil: "2026-01-17T06:30:00Z",
sunriseNautical: "2026-01-17T06:00:00Z",
sunriseAstronomical: "2026-01-17T05:30:00Z",
sunset: "2026-01-17T17:00:00Z",
sunsetCivil: "2026-01-17T17:30:00Z",
sunsetNautical: "2026-01-17T18:00:00Z",
sunsetAstronomical: "2026-01-17T18:30:00Z",
temperatureMax: 15,
temperatureMin: 5,
}
const severeDay: DailyForecast = {
...mildDay,
forecastStart: "2026-01-18T00:00:00Z",
forecastEnd: "2026-01-19T00:00:00Z",
conditionCode: "SevereThunderstorm",
}
const mixedResponse: WeatherKitResponse = {
forecastDaily: { days: [mildDay, severeDay] },
}
const source = new WeatherSource({ client: createMockClient(mixedResponse) })
const context = createMockContext({ lat: 37.7749, lng: -122.4194 })
const items = await source.fetchItems(context)
const dailyItem = items.find((i) => i.type === WeatherFeedItemType.Daily)
expect(dailyItem).toBeDefined()
// Mild urgency = 0.2, severe urgency = 0.5, average = 0.35
expect(dailyItem!.signals!.urgency).toBeCloseTo(0.35, 5)
// Worst-case: SevereThunderstorm → Imminent
expect(dailyItem!.signals!.timeRelevance).toBe("imminent")
}) })
test("sets timestamp from context.time", async () => { test("sets timestamp from context.time", async () => {

View File

@@ -3,7 +3,7 @@ import type { ActionDefinition, ContextEntry, FeedItemSignals, FeedSource } from
import { Context, TimeRelevance, UnknownActionError } from "@aelis/core" import { Context, TimeRelevance, UnknownActionError } from "@aelis/core"
import { LocationKey } from "@aelis/source-location" import { LocationKey } from "@aelis/source-location"
import { WeatherFeedItemType, type DailyWeatherEntry, type HourlyWeatherEntry, type WeatherFeedItem } from "./feed-items" import { WeatherFeedItemType, type WeatherFeedItem } from "./feed-items"
import currentWeatherInsightPrompt from "./prompts/current-weather-insight.txt" import currentWeatherInsightPrompt from "./prompts/current-weather-insight.txt"
import { WeatherKey, type Weather } from "./weather-context" import { WeatherKey, type Weather } from "./weather-context"
import { import {
@@ -174,15 +174,21 @@ export class WeatherSource implements FeedSource<WeatherFeedItem> {
if (response.forecastHourly?.hours) { if (response.forecastHourly?.hours) {
const hours = response.forecastHourly.hours.slice(0, this.hourlyLimit) const hours = response.forecastHourly.hours.slice(0, this.hourlyLimit)
if (hours.length > 0) { for (let i = 0; i < hours.length; i++) {
items.push(createHourlyForecastFeedItem(hours, timestamp, this.units, this.id)) const hour = hours[i]
if (hour) {
items.push(createHourlyWeatherFeedItem(hour, i, timestamp, this.units, this.id))
}
} }
} }
if (response.forecastDaily?.days) { if (response.forecastDaily?.days) {
const days = response.forecastDaily.days.slice(0, this.dailyLimit) const days = response.forecastDaily.days.slice(0, this.dailyLimit)
if (days.length > 0) { for (let i = 0; i < days.length; i++) {
items.push(createDailyForecastFeedItem(days, timestamp, this.units, this.id)) const day = days[i]
if (day) {
items.push(createDailyWeatherFeedItem(day, i, timestamp, this.units, this.id))
}
} }
} }
@@ -317,18 +323,24 @@ function createCurrentWeatherFeedItem(
} }
} }
function createHourlyForecastFeedItem( function createHourlyWeatherFeedItem(
hourlyForecasts: HourlyForecast[], hourly: HourlyForecast,
index: number,
timestamp: Date, timestamp: Date,
units: Units, units: Units,
sourceId: string, sourceId: string,
): WeatherFeedItem { ): WeatherFeedItem {
const hours: HourlyWeatherEntry[] = [] const signals: FeedItemSignals = {
let totalUrgency = 0 urgency: adjustUrgencyForCondition(BASE_URGENCY.hourly, hourly.conditionCode),
let worstTimeRelevance: TimeRelevance = TimeRelevance.Ambient timeRelevance: timeRelevanceForCondition(hourly.conditionCode),
}
for (const hourly of hourlyForecasts) { return {
hours.push({ id: `weather-hourly-${timestamp.getTime()}-${index}`,
sourceId,
type: WeatherFeedItemType.Hourly,
timestamp,
data: {
forecastTime: new Date(hourly.forecastStart), forecastTime: new Date(hourly.forecastStart),
conditionCode: hourly.conditionCode, conditionCode: hourly.conditionCode,
daylight: hourly.daylight, daylight: hourly.daylight,
@@ -342,43 +354,29 @@ function createHourlyForecastFeedItem(
windDirection: hourly.windDirection, windDirection: hourly.windDirection,
windGust: convertSpeed(hourly.windGust, units), windGust: convertSpeed(hourly.windGust, units),
windSpeed: convertSpeed(hourly.windSpeed, units), windSpeed: convertSpeed(hourly.windSpeed, units),
}) },
totalUrgency += adjustUrgencyForCondition(BASE_URGENCY.hourly, hourly.conditionCode)
const rel = timeRelevanceForCondition(hourly.conditionCode)
if (rel === TimeRelevance.Imminent) {
worstTimeRelevance = TimeRelevance.Imminent
} else if (rel === TimeRelevance.Upcoming && worstTimeRelevance !== TimeRelevance.Imminent) {
worstTimeRelevance = TimeRelevance.Upcoming
}
}
const signals: FeedItemSignals = {
urgency: totalUrgency / hours.length,
timeRelevance: worstTimeRelevance,
}
return {
id: `weather-hourly-${timestamp.getTime()}`,
sourceId,
type: WeatherFeedItemType.Hourly,
timestamp,
data: { hours },
signals, signals,
} }
} }
function createDailyForecastFeedItem( function createDailyWeatherFeedItem(
dailyForecasts: DailyForecast[], daily: DailyForecast,
index: number,
timestamp: Date, timestamp: Date,
units: Units, units: Units,
sourceId: string, sourceId: string,
): WeatherFeedItem { ): WeatherFeedItem {
const days: DailyWeatherEntry[] = [] const signals: FeedItemSignals = {
let totalUrgency = 0 urgency: adjustUrgencyForCondition(BASE_URGENCY.daily, daily.conditionCode),
let worstTimeRelevance: TimeRelevance = TimeRelevance.Ambient timeRelevance: timeRelevanceForCondition(daily.conditionCode),
}
for (const daily of dailyForecasts) { return {
days.push({ id: `weather-daily-${timestamp.getTime()}-${index}`,
sourceId,
type: WeatherFeedItemType.Daily,
timestamp,
data: {
forecastDate: new Date(daily.forecastStart), forecastDate: new Date(daily.forecastStart),
conditionCode: daily.conditionCode, conditionCode: daily.conditionCode,
maxUvIndex: daily.maxUvIndex, maxUvIndex: daily.maxUvIndex,
@@ -390,27 +388,7 @@ function createDailyForecastFeedItem(
sunset: new Date(daily.sunset), sunset: new Date(daily.sunset),
temperatureMax: convertTemperature(daily.temperatureMax, units), temperatureMax: convertTemperature(daily.temperatureMax, units),
temperatureMin: convertTemperature(daily.temperatureMin, units), temperatureMin: convertTemperature(daily.temperatureMin, units),
}) },
totalUrgency += adjustUrgencyForCondition(BASE_URGENCY.daily, daily.conditionCode)
const rel = timeRelevanceForCondition(daily.conditionCode)
if (rel === TimeRelevance.Imminent) {
worstTimeRelevance = TimeRelevance.Imminent
} else if (rel === TimeRelevance.Upcoming && worstTimeRelevance !== TimeRelevance.Imminent) {
worstTimeRelevance = TimeRelevance.Upcoming
}
}
const signals: FeedItemSignals = {
urgency: totalUrgency / days.length,
timeRelevance: worstTimeRelevance,
}
return {
id: `weather-daily-${timestamp.getTime()}`,
sourceId,
type: WeatherFeedItemType.Daily,
timestamp,
data: { days },
signals, signals,
} }
} }

View File

@@ -1,21 +0,0 @@
#!/bin/bash
# Tailscale setup script
# Authenticates with Tailscale if TS_AUTH_KEY is set and Tailscale is not already logged in
set -e
if [ -z "$TS_AUTH_KEY" ]; then
echo "TS_AUTH_KEY is not set, skipping Tailscale login."
exit 0
fi
STATUS=$(tailscale status 2>&1 || true)
if echo "$STATUS" | grep -qi "logged out\|stopped"; then
echo "Tailscale is not authenticated. Logging in..."
sudo tailscale up --accept-routes --auth-key="$TS_AUTH_KEY"
echo "Tailscale login complete."
else
echo "Tailscale is already authenticated, skipping."
fi