Compare commits

..

32 Commits

Author SHA1 Message Date
7612dd6bd9 refactor: tidy up code style and remove unused fields
Some checks are pending
CI / scan_ruby (push) Waiting to run
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
Docker / docker (push) Waiting to run
- Added a space in the array passed to `add_index` in the migration
- Removed unused columns `last_weather_fetch` and `last_image_generation` from the `cities` table
- Ensured consistent code style in the `GenerateWeatherArtWorker` and added required newline at the end of files

These changes improve code readability and maintainability while ensuring that
no unnecessary fields exist in the database schema.
2025-01-24 00:25:06 +08:00
b4af78aa77 feat: add logging and refactor image attachment
- Add logging to track the generation of weather art for each city.
- Refactor image attachment process to streamline the code by removing the
  separate method for attaching images.
- Ensure proper handling of the temporary file used for image processing.

These changes improve observability during the weather art generation
process and encapsulate the image attachment logic within the primary
method, reducing the overhead of a method call. The adjustments also
ensure that temporary files are managed correctly to prevent resource
leaks.
2025-01-24 00:23:09 +08:00
b05cf10017 refactor: simplify city weather generation logic
- Introduced constants for configuration settings such as generation interval, maximum duration, and sleep duration.
- Updated the `perform` method to utilize these constants for better readability and maintainability.
- Refactored the `perform` method in `GenerateWeatherArtWorker` to improve flow and error handling by creating separate methods for fetching weather data, generating prompts, images, and handling database transactions.
- Cleaned up city seeding data by removing unnecessary fields while maintaining required functionality.

These changes improve the overall readability of the code and make it easier to adjust the behavior of the workers in the future without digging through the logic.
2025-01-24 00:14:29 +08:00
06a861c639 refactor: clean up city model and adjust worker timing
- Implement caching methods for last weather fetch and image generation
- Adjust sleep duration in BatchGenerateWeatherArtsWorker from 10 seconds to 3 seconds
- Remove unused fields `last_weather_fetch` and `last_image_generation` from the cities table
- Add index on the weather_arts table for optimized querying

This refactor improves data retrieval performance for weather data
associated with cities. Caching reduces database load while the
worker modification allows for faster iterations in generating
weather arts without significantly impacting performance.
2025-01-23 23:59:48 +08:00
2cd23a6047 fix: update ownership permissions in Dockerfile
- Added 'public' directory to the chown command to ensure the
  proper ownership for runtime files.
- This change enhances the security by ensuring that all
  necessary directories are owned by the designated non-root
  user.

The previous behavior did not account for the 'public'
directory, which could lead to permission issues at runtime.
2025-01-23 19:53:50 +08:00
80a75d3fbb feat: add Google Analytics tracking code
- Include gtag.js for Google Analytics
- Setup dataLayer for tracking events
- Configure Google Analytics with unique ID

This commit integrates Google Analytics into the web
application to enable tracking of user interactions and
site usage. It sets up the necessary scripts and initializes
the tracking code with the provided unique ID, improving
analytics capabilities.
2025-01-23 19:49:59 +08:00
f477f205ab fix: update default host in sitemap generator and refresh sitemap on worker
- update default host in sitemap generator
- refresh sitemap on worker with new host

These changes allow the sitemap to be correctly generated for different environments and to handle the new host correctly, which was the motivation behind this update. No other side effects are expected.
2025-01-23 19:40:08 +08:00
1f47ba59c9 style: format code for consistency
- Ensure consistent use of double quotes for strings in the
  Gemfile and sitemap configuration files.
- Add spaces for better readability in array declarations
  within the RefreshSitemapWorker.

These changes improve the readability of the code without
changing any functionality. Adhering to a consistent coding
style helps maintainability and team collaboration.
2025-01-23 19:03:14 +08:00
6544f0247c chore: update robots.txt for better indexing
- Add disallow rules for /admin/ to prevent indexing
- Update sitemap URL to point to the correct domain

These changes improve site indexing by ensuring that sensitive
admin pages are not accessible to search engine crawlers
and updating the sitemap to reflect the accurate URL, helping
discovery and SEO efforts.
2025-01-23 19:03:00 +08:00
a0516f731c feat: add SEO meta tags and sitemap generation
- Introduced `SeoConcern` module to handle SEO meta tags
- Integrated `meta-tags` gem for customizable meta tags
- Created `RefreshSitemapWorker` to automate sitemap updates
- Added relevant meta tags in controllers for weather art and cities
- Configured sitemap generation settings

These changes improve the SEO of the application by ensuring that
pages have appropriate meta tags. Additionally, a sitemap is now
generated and refreshed daily, enhancing site visibility to search
engines.
2025-01-23 19:02:52 +08:00
18f751938f feat: update application layout
- Add language attribute to HTML tag
- Update meta viewport attribute for accessibility
- Include Plausible JavaScript tracking script for analytics

