diff --git a/.idea/runConfigurations/Dockerfile.xml b/.idea/runConfigurations/Dockerfile.xml new file mode 100644 index 0000000..3cf996b --- /dev/null +++ b/.idea/runConfigurations/Dockerfile.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7baafb5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM golang:1.24 as builder + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY *.go ./ +COPY web ./web + +RUN CGO_ENABLED=0 GOOS=linux go build -o ./server + +FROM gcr.io/distroless/base-debian11 AS build-release-stage + +COPY --from=builder /app/server /app/server + +USER nonroot:nonroot + +WORKDIR /app + +EXPOSE 8080 + +ENTRYPOINT ["./server"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7e6dbb9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + server: + build: + dockerfile: ./Dockerfile + env_file: + - .env + ports: + - "8080:8080" + volumes: + - ./data.sqlite:/app/data.sqlite \ No newline at end of file diff --git a/main.go b/main.go index aa7b4f1..e914250 100644 --- a/main.go +++ b/main.go @@ -89,7 +89,57 @@ type state struct { //go:embed web var webDir embed.FS -var prompt = "The current time is 7am. Provide a summary of today's weather in %v below, as well as how to deal with the weather, such as how to dress for the weather, and whether they need an umbrella Use celsius and fahrenheit for temperature. Mention %v in the summary, but don't add anything else, as the summary will be displayed on a website." +var envKeys = []string{"GEMINI_API_KEY", "OPEN_WEATHER_MAP_API_KEY", "VAPID_PRIVATE_KEY_BASE64", "VAPID_PUBLIC_KEY_BASE64"} + +var prompt = `The current time is 5pm. Provide a short summary of the weather forecast for the next 48 hours in JSON in %v below. + +This is the JSON schema of the JSON: +current Current weather data API response +current.dt Current time, Unix, UTC +current.sunrise Sunrise time, Unix, UTC. For polar areas in midnight sun and polar night periods this parameter is not returned in the response +current.sunset Sunset time, Unix, UTC. For polar areas in midnight sun and polar night periods this parameter is not returned in the response +current.temp Temperature. Units - default: kelvin, metric: Celsius, imperial: Fahrenheit. How to change units used +current.feels_like Temperature. This temperature parameter accounts for the human perception of weather. Units – default: kelvin, metric: Celsius, imperial: Fahrenheit. +current.pressure Atmospheric pressure on the sea level, hPa +current.humidity Humidity, percentage +current.dew_point Atmospheric temperature (varying according to pressure and humidity) below which water droplets begin to condense and dew can form. Units – default: kelvin, metric: Celsius, imperial: Fahrenheit +current.clouds Cloudiness, percentage +current.uvi Current UV index. +current.visibility Average visibility, metres. The maximum value of the visibility is 10 km +current.wind_speed Wind speed. Wind speed. Units – default: metre/sec, metric: metre/sec, imperial: miles/hour. How to change units used +current.wind_gust (where available) Wind gust. Units – default: metre/sec, metric: metre/sec, imperial: miles/hour. How to change units used +current.wind_deg Wind direction, degrees (meteorological) +current.rain.1h (where available) Precipitation, mm/h. Please note that only mm/h as units of measurement are available for this parameter +current.snow.1h (where available) Precipitation, mm/h. Please note that only mm/h as units of measurement are available for this parameter +current.weather +current.weather.id Weather condition id +current.weather.main Group of weather parameters (Rain, Snow etc.) +current.weather.description Weather condition within the group (full list of weather conditions). Get the output in your language +current.weather.icon +hourly Hourly forecast weather data API response +hourly.dt Time of the forecasted data, Unix, UTC +hourly.temp Temperature. Units – default: kelvin, metric: Celsius, imperial: Fahrenheit. How to change units used +hourly.feels_like Temperature. This accounts for the human perception of weather. Units – default: kelvin, metric: Celsius, imperial: Fahrenheit. +hourly.pressure Atmospheric pressure on the sea level, hPa +hourly.humidity Humidity, percentage +hourly.dew_point Atmospheric temperature (varying according to pressure and humidity) below which water droplets begin to condense and dew can form. Units – default: kelvin, metric: Celsius, imperial: Fahrenheit. +hourly.uvi UV index +hourly.clouds Cloudiness, percentage +hourly.visibility Average visibility, metres. The maximum value of the visibility is 10 km +hourly.wind_speed Wind speed. Units – default: metre/sec, metric: metre/sec, imperial: miles/hour.How to change units used +hourly.wind_gust (where available) Wind gust. Units – default: metre/sec, metric: metre/sec, imperial: miles/hour. How to change units used +hourly.wind_deg Wind direction, degrees (meteorological) +hourly.pop Probability of precipitation. The values of the parameter vary between 0 and 1, where 0 is equal to 0 percent, 1 is equal to 100 percent +hourly.rain.1h (where available) Precipitation, mm/h. Please note that only mm/h as units of measurement are available for this parameter +hourly.snow.1h (where available) Precipitation, mm/h. Please note that only mm/h as units of measurement are available for this parameter +hourly.weather.id Weather condition id +hourly.weather.main Group of weather parameters (Rain, Snow etc.) +hourly.weather.description Weather condition within the group (full list of weather conditions). Get the output in your language + +Summarize hourly weather until midnight today. Keep it concise. Suggest how to deal with the weather, such as how to dress for the weather, and whether they need an umbrella. +Use celsius and fahrenheit but not Kelvin for temperature. Mention %v in the summary, but don't add anything else, as the summary will be displayed on a website. +The summary should be in plaintext for humans. Do not output in JSON. +` var supportedLocations = map[string]location{ "london": {51.507351, -0.127758, "Europe/London"}, @@ -120,9 +170,10 @@ func main() { return } - err := godotenv.Load() + _ = godotenv.Load() + err := checkEnv() if err != nil { - log.Fatalln("please create a .env file using the provided template!") + log.Fatal(err) } db, err := initDB() @@ -229,6 +280,20 @@ func generateKeys() { fmt.Printf("private key: %v\n", priv) } +func checkEnv() error { + var missing []string + for _, k := range envKeys { + v := os.Getenv(k) + if v == "" { + missing = append(missing, k) + } + } + if len(missing) > 0 { + return fmt.Errorf("missing env: %v", strings.Join(missing, ", ")) + } + return nil +} + func handleHTTPRequest(state *state) http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { path := strings.TrimPrefix(request.URL.Path, "/") @@ -463,7 +528,7 @@ func deleteSubscription(state *state, regID uuid.UUID) error { func updateSummaries(state *state, locKey string, loc *location) { log.Printf("updating summary for %v...\n", locKey) - resp, err := http.Get(fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?lat=%f&lon=%f&appid=%v", loc.lat, loc.lon, state.apiKey.openWeatherMap)) + resp, err := http.Get(fmt.Sprintf("https://api.openweathermap.org/data/3.0/onecall?lat=%v&lon=%v&exclude=minutely,daily&appid=%v", loc.lat, loc.lon, state.apiKey.openWeatherMap)) if err != nil { log.Printf("error updating summaries for %s: %e\n", locKey, err) return @@ -478,7 +543,7 @@ func updateSummaries(state *state, locKey string, loc *location) { result, err := state.genai.Models.GenerateContent(state.ctx, "gemini-2.0-flash", []*genai.Content{{ Parts: []*genai.Part{ - {Text: fmt.Sprintf(prompt, locationNames[locKey])}, + {Text: fmt.Sprintf(prompt, locationNames[locKey], locationNames[locKey])}, {Text: string(b)}, }, }}, nil) diff --git a/web/style.css b/web/style.css index 73d8b6e..45511ce 100644 --- a/web/style.css +++ b/web/style.css @@ -1,8 +1,8 @@ :root { --background-color: #f3f4f6; --text-color: #1f2937; - --button-color: #030712; - --button-text-color: #f3f4f6; + --button-color: #d1d5db; + --button-text-color: #1f2937; } @media (prefers-color-scheme: dark) { @@ -81,10 +81,12 @@ main { button { background-color: var(--button-color); color: var(--button-text-color); - border: 0; - padding: 1rem 3rem; + border: 1px #9ca3af solid; + padding: 0.5rem 2rem; border-radius: 4px; - font-weight: bold; + width: 100%; + font-weight: 500; + box-shadow: rgba(0, 0, 0, 0.05) 0px 0px 0px 1px; } footer { @@ -102,8 +104,11 @@ footer > p { } .summary { - max-width: 30ch; - font-size: 2em; + max-width: 40ch; + font-size: 1em; + line-height: 1.5em; + padding-top: 1rem; + padding-bottom: 1rem; } .back-link { diff --git a/web/summary.html b/web/summary.html index 05c689b..9626e0c 100644 --- a/web/summary.html +++ b/web/summary.html @@ -15,7 +15,7 @@
- <- All locations + <- All locations

{{.Summary}}