mirror of
https://github.com/kennethnym/aris.git
synced 2026-04-21 01:01:18 +01:00
Compare commits
3 Commits
feat/combi
...
feat/admin
| Author | SHA1 | Date | |
|---|---|---|---|
|
39ced53900
|
|||
|
c1d9ec9399
|
|||
|
34214f5f3e
|
@@ -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": {
|
||||||
|
|||||||
@@ -17,23 +17,3 @@ services:
|
|||||||
FORWARD_URL=$(gitpod environment port open 4983 --name drizzle-studio-server | sed 's|https://||')
|
FORWARD_URL=$(gitpod environment port open 4983 --name drizzle-studio-server | sed 's|https://||')
|
||||||
echo "Drizzle Studio: https://local.drizzle.studio/?host=${FORWARD_URL}&port=443"
|
echo "Drizzle Studio: https://local.drizzle.studio/?host=${FORWARD_URL}&port=443"
|
||||||
cd apps/aelis-backend && bunx drizzle-kit studio --host 0.0.0.0 --port 4983
|
cd apps/aelis-backend && bunx drizzle-kit studio --host 0.0.0.0 --port 4983
|
||||||
|
|
||||||
aelis-backend:
|
|
||||||
name: Aelis Backend
|
|
||||||
description: Hono API server for aelis-backend (port 3000)
|
|
||||||
triggeredBy:
|
|
||||||
- manual
|
|
||||||
commands:
|
|
||||||
start: |
|
|
||||||
gitpod --context environment environment port open 3000 --name "Aelis Backend" --protocol http
|
|
||||||
cd apps/aelis-backend && bun run dev
|
|
||||||
|
|
||||||
admin-dashboard:
|
|
||||||
name: Admin Dashboard
|
|
||||||
description: Vite dev server for admin-dashboard (port 5174)
|
|
||||||
triggeredBy:
|
|
||||||
- manual
|
|
||||||
commands:
|
|
||||||
start: |
|
|
||||||
gitpod --context environment environment port open 5174 --name "Admin Dashboard" --protocol http
|
|
||||||
cd apps/admin-dashboard && bun run dev --host
|
|
||||||
|
|||||||
@@ -47,15 +47,10 @@ export const Route = createRoute({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
id: "dashboard",
|
id: "dashboard",
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async ({ context }) => {
|
||||||
let session: Awaited<ReturnType<typeof getSession>> | null = null
|
const session = await context.queryClient.ensureQueryData({
|
||||||
try {
|
queryKey: ["session"],
|
||||||
session = await context.queryClient.ensureQueryData({
|
queryFn: getSession,
|
||||||
queryKey: ["session"],
|
})
|
||||||
queryFn: getSession,
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
throw redirect({ to: "/login" })
|
|
||||||
}
|
|
||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
throw redirect({ to: "/login" })
|
throw redirect({ to: "/login" })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ export function createAuth(db: Database) {
|
|||||||
provider: "pg",
|
provider: "pg",
|
||||||
schema,
|
schema,
|
||||||
}),
|
}),
|
||||||
advanced: {
|
|
||||||
disableCSRFCheck: process.env.NODE_ENV !== "production",
|
|
||||||
},
|
|
||||||
emailAndPassword: {
|
emailAndPassword: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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")
|
||||||
])
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const enhancementResultJsonSchema = {
|
|||||||
additionalProperties: {
|
additionalProperties: {
|
||||||
type: "object",
|
type: "object",
|
||||||
additionalProperties: {
|
additionalProperties: {
|
||||||
anyOf: [{ type: "string" }, { type: "null" }],
|
type: ["string", "null"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import { cors } from "hono/cors"
|
|
||||||
|
|
||||||
import { registerAdminHttpHandlers } from "./admin/http.ts"
|
import { registerAdminHttpHandlers } from "./admin/http.ts"
|
||||||
import { createRequireAdmin } from "./auth/admin-middleware.ts"
|
import { createRequireAdmin } from "./auth/admin-middleware.ts"
|
||||||
@@ -51,34 +50,6 @@ function main() {
|
|||||||
|
|
||||||
const app = new Hono()
|
const app = new Hono()
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV !== "production"
|
|
||||||
const allowedOrigins = process.env.CORS_ORIGINS?.split(",").map((o) => o.trim()) ?? []
|
|
||||||
|
|
||||||
function resolveOrigin(origin: string): string | undefined {
|
|
||||||
if (isDev) return origin
|
|
||||||
return allowedOrigins.includes(origin) ? origin : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
"/api/auth/*",
|
|
||||||
cors({
|
|
||||||
origin: resolveOrigin,
|
|
||||||
allowHeaders: ["Content-Type", "Authorization"],
|
|
||||||
allowMethods: ["POST", "GET", "OPTIONS"],
|
|
||||||
exposeHeaders: ["Content-Length"],
|
|
||||||
maxAge: 600,
|
|
||||||
credentials: true,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
"*",
|
|
||||||
cors({
|
|
||||||
origin: resolveOrigin,
|
|
||||||
credentials: true,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
app.get("/health", (c) => c.json({ status: "ok" }))
|
app.get("/health", (c) => c.json({ status: "ok" }))
|
||||||
|
|
||||||
const authSessionMiddleware = createRequireSession(auth)
|
const authSessionMiddleware = createRequireSession(auth)
|
||||||
|
|||||||
@@ -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,10 +48,6 @@ 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
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export {
|
|||||||
type CurrentWeatherData,
|
type CurrentWeatherData,
|
||||||
type HourlyWeatherFeedItem,
|
type HourlyWeatherFeedItem,
|
||||||
type HourlyWeatherData,
|
type HourlyWeatherData,
|
||||||
type HourlyWeatherEntry,
|
|
||||||
type DailyWeatherFeedItem,
|
type DailyWeatherFeedItem,
|
||||||
type DailyWeatherData,
|
type DailyWeatherData,
|
||||||
type WeatherAlertFeedItem,
|
type WeatherAlertFeedItem,
|
||||||
|
|||||||
@@ -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 } 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 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,67 +131,10 @@ 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(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("sets timestamp from context.time", async () => {
|
test("sets timestamp from context.time", async () => {
|
||||||
const source = new WeatherSource({ client: mockClient })
|
const source = new WeatherSource({ client: mockClient })
|
||||||
const queryTime = new Date("2026-01-17T12:00:00Z")
|
const queryTime = new Date("2026-01-17T12:00:00Z")
|
||||||
|
|||||||
@@ -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 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,8 +174,11 @@ 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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,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,
|
||||||
@@ -345,27 +354,7 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user