This change improves application accessibility by setting the language attribute for the HTML tag, and it enhances data collection capabilities by integrating Plausible analytics tracking.
2025-01-23 17:46:37 +08:00
2759646145 chore: update docker compose file
- Update compose.yaml to pull policy: always for production environment
- Added RAILS_ENV environment variable for production

Changes impact the overall system functionality by ensuring containers are always pulled from the latest images, and provide clear environment variables for the production environment.
2025-01-23 17:44:03 +08:00
665f6f29b6 refactor: change Sidekiq route to admin/tasks
- Update route for Sidekiq web interface from '/sidekiq'
  to '/admin/tasks'

This change improves the coherence of the application's routing
by placing the Sidekiq interface under a more descriptive
namespace. It consolidates admin-related tasks under a common
path, enhancing the organization of the routing structure.
2025-01-23 17:34:45 +08:00
bafb90f5fb style: format code style in Gemfile and controllers
- Adjust spacing around the quotes in the Gemfile
- Standardize spacing in the arts_controller for improved readability
- Modify routes file for consistent array formatting

These changes enhance the consistency of code style across the project without altering any functionality or behavior.
2025-01-23 17:33:01 +08:00
f33fb4d2ba feat: add pagination to cities and arts index
- Implement pagination for the cities index view.
- Add shared pagination partial to reduce code duplication.
- Modify arts index view to utilize the new pagination.
- Update cities controller to include pagination logic.

These updates improve usability by allowing better navigation through larger datasets, ensuring users can easily access and view items across multiple pages.
2025-01-23 17:30:05 +08:00
a4de04874d feat: implement filtering and pagination for weather arts
- Add region selection for filtering weather arts by region
- Implement sorting options for newest and oldest entries
- Update pagination to show links for each page when applicable
- Adjust the number of items displayed per page to 10

This commit enhances the user experience by allowing users to filter
and sort weather arts based on their preferences. It also improves
the pagination logic to provide more manageable navigation through
large datasets, making it easier for users to find the artworks they
are interested in.
2025-01-23 14:13:32 +08:00
3a6d247451 feat: add pagination to weather arts gallery
- Introduce ArtsController with index action
- Create index view for displaying weather arts
- Implement Kaminari for pagination functionality
- Add necessary routes for accessing arts
- Update Gemfile to include Kaminari gem
- Create views for pagination controls

This implementation enhances the Weather Arts Gallery, allowing users to view and navigate through a collection of AI-generated weather arts easily. Pagination controls have been added to improve usability.
2025-01-23 14:10:13 +08:00
b3089856c2 chore: update docker network configuration
- Change network from 'net1' to 'dokploy-network'
- Make 'dokploy-network' an external network
- Update references in multiple services

This update modifies the docker-compose configuration to
utilize an external network named 'dokploy-network' instead
of the previously defined 'net1'. This change is intended
to improve network management and integration with existing
infrastructure.
2025-01-23 10:49:53 +08:00
ccb48a387b feat: add network configuration to Docker compose
- Define a custom network 'net1' for services
- Attach services to 'net1' network
- Ensure connectivity between application components

This commit enhances the Docker Compose setup by introducing a
custom network. This isolation can help manage service
dependencies better and improve communication between services,
establishing clearer boundaries. It simplifies networking
configuration and lays the groundwork for further scaling
if needed.
2025-01-23 10:44:37 +08:00
e1f9118ead chore: remove version from compose file
- Removed the version declaration from the compose.yaml file.

This change simplifies the configuration by removing a version number,
allowing Docker Compose to use the latest compatible version. This
change does not impact the functionality of the services defined in
this file.
2025-01-23 10:31:50 +08:00
af95c2e55f chore: update environment variables in compose.yaml
- Replace hardcoded DATABASE_URL and RAILS_MASTER_KEY with
  environment variables.
- Change POSTGRES_PASSWORD to utilize an environment
  variable instead of a hardcoded value.

These changes enhance security by ensuring sensitive
information is not exposed in the configuration files,
allowing for better practices in managing environment
variables.
2025-01-23 10:22:07 +08:00
91e62234b4 chore: comment out port configuration for web service
- Disabled port mapping for the web service in the Docker Compose file
- This may affect how the application is accessed outside of the container
- The decision to comment out the ports could be for environmental reasons or to avoid port conflicts
2025-01-23 10:19:27 +08:00
6eb8d10965 chore: update port mapping in docker-compose 2025-01-23 10:18:49 +08:00
97d7930daa chore: update redis data volume path
- Changed Redis data volume path from '../daw_data/redis' to '../taw_data/redis'.

