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 @@