Compare commits

..

No commits in common. "7612dd6bd9300b4cc845899012a18bcd73edb9b9" and "799dfc18edf39a1631e9720d6d3c2db53244839c" have entirely different histories.

62 changed files with 279 additions and 865 deletions

View File

@ -8,7 +8,6 @@ on:
- v* - v*
env: env:
# Use docker.io for Docker Hub if empty
REGISTRY: docker.io REGISTRY: docker.io
IMAGE_NAME: ${{ github.event.repository.name }} IMAGE_NAME: ${{ github.event.repository.name }}
@ -16,36 +15,51 @@ jobs:
docker: docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - name: Checkout
name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 # 获取完整的 git history 以便生成正确的 tag fetch-depth: 0
-
name: Get Version - name: Get Version
id: get_version id: get_version
run: | run: |
echo "VERSION=$(git describe --dirty --always --tags --abbrev=7)" >> $GITHUB_OUTPUT echo "VERSION=$(git describe --dirty --always --tags --abbrev=7)" >> $GITHUB_OUTPUT
-
name: Login to ${{ env.REGISTRY }} - name: Login to ${{ env.REGISTRY }}
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{github.actor}} username: ${{github.actor}}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- with:
name: Build and push driver-opts: |
network=host
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: | tags: |
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:latest ${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:latest
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:${{ steps.get_version.outputs.VERSION }} ${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:${{ steps.get_version.outputs.VERSION }}
cache-from: |
type=local,src=/tmp/.buildx-cache
type=registry,ref=${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:latest
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max

2
.gitignore vendored
View File

@ -38,5 +38,3 @@
/node_modules /node_modules
.idea .idea
public/sitemap.xml.gz

View File

@ -75,7 +75,7 @@ COPY --from=build /rails /rails
# Run and own only the runtime files as a non-root user for security # Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \ RUN groupadd --system --gid 1000 rails && \
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
chown -R rails:rails db log storage tmp public chown -R rails:rails db log storage tmp
USER 1000:1000 USER 1000:1000
# Entrypoint prepares the database. # Entrypoint prepares the database.

View File

@ -45,11 +45,6 @@ gem "devise", "~> 4.9"
gem "activeadmin", "~> 3.2" gem "activeadmin", "~> 3.2"
gem "friendly_id", "~> 5.5" gem "friendly_id", "~> 5.5"
gem "kaminari", "~> 1.2"
gem "meta-tags", "~> 2.22"
gem "sitemap_generator", "~> 6.3"
# gem "whenever", "~> 1.0" # gem "whenever", "~> 1.0"
gem "ruby-openai", "~> 7.3" gem "ruby-openai", "~> 7.3"
gem "httparty", "~> 0.22.0" gem "httparty", "~> 0.22.0"

View File

@ -233,8 +233,6 @@ GEM
net-smtp net-smtp
marcel (1.0.4) marcel (1.0.4)
matrix (0.4.2) matrix (0.4.2)
meta-tags (2.22.1)
actionpack (>= 6.0.0, < 8.1)
mini_mime (1.1.5) mini_mime (1.1.5)
minitest (5.25.4) minitest (5.25.4)
msgpack (1.7.5) msgpack (1.7.5)
@ -402,8 +400,6 @@ GEM
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8) sidekiq (>= 6, < 8)
tilt (>= 1.4.0, < 3) tilt (>= 1.4.0, < 3)
sitemap_generator (6.3.0)
builder (~> 3.0)
solid_cable (3.0.5) solid_cable (3.0.5)
actioncable (>= 7.2) actioncable (>= 7.2)
activejob (>= 7.2) activejob (>= 7.2)
@ -498,8 +494,6 @@ DEPENDENCIES
jbuilder jbuilder
jsbundling-rails jsbundling-rails
kamal kamal
kaminari (~> 1.2)
meta-tags (~> 2.22)
pg (~> 1.5) pg (~> 1.5)
propshaft propshaft
puma (>= 5.0) puma (>= 5.0)
@ -509,7 +503,6 @@ DEPENDENCIES
selenium-webdriver selenium-webdriver
sidekiq (~> 7.3) sidekiq (~> 7.3)
sidekiq-scheduler (~> 5.0) sidekiq-scheduler (~> 5.0)
sitemap_generator (~> 6.3)
solid_cable solid_cable
solid_cache solid_cache
solid_queue solid_queue

View File

@ -1,24 +0,0 @@
# app/concerns/seo_concern.rb
module SeoConcern
extend ActiveSupport::Concern
included do
before_action :prepare_meta_tags
end
private
def prepare_meta_tags
set_meta_tags(
site: "TodayAIWeather",
description: "Discover AI-generated weather art from cities around the world. Real-time weather visualization through artificial intelligence.",
keywords: "weather, AI art, weather visualization, city weather, artificial intelligence",
og: {
title: :title,
description: :description,
type: "website",
url: request.original_url
}
)
end
end

View File

@ -1,5 +1,4 @@
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
include SeoConcern
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
allow_browser versions: :modern allow_browser versions: :modern
before_action :set_locale before_action :set_locale

View File

@ -1,21 +0,0 @@
class ArtsController < ApplicationController
def index
@regions = Region.all
@current_region = Region.find(params[:region]) if params[:region].present?
@weather_arts = WeatherArt.includes(city: [ :country, { country: :region } ])
if @current_region
@weather_arts = @weather_arts.joins(city: :country)
.where(countries: { region_id: @current_region.id })
end
@weather_arts = if params[:sort] == "oldest"
@weather_arts.order(created_at: :asc)
else
@weather_arts.order(created_at: :desc)
end
@weather_arts = @weather_arts.page(params[:page]).per(10)
end
end

View File

@ -1,5 +1,6 @@
class CitiesController < ApplicationController class CitiesController < ApplicationController
def index def index
@cities = City.all.order(:name)
@regions = Region.includes(:countries).order(:name) @regions = Region.includes(:countries).order(:name)
@cities = City.includes(:country, country: :region).active.order(:name) @cities = City.includes(:country, country: :region).active.order(:name)
@ -12,26 +13,9 @@ class CitiesController < ApplicationController
@current_country = Country.friendly.find(params[:country]) @current_country = Country.friendly.find(params[:country])
@cities = @cities.by_country(@current_country.id) @cities = @cities.by_country(@current_country.id)
end end
@cities = @cities.page(params[:page]).per(10)
set_meta_tags(
title: @current_region ? "Cities in #{@current_region.name}" : "Explore Cities",
description: "Discover weather art for cities #{@current_region ? "in #{@current_region.name}" : 'worldwide'}. Real-time AI-generated weather visualization.",
keywords: "#{@current_region&.name}, cities, weather art, AI visualization"
)
end end
def show def show
@city = City.friendly.find(params[:id]) @city = City.friendly.find(params[:id])
set_meta_tags(
title: @city.name,
description: "Experience #{@city.name}'s weather through AI-generated art. Daily updates of weather conditions visualized through artificial intelligence.",
keywords: "#{@city.name}, #{@city.country.name}, weather art, AI visualization",
og: {
image: @city.latest_weather_art&.image&.attached? ? url_for(@city.latest_weather_art.image) : nil
}
)
end end
end end

View File

@ -2,10 +2,5 @@ class HomeController < ApplicationController
def index def index
@latest_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(6) @latest_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(6)
@featured_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(5) @featured_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(5)
set_meta_tags(
title: "AI-Generated Weather Art",
description: "Experience weather through artistic AI visualization. Daily updated weather art for cities worldwide.",
keywords: "AI weather art, weather visualization, city weather, artificial intelligence"
)
end end
end end

View File

@ -2,13 +2,5 @@ class WeatherArtsController < ApplicationController
def show def show
@city = City.friendly.find(params[:city_id]) @city = City.friendly.find(params[:city_id])
@weather_art = @city.weather_arts.friendly.find(params[:slug]) @weather_art = @city.weather_arts.friendly.find(params[:slug])
set_meta_tags(
title: "#{@city.name} Weather Art - #{@weather_art.weather_date.strftime('%B %d, %Y')}",
description: "#{@city.name}'s weather visualized through AI art. #{@weather_art.description} at #{@weather_art.temperature}°C.",
keywords: "#{@city.name}, weather art, #{@weather_art.description}, AI visualization",
og: {
image: @weather_art.image.attached? ? url_for(@weather_art.image) : nil
}
)
end end
end end

View File

@ -1,24 +1,2 @@
module ApplicationHelper module ApplicationHelper
def weather_art_schema(weather_art)
{
"@context": "https://schema.org",
"@type": "ImageObject",
"name": "#{weather_art.city.name} Weather Art",
"description": weather_art.description,
"datePublished": weather_art.created_at.iso8601,
"contentUrl": url_for(weather_art.image),
"author": {
"@type": "Organization",
"name": "TodayAIWeather"
},
"locationCreated": {
"@type": "Place",
"name": weather_art.city.name,
"address": {
"@type": "PostalAddress",
"addressCountry": weather_art.city.country.name
}
}
}.to_json.html_safe if weather_art.image.attached?
end
end end

View File

@ -1,2 +0,0 @@
module ArtsHelper
end

View File

@ -5,8 +5,6 @@ class City < ApplicationRecord
has_many :weather_arts, dependent: :destroy has_many :weather_arts, dependent: :destroy
delegate :region, to: :country
validates :name, presence: true validates :name, presence: true
validates :latitude, presence: true validates :latitude, presence: true
validates :longitude, presence: true validates :longitude, presence: true
@ -48,20 +46,6 @@ class City < ApplicationRecord
[ "active", "country", "created_at", "id", "id_value", "last_image_generation", "last_weather_fetch", "latitude", "longitude", "name", "priority", "region", "slug", "timezone", "updated_at" ] [ "active", "country", "created_at", "id", "id_value", "last_image_generation", "last_weather_fetch", "latitude", "longitude", "name", "priority", "region", "slug", "timezone", "updated_at" ]
end end
def last_weather_fetch
# latest_weather_art&.created_at
Rails.cache.fetch("city/#{id}/last_weather_fetch", expires_in: 1.hour) do
latest_weather_art&.created_at
end
end
def last_image_generation
# latest_weather_art&.image&.created_at
Rails.cache.fetch("city/#{id}/last_image_generation", expires_in: 1.hour) do
latest_weather_art&.image&.created_at
end
end
def latest_weather_art def latest_weather_art
weather_arts.order(weather_date: :desc).first weather_arts.order(weather_date: :desc).first
end end

View File

@ -1,172 +0,0 @@
<!-- app/views/arts/index.html.erb -->
<div class="min-h-screen">
<!-- 页面标题和背景 -->
<% featured_art = @weather_arts.first %>
<div class="relative">
<!-- 背景图像 -->
<% if featured_art&.image&.attached? %>
<div class="absolute inset-0 h-[40vh] overflow-hidden">
<%= image_tag featured_art.image,
class: "w-full h-full object-cover" %>
<div class="absolute inset-0 bg-gradient-to-b from-base-100/30 via-base-100/60 to-base-100"></div>
</div>
<% end %>
<!-- 标题内容 -->
<div class="relative pt-20 pb-32">
<div class="container mx-auto px-4">
<div class="max-w-3xl mx-auto text-center space-y-6">
<h1 class="text-4xl md:text-5xl font-display font-bold">
Weather Arts Gallery
</h1>
<p class="text-xl text-base-content/70">
Discover AI-generated weather art from cities around the world
</p>
<!-- 如果有特色图片,显示其信息 -->
<% if featured_art %>
<div class="text-sm text-base-content/60 pt-4">
Latest from <%= featured_art.city.name %>, <%= featured_art.city.country.name %>
<span class="mx-2">•</span>
<%= featured_art.weather_date.strftime("%B %d, %Y") %>
</div>
<% end %>
</div>
</div>
</div>
</div>
<!-- 筛选导航 -->
<div class="container mx-auto px-4 -mt-8">
<div class="bg-base-100 shadow-xl rounded-box p-6 mb-12">
<!-- 筛选选项 -->
<div class="flex flex-wrap gap-4 justify-center items-center">
<!-- 时间排序 -->
<div class="dropdown">
<button class="btn btn-ghost gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<%= params[:sort] == 'oldest' ? 'Oldest First' : 'Newest First' %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52">
<li>
<%= link_to "Newest First", arts_path(sort: 'newest', region: params[:region]),
class: "#{'active' if params[:sort] != 'oldest'}" %>
</li>
<li>
<%= link_to "Oldest First", arts_path(sort: 'oldest', region: params[:region]),
class: "#{'active' if params[:sort] == 'oldest'}" %>
</li>
</ul>
</div>
<!-- 区域筛选 -->
<div class="dropdown">
<button class="btn btn-ghost gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<%= @current_region&.name || 'All Regions' %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<ul class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-52">
<li>
<%= link_to "All Regions", arts_path(sort: params[:sort]),
class: "#{'active' unless @current_region}" %>
</li>
<div class="divider my-1"></div>
<% @regions.each do |region| %>
<li>
<%= link_to region.name, arts_path(region: region.id, sort: params[:sort]),
class: "#{'active' if @current_region == region}" %>
</li>
<% end %>
</ul>
</div>
</div>
<!-- 结果统计 -->
<div class="text-center text-sm text-base-content/70 mt-4">
Showing <%= @weather_arts.total_count %> weather arts
<% if @current_region %>
from <%= @current_region.name %>
<% end %>
</div>
</div>
</div>
<div class="container mx-auto px-4 pb-16">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<% @weather_arts.each do |art| %>
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 group overflow-hidden">
<figure class="relative aspect-square overflow-hidden">
<% if art.image.attached? %>
<%= image_tag art.image,
class: "w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-500" %>
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div class="absolute inset-0 p-6 flex flex-col justify-end translate-y-4 opacity-0 group-hover:translate-y-0 group-hover:opacity-100 transition-all duration-300">
<div class="text-white space-y-2">
<h3 class="text-xl font-display font-bold">
<%= art.city.name %>
</h3>
<p class="text-sm text-white/80">
<%= art.city.country.name %>
</p>
<div class="flex items-center gap-2 text-white/90">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
</svg>
<%= art.description %>
</div>
</div>
</div>
<% end %>
</figure>
<!-- 信息部分 -->
<div class="card-body p-4">
<div class="flex justify-between items-start mb-3">
<div>
<h3 class="font-display font-bold leading-tight">
<%= art.city.name %>
</h3>
<p class="text-sm text-base-content/70">
<%= art.weather_date.strftime("%B %d, %Y") %>
</p>
</div>
<div class="text-right">
<div class="text-2xl font-bold text-primary">
<%= art.temperature %>°C
</div>
<div class="text-sm text-base-content/70">
<%= art.humidity %>% humidity
</div>
</div>
</div>
<%= link_to city_weather_art_path(art.city, art),
class: "btn btn-primary btn-sm w-full" do %>
View Details
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
<% end %>
</div>
</div>
<% end %>
</div>
<%= render 'shared/pagination',
collection: @weather_arts,
collection_name: 'weather arts' %>
</div>
</div>
</div>

View File

@ -12,6 +12,7 @@
</div> </div>
<% end %> <% end %>
<!-- 标题内容 -->
<div class="relative pt-24 pb-32"> <div class="relative pt-24 pb-32">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="max-w-3xl mx-auto text-center space-y-6"> <div class="max-w-3xl mx-auto text-center space-y-6">
@ -37,10 +38,13 @@
</div> </div>
</div> </div>
<!-- 筛选导航 - 使用下拉菜单 -->
<div class="sticky top-16 z-20 bg-base-100/95 backdrop-blur-sm border-b border-base-200"> <div class="sticky top-16 z-20 bg-base-100/95 backdrop-blur-sm border-b border-base-200">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="py-3 flex items-center justify-between gap-4"> <div class="py-3 flex items-center justify-between gap-4">
<!-- 左侧筛选器 -->
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<!-- 区域选择下拉框 -->
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-ghost gap-2"> <button class="btn btn-ghost gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@ -69,6 +73,7 @@
</ul> </ul>
</div> </div>
<!-- 国家选择下拉框 (如果选择了区域) -->
<% if @current_region %> <% if @current_region %>
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-ghost gap-2"> <button class="btn btn-ghost gap-2">
@ -99,6 +104,7 @@
<% end %> <% end %>
</div> </div>
<!-- 右侧结果统计 -->
<div class="text-sm text-base-content/70"> <div class="text-sm text-base-content/70">
<%= @cities.count %> <%= 'city'.pluralize(@cities.count) %> <%= @cities.count %> <%= 'city'.pluralize(@cities.count) %>
<% if @current_country %> <% if @current_country %>
@ -111,16 +117,10 @@
</div> </div>
</div> </div>
<!-- 城市网格 -->
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<div class="container mx-auto px-4 pb-16"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> <%= render partial: 'city', collection: @cities %>
<%= render partial: 'city', collection: @cities %>
</div>
<%= render 'shared/pagination',
collection: @cities,
collection_name: 'cities' %>
</div> </div>
</div> </div>
</div> </div>

View File

@ -57,12 +57,4 @@
<% end %> <% end %>
</div> </div>
</section> </section>
</div>
<div class="text-center mt-12">
<%= link_to arts_path, class: "btn btn-primary btn-lg gap-2" do %>
View All Weather Arts
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
<% end %>
</div> </div>

View File

@ -1,11 +0,0 @@
<%# Link to the "First" page
- available local variables
url: url to the first page
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
-%>
<span class="first">
<%= link_to_unless current_page.first?, t('views.pagination.first').html_safe, url, remote: remote %>
</span>

View File

@ -1,8 +0,0 @@
<%# Non-link tag that stands for skipped pages...
- available local variables
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
-%>
<span class="page gap"><%= t('views.pagination.truncate').html_safe %></span>

View File

@ -1,11 +0,0 @@
<%# Link to the "Last" page
- available local variables
url: url to the last page
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
-%>
<span class="last">
<%= link_to_unless current_page.last?, t('views.pagination.last').html_safe, url, remote: remote %>
</span>

View File

@ -1,11 +0,0 @@
<%# Link to the "Next" page
- available local variables
url: url to the next page
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
-%>
<span class="next">
<%= link_to_unless current_page.last?, t('views.pagination.next').html_safe, url, rel: 'next', remote: remote %>
</span>

View File

@ -1,12 +0,0 @@
<%# Link showing page number
- available local variables
page: a page object for "this" page
url: url to this page
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
-%>
<span class="page<%= ' current' if page.current? %>">
<%= link_to_unless page.current?, page, url, {remote: remote, rel: page.rel} %>
</span>

View File

@ -1,25 +0,0 @@
<%# The container tag
- available local variables
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
paginator: the paginator that renders the pagination tags inside
-%>
<%= paginator.render do -%>
<nav class="pagination" role="navigation" aria-label="pager">
<%= first_page_tag unless current_page.first? %>
<%= prev_page_tag unless current_page.first? %>
<% each_page do |page| -%>
<% if page.display_tag? -%>
<%= page_tag page %>
<% elsif !page.was_truncated? -%>
<%= gap_tag %>
<% end -%>
<% end -%>
<% unless current_page.out_of_range? %>
<%= next_page_tag unless current_page.last? %>
<%= last_page_tag unless current_page.last? %>
<% end %>
</nav>
<% end -%>

View File

@ -1,11 +0,0 @@
<%# Link to the "Previous" page
- available local variables
url: url to the previous page
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
-%>
<span class="prev">
<%= link_to_unless current_page.first?, t('views.pagination.previous').html_safe, url, rel: 'prev', remote: remote %>
</span>

View File

@ -1,23 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html>
<head> <head>
<title><%= content_for(:title) || "Today Ai Weather" %></title> <title><%= content_for(:title) || "Today Ai Weather" %></title>
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<%= display_meta_tags(
site: 'TodayAIWeather',
reverse: true,
og: {
site_name: 'TodayAIWeather',
type: 'website',
url: request.original_url
},
alternate: {
"zh-CN" => url_for(locale: 'zh-CN'),
"en" => url_for(locale: 'en')
}
) %>
<%= csrf_meta_tags %> <%= csrf_meta_tags %>
<%= csp_meta_tag %> <%= csp_meta_tag %>
@ -33,18 +20,6 @@
<%# Includes all stylesheet files in app/assets/stylesheets %> <%# Includes all stylesheet files in app/assets/stylesheets %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %> <%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<script defer data-domain="todayaiweather.com" src="https://plausible.frytea.com/js/script.js"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-PX1C92V5L7"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-PX1C92V5L7');
</script>
</head> </head>
<body class="min-h-screen bg-base-100 font-sans"> <body class="min-h-screen bg-base-100 font-sans">
@ -58,7 +33,6 @@
</div> </div>
<div class="flex-none"> <div class="flex-none">
<%= link_to "Cities", cities_path, class: "btn btn-ghost font-sans" %> <%= link_to "Cities", cities_path, class: "btn btn-ghost font-sans" %>
<%= link_to "Arts", arts_path, class: "btn btn-ghost font-sans" %>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,86 +0,0 @@
<%# app/views/shared/_pagination.html.erb %>
<% if collection.total_pages > 1 %>
<div class="flex flex-col items-center mt-16 gap-6">
<!-- 页码信息 -->
<div class="text-base-content/70 font-light">
<span class="px-4 py-2 bg-base-200/50 rounded-full">
Page <%= collection.current_page %> of <%= collection.total_pages %>
</span>
</div>
<!-- 分页控件 -->
<div class="join shadow-lg">
<!-- 首页 -->
<%= link_to url_for(page: 1, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn #{collection.first_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
</svg>
<% end %>
<!-- 上一页 -->
<%= link_to url_for(page: collection.prev_page || 1, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn #{collection.first_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
<% end %>
<!-- 页码 -->
<% page_window = 2 # 当前页面前后显示的页码数 %>
<% start_page = [1, collection.current_page - page_window].max %>
<% end_page = [collection.total_pages, collection.current_page + page_window].min %>
<% if start_page > 1 %>
<%= link_to 1, url_for(page: 1, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn btn-ghost hover:bg-primary/5" %>
<% if start_page > 2 %>
<button class="join-item btn btn-ghost btn-disabled">...</button>
<% end %>
<% end %>
<% (start_page..end_page).each do |page| %>
<% if page == collection.current_page %>
<button class="join-item btn btn-ghost bg-primary/10 font-medium">
<%= page %>
</button>
<% else %>
<%= link_to page, url_for(page: page, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn btn-ghost hover:bg-primary/5" %>
<% end %>
<% end %>
<% if end_page < collection.total_pages %>
<% if end_page < collection.total_pages - 1 %>
<button class="join-item btn btn-ghost btn-disabled">...</button>
<% end %>
<%= link_to collection.total_pages,
url_for(page: collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn btn-ghost hover:bg-primary/5" %>
<% end %>
<!-- 下一页 -->
<%= link_to url_for(page: collection.next_page || collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn #{collection.last_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
<% end %>
<!-- 末页 -->
<%= link_to url_for(page: collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn #{collection.last_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
</svg>
<% end %>
</div>
<!-- 结果统计 -->
<div class="text-sm text-base-content/60 font-light">
Showing <%= collection.offset_value + 1 %> to
<%= collection.last_page? ? collection.total_count : collection.offset_value + collection.limit_value %>
of <%= collection.total_count %> <%= collection_name || 'items' %>
</div>
</div>
<% end %>

View File

@ -1,9 +1,3 @@
<% content_for :head do %>
<script type="application/ld+json">
<%= weather_art_schema(@weather_art) %>
</script>
<% end %>
<div class="min-h-screen"> <div class="min-h-screen">
<!-- 返回导航 --> <!-- 返回导航 -->
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">

View File

@ -1,36 +1,33 @@
class BatchGenerateWeatherArtsWorker class BatchGenerateWeatherArtsWorker
include Sidekiq::Worker include Sidekiq::Worker
GENERATION_INTERVAL = 6.hours
MAX_DURATION = 50.minutes
SLEEP_DURATION = 3.seconds
def perform(*args) def perform(*args)
start_time = Time.current start_time = Time.current
max_duration = 50.minutes
cities_to_process = get_eligible_cities cities_to_process = get_eligible_cities
cities_to_process.each do |city| cities_to_process.each do |city|
break if Time.current - start_time > MAX_DURATION break if Time.current - start_time > max_duration
Rails.logger.info "Generating weather art for #{city.name}"
GenerateWeatherArtWorker.perform_async(city.id) GenerateWeatherArtJob.perform_now(city)
sleep SLEEP_DURATION sleep 1.minute # 确保不超过API限制
end end
end end
private private
def get_eligible_cities def get_eligible_cities
cutoff_time = Time.current - GENERATION_INTERVAL
City.active City.active
.joins("LEFT JOIN ( .where(active: true)
SELECT city_id, MAX(created_at) as last_generation_time .where("last_weather_fetch IS NULL OR last_weather_fetch < ?", Date.today)
FROM weather_arts # .select { |city| early_morning_in_timezone?(city.timezone) }
GROUP BY city_id
) latest_arts ON cities.id = latest_arts.city_id")
.where("latest_arts.last_generation_time IS NULL OR latest_arts.last_generation_time < ?", cutoff_time)
.order(:priority)
end end
# def early_morning_in_timezone?(timezone)
# return false if timezone.blank?
# time = Time.current.in_time_zone(timezone)
# time.hour == 2
# end
end end

View File

@ -1,68 +1,43 @@
class GenerateWeatherArtWorker class GenerateWeatherArtWorker
include Sidekiq::Worker def perform(*args)
city = args[0]
return if city.last_weather_fetch&.today?
def perform(city_id) weather_service = WeatherService.new
@city = City.find(city_id) ai_service = AiService.new
weather_data = fetch_weather_data # 获取天气数据
weather_data = weather_service.get_weather(city.latitude, city.longitude)
return unless weather_data return unless weather_data
prompt = generate_prompt(weather_data) # 生成提示词
prompt = ai_service.generate_prompt(city, weather_data)
return unless prompt return unless prompt
image_url = generate_image(prompt) # 生成图像
image_url = ai_service.generate_image(prompt)
return unless image_url return unless image_url
create_weather_art(weather_data, prompt, image_url) # 创建天气艺术记录
rescue StandardError => e weather_art = city.weather_arts.create!(
Rails.logger.error "Error generating weather art for city #{city_id}: #{e.message}" weather_date: Date.today,
Rails.logger.error e.backtrace.join("\n") **weather_data,
end prompt: prompt
)
private # 下载并附加图像
tempfile = Down.download(image_url)
weather_art.image.attach(
io: tempfile,
filename: "#{city.country.name}-#{city.name.parameterize}-#{Time.current.strftime('%Y%m%d-%H%M%S')}.png"
)
attr_reader :city # 更新城市状态
city.update!(
def fetch_weather_data last_weather_fetch: Time.current,
WeatherService.new.get_weather(city.latitude, city.longitude) last_image_generation: Time.current
end )
rescue => e
def generate_prompt(weather_data) Rails.logger.error "Error generating weather art for #{city.name}: #{e.message}"
AiService.new.generate_prompt(city, weather_data)
end
def generate_image(prompt)
AiService.new.generate_image(prompt)
end
def create_weather_art(weather_data, prompt, image_url)
tempfile = nil
ActiveRecord::Base.transaction do
weather_art = city.weather_arts.create!(
weather_date: Date.today,
prompt: prompt,
**weather_data
)
tempfile = Down.download(image_url)
weather_art.image.attach(
io: File.open(tempfile.path),
filename: generate_filename,
content_type: "image/png"
)
weather_art
end
ensure
if tempfile
tempfile.close
tempfile.unlink
end
end
def generate_filename
"#{city.country.name}-#{city.name.parameterize}-#{Time.current.strftime('%Y%m%d-%H%M%S')}.png"
end end
end end

View File

@ -1,39 +0,0 @@
class RefreshSitemapWorker
include Sidekiq::Worker
def perform
host = Rails.env.production? ? "https://todayaiweather.com" : "http://127.0.0.1:3000"
Rails.application.routes.default_url_options[:host] = host
SitemapGenerator::Sitemap.default_host = host
SitemapGenerator::Sitemap.create do
add root_path, changefreq: "daily", priority: 1.0
add cities_path, changefreq: "daily", priority: 0.9
add arts_path, changefreq: "daily", priority: 0.9
City.find_each do |city|
add city_path(city),
changefreq: "daily",
priority: 0.8,
lastmod: city.updated_at
end
WeatherArt.includes(:city).find_each do |art|
if art.image.attached?
add city_weather_art_path(art.city, art),
changefreq: "daily",
priority: 0.7,
lastmod: art.updated_at,
images: [ {
loc: url_for(art.image),
title: "#{art.city.name} Weather Art - #{art.weather_date.strftime('%B %d, %Y')}"
} ]
end
end
end
SitemapGenerator::Sitemap.ping_search_engines if Rails.env.production?
Rails.logger.info "Sitemap has been generated successfully"
rescue => e
Rails.logger.error "Error refreshing sitemap: #{e.message}"
end
end

View File

@ -1,16 +1,15 @@
version: '3.8'
services: services:
web: web:
image: songtianlun/today_ai_weather:latest image: songtianlun/today_ai_weather:latest
#ports: ports:
# - "3000:3000" - "2222:3000"
pull_policy: always
environment: environment:
- RAILS_ENV=production - RAILS_ENV=production
- DATABASE_URL=postgresql://postgres:${PG_PASSWORD}@db:5432/db - DATABASE_URL=postgresql://postgres:xxx@db:5432/db
- RAILS_MASTER_KEY=${RAILS_MASTER_KEY} - RAILS_MASTER_KEY=xxx
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
networks:
- dokploy-network
depends_on: depends_on:
- db - db
- redis - redis
@ -20,11 +19,9 @@ services:
command: bundle exec sidekiq command: bundle exec sidekiq
environment: environment:
- RAILS_ENV=production - RAILS_ENV=production
- DATABASE_URL=postgresql://postgres:${PG_PASSWORD}@db:5432/db - DATABASE_URL=postgresql://postgres:xxx@db:5432/db
- RAILS_MASTER_KEY=${RAILS_MASTER_KEY} - RAILS_MASTER_KEY=xxx
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
networks:
- dokploy-network
depends_on: depends_on:
- db - db
- redis - redis
@ -32,21 +29,13 @@ services:
db: db:
image: postgres:16 image: postgres:16
volumes: volumes:
- ../taw_data/pg:/var/lib/postgresql/data - ./data/pg:/var/lib/postgresql/data
networks:
- dokploy-network
environment: environment:
- POSTGRES_PASSWORD=${PG_PASSWORD} - POSTGRES_PASSWORD=xxx
- POSTGRES_DB=db - POSTGRES_DB=db
redis: redis:
image: redis:7-alpine image: redis:7-alpine
volumes: volumes:
- ../taw_data/redis:/data - ./data/redis:/data
networks: command: redis-server --appendonly yes
- dokploy-network
command: redis-server --appendonly yes
networks:
dokploy-network:
external: true

View File

@ -1,14 +0,0 @@
# frozen_string_literal: true
Kaminari.configure do |config|
# config.default_per_page = 25
# config.max_per_page = nil
# config.window = 4
# config.outer_window = 0
# config.left = 0
# config.right = 0
# config.page_method_name = :page
# config.param_name = :page
# config.max_pages = nil
# config.params_on_first_page = false
end

View File

@ -1,52 +0,0 @@
# frozen_string_literal: true
# Use this setup block to configure all options available in MetaTags.
MetaTags.configure do |config|
config.title_limit = 70
config.description_limit = 160
config.keywords_limit = 255
# How many characters should the title meta tag have at most. Default is 70.
# Set to nil or 0 to remove limits.
# config.title_limit = 70
# When true, site title will be truncated instead of title. Default is false.
# config.truncate_site_title_first = false
# Add HTML attributes to the <title> HTML tag. Default is {}.
# config.title_tag_attributes = {}
# Natural separator when truncating. Default is " " (space character).
# Set to nil to disable natural separator.
# This also allows you to use a whitespace regular expression (/\s/) or
# a Unicode space (/\p{Space}/).
# config.truncate_on_natural_separator = " "
# Maximum length of the page description. Default is 300.
# Set to nil or 0 to remove limits.
# config.description_limit = 300
# Maximum length of the keywords meta tag. Default is 255.
# config.keywords_limit = 255
# Default separator for keywords meta tag (used when an Array passed with
# the list of keywords). Default is ", ".
# config.keywords_separator = ', '
# When true, keywords will be converted to lowercase, otherwise they will
# appear on the page as is. Default is true.
# config.keywords_lowercase = true
# When true, the output will not include new line characters between meta tags.
# Default is false.
# config.minify_output = false
# When false, generated meta tags will be self-closing (<meta ... />) instead
# of open (`<meta ...>`). Default is true.
# config.open_meta_tags = true
# List of additional meta tags that should use "property" attribute instead
# of "name" attribute in <meta> tags.
# config.property_tags.push(
# 'x-hearthstone:deck',
# )
end

View File

@ -6,7 +6,6 @@ Rails.application.routes.draw do
resources :cities, only: [ :index, :show ] do resources :cities, only: [ :index, :show ] do
resources :weather_arts, path: "weather", only: [ :show ], param: :slug resources :weather_arts, path: "weather", only: [ :show ], param: :slug
end end
resources :arts, only: [ :index ]
# namespace :admin do # namespace :admin do
# resources :cities # resources :cities
@ -23,7 +22,7 @@ Rails.application.routes.draw do
# mount Sidekiq::Web => '/sidekiq' # mount Sidekiq::Web => '/sidekiq'
authenticate :admin_user do authenticate :admin_user do
mount Sidekiq::Web => "/admin/tasks" mount Sidekiq::Web => "/sidekiq"
end end
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

View File

@ -1,12 +1,5 @@
batch_generate_weather: batch_generate_weather:
cron: '0 */1 * * *' cron: '0 */2 * * *'
class: BatchGenerateWeatherArtsWorker class: BatchGenerateWeatherArtsWorker
description: "Generate weather arts every 2 hours" description: "Generate weather arts every 2 hours"
enabled: true enabled: true
refresh_sitemap:
cron: '0 5 * * *'
class: RefreshSitemapWorker
queue: default
description: "Refresh sitemap daily"
enabled: true

View File

@ -1,50 +0,0 @@
# Set the host name for URL creation
host = Rails.env.production? ? "https://todayaiweather.com" : "http://127.0.0.1:3000"
SitemapGenerator::Sitemap.default_host = host
SitemapGenerator::Sitemap.create do
add root_path, changefreq: "daily", priority: 1.0
add cities_path, changefreq: "daily", priority: 0.9
add arts_path, changefreq: "daily", priority: 0.9
City.find_each do |city|
add city_path(city),
changefreq: "daily",
priority: 0.8,
lastmod: city.updated_at
end
WeatherArt.includes(:city).find_each do |art|
add city_weather_art_path(art.city, art),
changefreq: "daily",
priority: 0.7,
lastmod: art.updated_at,
images: [ {
loc: url_for(art.image),
title: "#{art.city.name} Weather Art - #{art.weather_date.strftime('%B %d, %Y')}"
} ] if art.image.attached?
end
# Put links creation logic here.
#
# The root path '/' and sitemap index file are added automatically for you.
# Links are added to the Sitemap in the order they are specified.
#
# Usage: add(path, options={})
# (default options are used if you don't specify)
#
# Defaults: :priority => 0.5, :changefreq => 'weekly',
# :lastmod => Time.now, :host => default_host
#
# Examples:
#
# Add '/articles'
#
# add articles_path, :priority => 0.7, :changefreq => 'daily'
#
# Add all articles:
#
# Article.find_each do |article|
# add article_path(article), :lastmod => article.updated_at
# end
end

View File

@ -1,8 +0,0 @@
class RemoveLastFetchFieldsFromCities < ActiveRecord::Migration[8.0]
def change
remove_column :cities, :last_weather_fetch
remove_column :cities, :last_image_generation
add_index :weather_arts, [ :city_id, :weather_date ]
end
end

5
db/schema.rb generated
View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_01_23_155234) do ActiveRecord::Schema[8.0].define(version: 2025_01_22_053220) do
create_table "active_admin_comments", force: :cascade do |t| create_table "active_admin_comments", force: :cascade do |t|
t.string "namespace" t.string "namespace"
t.text "body" t.text "body"
@ -72,6 +72,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_23_155234) do
t.boolean "active" t.boolean "active"
t.integer "priority" t.integer "priority"
t.string "timezone" t.string "timezone"
t.datetime "last_weather_fetch"
t.datetime "last_image_generation"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "slug" t.string "slug"
@ -130,7 +132,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_23_155234) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "slug" t.string "slug"
t.index ["city_id", "weather_date"], name: "index_weather_arts_on_city_id_and_weather_date"
t.index ["city_id"], name: "index_weather_arts_on_city_id" t.index ["city_id"], name: "index_weather_arts_on_city_id"
t.index ["slug"], name: "index_weather_arts_on_slug", unique: true t.index ["slug"], name: "index_weather_arts_on_slug", unique: true
end end

View File

@ -8,7 +8,9 @@ City.create!([
country: australia, country: australia,
timezone: 'Australia/Sydney', timezone: 'Australia/Sydney',
active: true, active: true,
priority: 80 priority: 80,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Melbourne', name: 'Melbourne',
@ -17,6 +19,8 @@ City.create!([
country: australia, country: australia,
timezone: 'Australia/Melbourne', timezone: 'Australia/Melbourne',
active: true, active: true,
priority: 75 priority: 75,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,6 +8,8 @@ City.create!([
country: bangladesh, country: bangladesh,
timezone: 'Asia/Dhaka', timezone: 'Asia/Dhaka',
active: true, active: true,
priority: 85 priority: 85,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,6 +8,8 @@ City.create!([
country: brazil, country: brazil,
timezone: 'America/Sao_Paulo', timezone: 'America/Sao_Paulo',
active: true, active: true,
priority: 80 priority: 80,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -6,5 +6,7 @@ City.create!(
priority: 50, priority: 50,
country: canada, country: canada,
timezone: 'America/Toronto', timezone: 'America/Toronto',
active: true active: true,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
) )

View File

@ -8,7 +8,9 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Beijing', name: 'Beijing',
@ -17,7 +19,9 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Shenzhen', name: 'Shenzhen',
@ -26,7 +30,9 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Guangzhou', name: 'Guangzhou',
@ -35,7 +41,9 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Chengdu', name: 'Chengdu',
@ -44,7 +52,9 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Tianjin', name: 'Tianjin',
@ -53,7 +63,9 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Wuhan', name: 'Wuhan',
@ -62,7 +74,9 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Dongguan', name: 'Dongguan',
@ -71,7 +85,9 @@ City.create!([
country: china, country: china,
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Chongqing', name: 'Chongqing',
@ -81,6 +97,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: "Xi'an", name: "Xi'an",
@ -90,6 +108,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Hangzhou', name: 'Hangzhou',
@ -99,6 +119,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Foshan', name: 'Foshan',
@ -108,6 +130,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Nanjing', name: 'Nanjing',
@ -117,6 +141,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Hong Kong', name: 'Hong Kong',
@ -126,6 +152,8 @@ City.create!([
timezone: 'Asia/Hong_Kong', timezone: 'Asia/Hong_Kong',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Shenyang', name: 'Shenyang',
@ -135,6 +163,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Zhengzhou', name: 'Zhengzhou',
@ -144,6 +174,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Qingdao', name: 'Qingdao',
@ -153,6 +185,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Suzhou', name: 'Suzhou',
@ -162,6 +196,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Changsha', name: 'Changsha',
@ -171,6 +207,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Jinan', name: 'Jinan',
@ -180,6 +218,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Kunming', name: 'Kunming',
@ -189,6 +229,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Harbin', name: 'Harbin',
@ -198,6 +240,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Shijiazhuang', name: 'Shijiazhuang',
@ -207,6 +251,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Hefei', name: 'Hefei',
@ -216,6 +262,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Dalian', name: 'Dalian',
@ -225,6 +273,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Xiamen', name: 'Xiamen',
@ -234,6 +284,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Nanning', name: 'Nanning',
@ -243,6 +295,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Changchun', name: 'Changchun',
@ -252,6 +306,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Taiyuan', name: 'Taiyuan',
@ -261,6 +317,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'New Taipei City', name: 'New Taipei City',
@ -270,6 +328,8 @@ City.create!([
timezone: 'Asia/Taipei', timezone: 'Asia/Taipei',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Guiyang', name: 'Guiyang',
@ -279,6 +339,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Wuxi', name: 'Wuxi',
@ -288,6 +350,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Shantou', name: 'Shantou',
@ -297,6 +361,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Ürümqi', name: 'Ürümqi',
@ -306,6 +372,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Zhongshan', name: 'Zhongshan',
@ -315,6 +383,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Ningbo', name: 'Ningbo',
@ -324,6 +394,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Fuzhou', name: 'Fuzhou',
@ -333,6 +405,8 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Nanchang', name: 'Nanchang',
@ -342,5 +416,7 @@ City.create!([
timezone: 'Asia/Shanghai', timezone: 'Asia/Shanghai',
active: true, active: true,
priority: 100, priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,6 +8,8 @@ City.create!([
country: egypt, country: egypt,
timezone: 'Africa/Cairo', timezone: 'Africa/Cairo',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,6 +8,8 @@ City.create!([
country: france, country: france,
timezone: 'Europe/Paris', timezone: 'Europe/Paris',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,7 +8,9 @@ City.create!([
country: germany, country: germany,
timezone: 'Europe/Berlin', timezone: 'Europe/Berlin',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Berlin', name: 'Berlin',
@ -17,6 +19,8 @@ City.create!([
country: germany, country: germany,
timezone: 'Europe/Berlin', timezone: 'Europe/Berlin',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,7 +8,9 @@ City.create!([
country: india, country: india,
timezone: 'Asia/Kolkata', timezone: 'Asia/Kolkata',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Bengaluru', name: 'Bengaluru',
@ -17,6 +19,8 @@ City.create!([
country: india, country: india,
timezone: 'Asia/Kolkata', timezone: 'Asia/Kolkata',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,7 +8,9 @@ City.create!([
country: japan, country: japan,
timezone: 'Asia/Tokyo', timezone: 'Asia/Tokyo',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Yokohama', name: 'Yokohama',
@ -17,6 +19,8 @@ City.create!([
country: japan, country: japan,
timezone: 'Asia/Tokyo', timezone: 'Asia/Tokyo',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,6 +8,8 @@ City.create!([
country: mexico, country: mexico,
timezone: 'America/Mexico_City', timezone: 'America/Mexico_City',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,6 +8,8 @@ City.create!([
country: nigeria, country: nigeria,
timezone: 'Africa/Lagos', timezone: 'Africa/Lagos',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,6 +8,8 @@ City.create!([
country: pakistan, country: pakistan,
timezone: 'Asia/Karachi', timezone: 'Asia/Karachi',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,7 +8,9 @@ City.create!([
country: russia, country: russia,
timezone: 'Europe/Moscow', timezone: 'Europe/Moscow',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Sankt Petersburg', name: 'Sankt Petersburg',
@ -17,6 +19,8 @@ City.create!([
country: russia, country: russia,
timezone: 'Europe/Moscow', timezone: 'Europe/Moscow',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,6 +8,8 @@ City.create!([
country: saudi_arabia, country: saudi_arabia,
timezone: 'Asia/Riyadh', timezone: 'Asia/Riyadh',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,6 +8,8 @@ City.create!([
country: singapore, country: singapore,
timezone: 'Asia/Singapore', timezone: 'Asia/Singapore',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,6 +8,8 @@ City.create!([
country: south_korea, country: south_korea,
timezone: 'Asia/Seoul', timezone: 'Asia/Seoul',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,6 +8,8 @@ City.create!([
country: thailand, country: thailand,
timezone: 'Asia/Bangkok', timezone: 'Asia/Bangkok',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,7 +8,9 @@ City.create!([
country: turkey, country: turkey,
timezone: 'Europe/Istanbul', timezone: 'Europe/Istanbul',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Ankara', name: 'Ankara',
@ -17,6 +19,8 @@ City.create!([
country: turkey, country: turkey,
timezone: 'Europe/Istanbul', timezone: 'Europe/Istanbul',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,6 +8,8 @@ City.create!([
country: uk, country: uk,
timezone: 'Europe/London', timezone: 'Europe/London',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,7 +8,9 @@ City.create!([
country: usa, country: usa,
timezone: 'America/Los_Angeles', timezone: 'America/Los_Angeles',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Chicago', name: 'Chicago',
@ -17,7 +19,9 @@ City.create!([
country: usa, country: usa,
timezone: 'America/Chicago', timezone: 'America/Chicago',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'New York City', name: 'New York City',
@ -26,7 +30,9 @@ City.create!([
country: usa, country: usa,
timezone: 'America/New_York', timezone: 'America/New_York',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Los Angeles', name: 'Los Angeles',
@ -35,6 +41,8 @@ City.create!([
country: usa, country: usa,
timezone: 'America/Los_Angeles', timezone: 'America/Los_Angeles',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -8,7 +8,9 @@ City.create!([
country: vietnam, country: vietnam,
timezone: 'Asia/Ho_Chi_Minh', timezone: 'Asia/Ho_Chi_Minh',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
}, },
{ {
name: 'Hanoi', name: 'Hanoi',
@ -17,6 +19,8 @@ City.create!([
country: vietnam, country: vietnam,
timezone: 'Asia/Ho_Chi_Minh', timezone: 'Asia/Ho_Chi_Minh',
active: true, active: true,
priority: 100 priority: 100,
last_weather_fetch: 10.days.ago,
last_image_generation: 10.days.ago
} }
]) ])

View File

@ -1,7 +1 @@
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
User-agent: *
Allow: /
Disallow: /admin/
Disallow: /admin
Sitemap: https://todayaiweather.com/sitemap.xml.gz

View File

@ -1,7 +0,0 @@
require "test_helper"
class ArtsControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end