This update reflects a restructuring in the project directory for better
data organization and may require corresponding adjustments in other
configuration files to ensure data consistency.
2025-01-23 10:13:45 +08:00
a15bc349a2 feat: update docker compose configuration
- Update PostgreSQL data directory to point to taw_data/pg
- Update Redis data directory to point to daw_data/redis

This change updates the Docker compose file to use different data directories for PostgreSQL and Redis. It improves the overall organization and clarity of the configuration.
2025-01-23 10:11:39 +08:00
5fa49d97ca fix: reduce sleep time in weather arts worker
- Change sleep duration from 1 minute to 10 seconds in
  BatchGenerateWeatherArtsWorker.

This change addresses API limitations by reducing the wait time
between job submissions, thereby increasing the efficiency of
the batch processing for generating weather arts.
2025-01-23 09:58:08 +08:00
dffac6c665 feat: modify BatchGenerateWeatherArtsWorker to use city ID
- Change BatchGenerateWeatherArtsWorker to use city ID instead of city object
- Update API endpoint to accept city ID
- Improve performance by reducing database queries

This change improves the performance of the BatchGenerateWeatherArtsWorker by reducing database queries and increasing efficiency. It is a necessary modification to ensure the worker functions correctly and efficiently.
2025-01-23 09:50:17 +08:00
c5101fb822 feat: add background processing for weather art generation
- Include Sidekiq::Worker for asynchronous task execution
- Implement condition to skip execution if weather data is up to date

These changes enable efficient weather art generation by leveraging
background processing, thereby improving overall application
performance and responsiveness.
2025-01-23 09:43:37 +08:00
fd6292a81e chore: update Docker workflow configuration
- Simplify step definitions by removing unnecessary
  empty lines.
- Ensure compatibility with Docker Hub through the
  addition of a comment to clarify the usage of the
  REGISTRY variable.

These changes enhance the readability of the workflow file
without affecting its functionality. They make the CI/CD
pipeline clearer for future contributors by simplifying
structure and providing contextual information.
2025-01-23 09:36:37 +08:00
c529f5fd7b refactor: update job class for weather art generation
- Change from using GenerateWeatherArtJob to GenerateWeatherArtWorker
- Maintain asynchronous job processing

This refactoring improves the clarity of the job class being used
for generating weather art and adheres to project conventions. The
change does not affect the API rate limits as the sleep duration
remains the same.
2025-01-23 09:28:31 +08:00
c1fa16c690 refactor: change weather art job processing method
- Replace synchronous job processing with asynchronous
- Ensure processing does not exceed API rate limits

This change allows for better performance by offloading the
queue management to Sidekiq, enabling more efficient
parallel processing of weather art generation. It also
prevents potential API rate limit violations by maintaining
a delay between requests.
2025-01-23 09:20:49 +08:00
2d5521c3dc chore: adjust sidekiq worker schedule
- Update `BatchGenerateWeatherArtsWorker` cron job to run every 1 hour
- This change will result in weather arts being generated more frequently
2025-01-23 09:19:23 +08:00
62 changed files with 865 additions and 279 deletions

View File

@ -8,6 +8,7 @@ on:
- v*
env:
# Use docker.io for Docker Hub if empty
REGISTRY: docker.io
IMAGE_NAME: ${{ github.event.repository.name }}
@ -15,51 +16,36 @@ jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
-
name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get Version
fetch-depth: 0 # 获取完整的 git history 以便生成正确的 tag
-
name: Get Version
id: get_version
run: |
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'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{github.actor}}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
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
-
name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
tags: |
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:latest
${{ 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,3 +38,5 @@
/node_modules
.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 groupadd --system --gid 1000 rails && \
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
chown -R rails:rails db log storage tmp
chown -R rails:rails db log storage tmp public
USER 1000:1000
# Entrypoint prepares the database.

View File

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

View File

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

View File

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

View File

@ -0,0 +1,21 @@
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,6 +1,5 @@
class CitiesController < ApplicationController
def index
@cities = City.all.order(:name)
@regions = Region.includes(:countries).order(:name)
@cities = City.includes(:country, country: :region).active.order(:name)
@ -13,9 +12,26 @@ class CitiesController < ApplicationController
@current_country = Country.friendly.find(params[:country])
@cities = @cities.by_country(@current_country.id)
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
def show
@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

View File

@ -2,5 +2,10 @@ class HomeController < ApplicationController
def index
@latest_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(6)
@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

View File

@ -2,5 +2,13 @@ class WeatherArtsController < ApplicationController
def show
@city = City.friendly.find(params[:city_id])
@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

View File

@ -1,2 +1,24 @@
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

View File

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

View File

@ -5,6 +5,8 @@ class City < ApplicationRecord
has_many :weather_arts, dependent: :destroy
delegate :region, to: :country
validates :name, presence: true
validates :latitude, presence: true
validates :longitude, presence: true
@ -46,6 +48,20 @@ 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" ]
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
weather_arts.order(weather_date: :desc).first
end

View File

@ -0,0 +1,172 @@
<!-- 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,7 +12,6 @@
</div>
<% end %>
<!-- 标题内容 -->
<div class="relative pt-24 pb-32">
<div class="container mx-auto px-4">
<div class="max-w-3xl mx-auto text-center space-y-6">
@ -38,13 +37,10 @@
</div>
</div>
<!-- 筛选导航 - 使用下拉菜单 -->
<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="py-3 flex items-center justify-between gap-4">
<!-- 左侧筛选器 -->
<div class="flex items-center gap-4">
<!-- 区域选择下拉框 -->
<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">
@ -73,7 +69,6 @@
</ul>
</div>
<!-- 国家选择下拉框 (如果选择了区域) -->
<% if @current_region %>
<div class="dropdown">
<button class="btn btn-ghost gap-2">
@ -104,7 +99,6 @@
<% end %>
</div>
<!-- 右侧结果统计 -->
<div class="text-sm text-base-content/70">
<%= @cities.count %> <%= 'city'.pluralize(@cities.count) %>
<% if @current_country %>
@ -117,10 +111,16 @@
</div>
</div>
<!-- 城市网格 -->
<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">
<%= render partial: 'city', collection: @cities %>
</div>
<%= render 'shared/pagination',
collection: @cities,
collection_name: 'cities' %>
</div>
</div>
</div>

View File

@ -58,3 +58,11 @@
</div>
</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>

View File

@ -0,0 +1,11 @@
<%# 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

@ -0,0 +1,8 @@
<%# 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

@ -0,0 +1,11 @@
<%# 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

@ -0,0 +1,11 @@
<%# 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

@ -0,0 +1,12 @@
<%# 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

@ -0,0 +1,25 @@
<%# 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

@ -0,0 +1,11 @@
<%# 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,10 +1,23 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title><%= content_for(:title) || "Today Ai Weather" %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="apple-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 %>
<%= csp_meta_tag %>
@ -20,6 +33,18 @@
<%# Includes all stylesheet files in app/assets/stylesheets %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
<%= 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>
<body class="min-h-screen bg-base-100 font-sans">
@ -33,6 +58,7 @@
</div>
<div class="flex-none">
<%= 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>

View File

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

View File

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

View File

@ -1,43 +1,68 @@
class GenerateWeatherArtWorker
def perform(*args)
city = args[0]
return if city.last_weather_fetch&.today?
include Sidekiq::Worker
weather_service = WeatherService.new
ai_service = AiService.new
def perform(city_id)
@city = City.find(city_id)
# 获取天气数据
weather_data = weather_service.get_weather(city.latitude, city.longitude)
weather_data = fetch_weather_data
return unless weather_data
# 生成提示词
prompt = ai_service.generate_prompt(city, weather_data)
prompt = generate_prompt(weather_data)
return unless prompt
# 生成图像
image_url = ai_service.generate_image(prompt)
image_url = generate_image(prompt)
return unless image_url
# 创建天气艺术记录
create_weather_art(weather_data, prompt, image_url)
rescue StandardError => e
Rails.logger.error "Error generating weather art for city #{city_id}: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
end
private
attr_reader :city
def fetch_weather_data
WeatherService.new.get_weather(city.latitude, city.longitude)
end
def generate_prompt(weather_data)
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,
**weather_data,
prompt: prompt
prompt: prompt,
**weather_data
)
# 下载并附加图像
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"
io: File.open(tempfile.path),
filename: generate_filename,
content_type: "image/png"
)
# 更新城市状态
city.update!(
last_weather_fetch: Time.current,
last_image_generation: Time.current
)
rescue => e
Rails.logger.error "Error generating weather art for #{city.name}: #{e.message}"
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

View File

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

View File

@ -0,0 +1,14 @@
# 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

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

View File

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

50
config/sitemap.rb Normal file
View File

@ -0,0 +1,50 @@
# 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

@ -0,0 +1,8 @@
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.
ActiveRecord::Schema[8.0].define(version: 2025_01_22_053220) do
ActiveRecord::Schema[8.0].define(version: 2025_01_23_155234) do
create_table "active_admin_comments", force: :cascade do |t|
t.string "namespace"
t.text "body"
@ -72,8 +72,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_22_053220) do
t.boolean "active"
t.integer "priority"
t.string "timezone"
t.datetime "last_weather_fetch"
t.datetime "last_image_generation"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "slug"
@ -132,6 +130,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_22_053220) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
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 ["slug"], name: "index_weather_arts_on_slug", unique: true
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1,7 @@
# 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

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