Compare commits

..

168 Commits
new ... main

Author SHA1 Message Date
31c2913ea6 refactor: adjust ordering of weather arts
Some checks failed
Docker / docker (push) Has been cancelled
CI / scan_ruby (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
- Change ordering of previous weather art to use 'id' descending
- Change ordering of next weather art to use 'id' ascending
- Remove unnecessary blank line in seeds file

These changes enhance the consistency of the weather art
navigation by using 'id' for ordering, ensuring the
correct retrieval of records.
2025-02-03 11:15:31 +08:00
884e1dfc9f feat: add navigation between weather arts
- Implement previous and next weather art navigation
- Update weather arts controller to fetch adjacent weather arts
- Modify show view to include navigation links

This update enhances the user experience by allowing users to
navigate through weather arts seamlessly. The previous and
next buttons improve accessibility, providing a smoother
browsing experience. The implementation also accounts for
situations where no adjacent weather arts exist, ensuring
clarity for users.
2025-02-03 11:09:31 +08:00
5d1846e0a0 refactor: update weather art display
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
- Remove unnecessary image tag
- Add city name to weather art title
- Improve image scaling effect

These changes aim to enhance the user experience by providing a clearer and more visually appealing representation of the weather art, including the location it represents.
2025-02-02 14:23:06 +08:00
130a8983a1
Update application.html.erb 2025-02-02 10:46:06 +08:00
d69a193e6d refactor: consolidate Docker build process
Some checks failed
CI / scan_ruby (push) Waiting to run
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
Docker / docker (push) Has been cancelled
- Removed unused Dockerfile.base and Dockerfile.build.
- Combined build steps into the main Dockerfile for improved clarity and maintenance.
- Updated base image references accordingly.

This change streamlines the Docker build process by reducing the number of Dockerfiles and improving the clarity of dependency management.
2025-02-02 00:44:36 +08:00
2c72e96977 feat: switch to using official ruby slim image
Update the Dockerfile to use the official ruby slim image, rather than a custom image. This reduces the Docker image size and improves build times.

- Update the base build and development images to use the official ruby slim image
- Remove redundant arguments in the build and development stages

Using the official ruby slim image simplifies the build process and reduces the image size, resulting in faster build times and a more maintainable Dockerfile.
2025-02-02 00:40:59 +08:00
378530cc1b feat: automate base image build for GitHub Actions
- Added new GitHub Actions to docker-main.yml to build and push base images
- Created a new Dockerfile.base with the base packages installation
- Modified the existing Dockerfile to use the new base image
- Created a new Dockerfile.build with the build stage to reduce the size of the final image
2025-02-02 00:38:11 +08:00
536b97a7da feat: update latest weather art display
- Change latest weather art limit from 6 to 20
- Shuffle the collection and display the last 10 items
- Update the section title to reflect the shuffling

This commit enhances the visibility of the latest weather art by
increasing the limit of displayed items. It randomizes the selection
of the latest art pieces for a more dynamic user experience, while the
UI title is updated to clarify this feature. This change improves user
engagement with the content.
2025-02-02 00:21:51 +08:00
f2951e2741 feat: optimize batch weather art generation
- Introduce BATCH_SIZE constant to limit processed cities
- Shuffle and limit eligible cities processing to enhance worker efficiency

This update improves the performance of the BatchGenerateWeatherArtsWorker
by ensuring that only a set number of cities are processed within the
allotted time, reducing the risk of timeouts and making the overall
system more responsive.
2025-02-01 15:01:36 +08:00
9417358625 refactor: adjust image loading in weather art view
- Update image link to use original blob instead of a resized variant
- Remove the resizing option for the image tag for better fidelity
- Modify CSS class for a smoother hover effect without scaling issues

This refactor improves the image loading behavior by allowing
full-resolution images to be loaded directly. The previous resizing
was limiting image quality, and this change enhances user experience
when viewing weather art.
2025-02-01 14:58:51 +08:00
dependabot[bot]
31bd6fd74e build(deps): bump solid_cable from 3.0.5 to 3.0.7
Bumps [solid_cable](https://github.com/rails/solid_cable) from 3.0.5 to 3.0.7.
- [Release notes](https://github.com/rails/solid_cable/releases)
- [Commits](https://github.com/rails/solid_cable/compare/v3.0.5...v3.0.7)

---
updated-dependencies:
- dependency-name: solid_cable
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-01 14:47:06 +08:00
dependabot[bot]
e36448e6d7 chore(deps): bump solid_queue from 1.1.2 to 1.1.3
Bumps [solid_queue](https://github.com/rails/solid_queue) from 1.1.2 to 1.1.3.
- [Release notes](https://github.com/rails/solid_queue/releases)
- [Commits](https://github.com/rails/solid_queue/compare/v1.1.2...v1.1.3)

---
updated-dependencies:
- dependency-name: solid_queue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-01 14:46:56 +08:00
dependabot[bot]
11df3a31d1 chore(deps): bump puma from 6.5.0 to 6.6.0
Bumps [puma](https://github.com/puma/puma) from 6.5.0 to 6.6.0.
- [Release notes](https://github.com/puma/puma/releases)
- [Changelog](https://github.com/puma/puma/blob/master/History.md)
- [Commits](https://github.com/puma/puma/compare/v6.5.0...v6.6.0)

---
updated-dependencies:
- dependency-name: puma
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-01 14:46:42 +08:00
dependabot[bot]
2c3f8e1b8e chore(deps): bump aws-sdk-s3 from 1.177.0 to 1.179.0
Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.177.0 to 1.179.0.
- [Release notes](https://github.com/aws/aws-sdk-ruby/releases)
- [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-ruby/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-01 14:46:33 +08:00
9a745dcee0 Merge branch 'dev' 2025-02-01 14:45:27 +08:00
09b3f06ad4 style: adjust pagination button sizes
- Change button classes from `btn` to `btn btn-xs` for all pagination buttons
- Ensure consistent styling across all pagination elements

These changes standardize the button sizes in the pagination component, improving the visual uniformity. The adjustment enhances user experience by making the pagination buttons more appropriately sized within the interface, without affecting functionality.
2025-02-01 14:45:17 +08:00
05449d1e7f chore: update PhotoSwipe import paths
- Change import for PhotoSwipeLightbox to 'photoswipe/lightbox'
- Change import for PhotoSwipe to 'photoswipe'

These updates reflect the new module structure of the PhotoSwipe library, ensuring that the controller utilizes the correct paths for improved functionality and maintainability.
2025-02-01 14:31:53 +08:00
905ec35fd8 feat: add photo swipe lightbox functionality
- Integrate PhotoSwipe library for enhanced image viewing
- Create PhotoSwipeLightBoxController to manage images
- Register lightbox controller in Stimulus framework
- Update views to include lightbox functionality
- Modify styles to accommodate new design elements

This commit introduces a new way for users to view images with
PhotoSwipe, improving the interactivity of the photo gallery. It
also includes adjustments to the layout and styles for better
presentation and user experience.
2025-02-01 14:19:19 +08:00
0352923c5b feat: update weather data report format
Some checks are pending
Docker / docker (push) Waiting to run
- Include actual update time in weather report
- Rename watermark worker file for clarity

This commit enhances the weather data report by
updating the format to include the actual update time
retrieved from the weather service API. Additionally,
the watermark worker file has been renamed to improve
readability and consistency in the naming convention.
2025-01-31 10:46:32 +08:00
bbf8dfc2e6 feat: add watermarking functionality to weather art
Some checks failed
Docker / docker (push) Has been cancelled
- Include the image_processing gem to handle image manipulation
- Create a background worker for adding watermarks to weather art images
- Update WeatherArt model to attach watermark images
- Add a new watermark image asset

This commit enhances the WeatherArt feature by allowing images to
have watermarks added asynchronously, improving the visual
presentation of the art. It ensures sufficient image dimensions
before processing and includes error handling for the worker.
2025-01-29 01:53:50 +08:00
15d64b94b0 Merge branch 'dev'
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
Docker / docker (push) Has been cancelled
2025-01-28 02:12:35 +08:00
62bfe8888e feat: add toggle city status action
Some checks are pending
Docker / docker (push) Waiting to run
- Implement city status toggle functionality in the Ahoy Dashboard
- Add buttons for activating and deactivating cities
- Update the UI to show the current status of cities

This commit enhances the admin dashboard by allowing
administrators to toggle the activation status of cities.
The buttons provide confirmation prompts before executing
state changes, improving user experience and preventing
accidental actions.
2025-01-28 02:12:09 +08:00
42efca67c7 Merge branch 'dev' 2025-01-28 02:01:20 +08:00
138d610c3a chore: remove unused job classes
- Delete BatchGenerateWeatherArtsJob which queued jobs for generating
  weather art for eligible cities.
- Remove CleanAhoyDataJob responsible for cleaning up old Ahoy events
  and visits.
- Eliminate GenerateWeatherArtJob that managed the generation
  and attachment of weather art images.
- Drop RefreshSitemapJob which created and uploaded XML sitemaps.

These removals suggest a shift in how these functionalities will be
handled, possibly indicating a move towards a different architecture
or integration with another service.
2025-01-28 01:50:49 +08:00
c332230709 feat: enhance city localization and timezone handling
- Update localized_name method to provide a default value for
  missing translations
- Modify timezone display in city show view to show a message
  when the timezone is undefined

These changes improve user experience by ensuring that the city
localization falls back to the city name itself if a translation
is not found, and they handle potentially missing timezone data
more gracefully.
2025-01-28 01:49:13 +08:00
f0f94de528 feat: refactor code organization and query complexity
- Simplify query for most popular inactive cities in City model
    - Minor layout changes in Admin Dashboards for Ahoy and Sidekiq tasks

    This refactoring improves code organization, reducing complexity in the City model and making it easier to read. Additionally, the Admin dashboard layouts have been simplified for a better user experience.
2025-01-28 01:44:07 +08:00
069b6d4a4f feat: add ahoy dashboard and city statistics
- Introduce a new admin dashboard for viewing Ahoy statistics.
- Display total visits, event counts, and unique visitors.
- List most popular and least popular active cities with their visit counts.
- Add a panel for recent events.
- Modify existing dashboard to include a section for inactive cities.

This commit introduces a comprehensive dashboard that helps
admin users monitor the traffic and engagement statistics of
various cities. The changes include functionality to show
active and inactive cities based on their popularity,
allowing for better insights into user engagement across
the application.
2025-01-28 01:43:59 +08:00
2a0226eb68 feat: update sidekiq tasks manager for clarity
- Rename task label from 'Batch Generate Weather Arts' to 'Generate Weather Arts' for better understanding.
- Add a new button for manual task execution of 'BatchGenerateWeatherArts'.
- Update task value in form submissions to be more descriptive, enhancing maintainability.

These changes improve the usability of the Sidekiq tasks management interface, making it more intuitive for users to identify and execute tasks. The renamed button and the clear distinction between tasks aim to reduce confusion and assist in better workflow management.
2025-01-28 01:32:56 +08:00
8cd4c50024 fix: update log level for user agent logging
- Change logging to use Rails.logger.debug instead of
  Rails.logger.debugger for better compatibility.
- Remove unnecessary extra lines in the Sidekiq jobs file.

This commit ensures that log messages are recorded efficiently and
with the correct log level, improving logging practices in the
application.
2025-01-28 01:28:07 +08:00
4020f89271 style: change logging level for user agent
- Replace logger.info with logger.debugger for user agent logging.
- Commented out redundant info logging for blocked browsers.

This change improves the logging detail level for the user agent by
utilizing the debugger log method instead of info, which provides more
context during debugging sessions.
2025-01-28 01:26:31 +08:00
29de36f5fb chore: comment out deprecated allow_browser code
- Commented out the `allow_browser` lines to prevent
  disabling access for unsupported browsers.
- This change maintains previous behavior without removing
  the code permanently, allowing for future reference.
- The previous implementation was causing issues with
  newer browser versions, prompting the need for a review
  of browser support policies.
2025-01-28 01:25:40 +08:00
2e438166ee feat: implement sidekiq task management
- Add manual task execution buttons for BatchGenerateWeatherArtsWorker, RefreshSitemapWorker, and CleanAhoyDataWorker
- Improve browser blocking functionality in ApplicationController
- Refactor Sidekiq jobs management to include statistics and task execution
- Update various jobs to conform to new standards

This feature allows for more fine-grained control over Sidekiq tasks and improves the overall user experience.
2025-01-28 01:24:49 +08:00
bf2ff282bb refactor: rename workers to jobs
- Change class names from Worker to Job for better alignment
  with Rails convention.
- Includes changes in BatchGenerateWeatherArtsJob,
  CleanAhoyDataJob, GenerateWeatherArtJob, and
  RefreshSitemapJob classes.

This refactoring improves the clarity and consistency of the
codebase by adhering to established naming conventions,
making it easier for new developers to understand the
role of these classes within the application.
2025-01-28 01:16:17 +08:00
ce5d09b621 feat: add admin management for various entities
- Add menu labels and parents for AdminUser, City, Country, Region, WeatherArt, Ahoy::Event, and Ahoy::Visit.
- Introduce a new page for managing Sidekiq jobs, providing functionality to execute or delete scheduled jobs.
- Adjust batch job for generating weather art by using Sidekiq for improved performance.
- Implement clean-up worker for old Ahoy data and functionalities for refreshing the sitemap.

These changes enhance the administration interface by providing better organization and management tools for backend entities. The addition of Sidekiq jobs management further improves system maintenance capabilities.
2025-01-28 01:15:29 +08:00
c68fecf3fa
Update application_controller.rb 2025-01-27 09:50:39 +08:00
16ee512b0c
Update application_controller.rb 2025-01-27 09:42:08 +08:00
111fd85ebb
Update application_controller.rb 2025-01-27 09:30:01 +08:00
59a3f792c6
Update application_controller.rb 2025-01-27 09:20:14 +08:00
9e666310cf
Update application_controller.rb 2025-01-27 09:14:19 +08:00
cc74145033
Update application_controller.rb 2025-01-27 09:00:54 +08:00
3e713a9b26
Update application_controller.rb 2025-01-27 08:43:50 +08:00
263c85486c
Update application_controller.rb 2025-01-27 08:23:44 +08:00
a895216bda
Update application_controller.rb
Some checks are pending
Docker / docker (push) Waiting to run
2025-01-27 07:49:08 +08:00
75cdd69b9b
Update application_controller.rb 2025-01-27 07:40:13 +08:00
70f977cae1 更新 application.html.erb 2025-01-27 07:01:20 +08:00
444c3e67bc 更新 application.html.erb 2025-01-27 07:01:20 +08:00
ffbd201d62 更新 application_controller.rb 2025-01-27 07:01:20 +08:00
eb06398308 更新 application.html.erb
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
2025-01-27 07:00:18 +08:00
93543c6db3 更新 application.html.erb 2025-01-27 07:00:18 +08:00
5bfa94bc82
更新 application.html.erb 2025-01-27 06:54:24 +08:00
a16b14c7ca
更新 application_controller.rb 2025-01-27 06:53:01 +08:00
f59955ca6e
更新 application.html.erb 2025-01-27 01:38:21 +08:00
978cec359e feat: add docker CI workflow for development
- Create new workflow for building and pushing Docker
  images on push to 'dev' branch.
- Rename existing docker.yml to docker-main.yml and
  update image tag from 'latest' to 'main'.

This commit enhances the CI process by integrating Docker builds for
continuous delivery on the development branch, ensuring a clear
versioning strategy with updated tagging for production readiness.
2025-01-27 01:07:10 +08:00
6eca78da8d fix: update JSONB handling for ahoy events
- Change query conditions in City model to use the
  `properties::jsonb` syntax for compatibility.
- Update WeatherArt model's event counting to reflect
  the same JSONB handling for consistency across models.

These changes ensure that the queries correctly access the
JSONB fields in the database, which enhances robustness
when handling different database adapters like Postgres
or SQLite. This fixes potential issues with ahoy events
not being counted accurately due to incorrect property access.
2025-01-27 01:05:16 +08:00
f918a42619 fix: correct string quotation in city scope
- Change single quotes to double quotes for adapter name check

This change ensures consistent usage of double quotes in the
conditional check for the SQLite adapter. It does not affect
any functionality but improves code readability.
2025-01-27 00:50:35 +08:00
adb671e668 feat: add least popular active cities panel
- Introduce new panel in the admin dashboard for displaying
  the least popular active cities.
- Implement a database scope `least_popular_active` to
  retrieve cities based on their view counts.
- Ensure compatibility with both SQLite and PostgreSQL
  for fetching city view data.

This addition enhances the admin dashboard by allowing
administrators to easily identify and manage cities that
are receiving less user interaction, thus aiding in strategic
planning for engagement and promotion.
2025-01-27 00:48:07 +08:00
50321533f7 chore: clean up trailing whitespace and formatting
- Removed unnecessary leading and trailing blank lines in several
  ActiveAdmin register files.
- Reformatted string delimiters for consistency, changing single quotes
to double quotes in tracking events in the Cities and WeatherArts
controllers.
- Ensured proper spacing in array definitions across several
  models, including Ahoy::Event and Ahoy::Visit.

These changes improve code readability and maintain consistency
throughout the codebase by ensuring uniform use of quotes and
removing excess whitespace.
2025-01-27 00:43:36 +08:00
dd6cd0451d feat: add ahoy analytics for event tracking
- Integrate Ahoy gem for tracking user events and visits
- Create models for Ahoy events and visits
- Implement admin interfaces for managing events and visits
- Add background job for cleaning up old analytics data
- Update application controller and other relevant controllers to track specific actions

This commit implements a comprehensive event tracking system that logs user interactions
within the application. Additionally, it includes mechanisms for managing and
cleaning historical visit and event data, ensuring efficient data handling.
2025-01-27 00:43:18 +08:00
5f30e08a6e refactor: update city ordering
Order cities by name instead of filtering active cities. This change simplifies the CitiesController and improves code readability.

- Update City.includes method in CitiesController
- Remove active filter in City.includes method
2025-01-26 23:49:35 +08:00
bf10e41c1e fix: update view variable names for clarity
- Rename page view variables for consistency:
  - `busuanzi_value_page_pv` to `busuanzi_page_pv`
  - `busuanzi_value_page_uv` to `busuanzi_page_uv`
  - `busuanzi_value_site_pv` to `busuanzi_site_pv`
  - `busuanzi_value_site_uv` to `busuanzi_site_uv`

These changes improve the readability of the code by removing redundant parts of the variable names. This ensures that variables accurately reflect their purpose without unnecessary prefixes. The functionality remains unchanged.
2025-01-26 12:35:55 +08:00
9d1ff31c53 style: make busuanzi_container visible
- Change visibility of busuanzi_container from hidden to default

This change allows page view counts to be visible to users, improving
transparency regarding page engagement metrics.
2025-01-26 02:36:26 +08:00
155669866a style: adjust font size in footer container
Some checks failed
CI / scan_ruby (push) Waiting to run
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
Docker / docker (push) Has been cancelled
- Change font size from 'text-sm' to 'text-xs' for better visibility

This adjustment improves the aesthetics of the footer by ensuring that the
text is appropriately sized and matches the overall design of the page.
2025-01-26 02:29:43 +08:00
8cacf2a9ff style: update footer layout and visibility
- Adjust footer padding for improved spacing
- Change visitor and view statistics display to a more concise
  format
- Maintain consistent font sizes for better readability

These changes enhance the visual presentation of the footer
while maintaining the functionality of hidden visitor statistics.
2025-01-26 02:27:19 +08:00
b0bdb72f8e feat: enhance site statistics display
- Update footer padding for better spacing
- Add hidden container for site statistics
- Modify display of page and total site statistics

These changes improve the presentation and organization of site statistics, making it easier for users to understand site traffic and engagement.
2025-01-26 02:19:56 +08:00
cea07ccfea chore: remove active_admin.js and add analytics scripts
- Removed the active_admin.js file from the project as it is no longer needed.
- Added the Busuanzi site statistics script to the application layout for
  tracking page views and visitors.
- Updated the footer layout to include site statistics, providing users
  with real-time view and visitor counts.

These changes streamline the asset management by removing unused files and
integrate site analytics directly into the footer for better visibility.
2025-01-26 02:08:48 +08:00
5c8308a991 feat: enhance city weather art view
- Update layout with improved styles and spacing
- Modify sections for better visual hierarchy
- Add gradient effects and responsive design adjustments

These changes enhance the overall user experience by providing a
more visually appealing and organized layout for the city
weather art view. Adjustments include background effects,
updated navigation, and improved display of weather statistics
and history records.
2025-01-26 00:35:08 +08:00
5feaee4922 feat: add sitemap functionality
- Create SitemapsController to serve sitemaps
- Configure AWS S3 storage for sitemaps
- Update routes to include sitemap paths
- Add SitemapsHelper module
- Configure SitemapGenerator with AWS adapter
- Update storage configurations for AWS

This feature adds sitemap functionality to the application, enabling search engines to discover and index its content more efficiently. It includes configuration for AWS S3 storage to host the sitemaps and updates the application's routes to serve them.
2025-01-26 00:07:44 +08:00
ead795266e refactor: simplify sitemap scheduling process
- Removed conditional check for production environment in
  the initializer to ensure the sitemap refresh worker
  always runs at startup.
- Renamed Sidekiq configuration file from sidekiq.yml to
  sidekiq_scheduler.yml to better reflect its purpose.

These changes streamline the initialization process
for scheduled tasks and enhance the clarity of the
configuration files used in the project.
2025-01-25 11:24:44 +08:00
1ca468f9af fix: improve error handling for sitemap task
- Add error handling when scheduling the RefreshSitemapWorker in
  production.
- Log successful scheduling and errors for improved diagnostics.

This change ensures that any issues with scheduling the worker are
logged and can be addressed promptly, enhancing the reliability of
the application in production.
2025-01-25 11:05:53 +08:00
1f35664590 fix: correct conditional for sitemap refresh
- Update the condition to check for 'RAILS_BUILD' environment variable
- Allow RefreshSitemapWorker to run in development/test environments

This change clarifies the logic for executing the sitemap refresh worker by
inverting the condition to allow it to run when 'RAILS_BUILD' is not set,
ensuring proper functionality during development and testing phases.
2025-01-25 11:01:29 +08:00
40631fe95b fix: alter sitemap refresh condition
- Change the condition to prevent sitemap refresh in production when
  `RAILS_BUILD` is set.
- Ensures that the task only runs in development or non-production
  environments, avoiding potential conflicts or wasted resources.

This change helps to streamline the initialization process and enhances
performance by not triggering unnecessary background tasks.
2025-01-25 10:56:13 +08:00
f7c5ae4ee7 chore: conditionally refresh sitemap
- Modified the schedule_tasks.rb initializer to only run RefreshSitemapWorker.perform_async unless RAILS_BUILD environment variable is set
This change optimizes the build process by preventing unnecessary sitemap refreshes during build time, thus improving overall project performance.
2025-01-25 10:54:19 +08:00
742c94ced1 style: update layout of city show view
- Change stats div to be responsive
- Adjust coordinates display format for better readability

This update enhances the layout of the city show view by making
it more responsive and improving the display of weather and
geographical data. Additionally, the startup task for
refreshing the sitemap is now consistently executed,
removing unnecessary complexity.
2025-01-25 10:51:14 +08:00
c37a93bcdf feat: remove trailing newline at end of file
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
Remove trailing newline at end of file in `schedule_tasks.rb`.

- Modify file to remove unnecessary newline at end of file.
2025-01-25 01:55:00 +08:00
7ebf9cefae feat: switch startup task to Sidekiq with Redis cache
- Update schedule tasks to use Sidekiq and Redis
- Improve task scheduling with caching
- Remove unnecessary code and error handling

This change enhances the reliability and performance of the startup task, leveraging the power of Sidekiq and Redis for background job processing and caching. The improvements ensure a smoother and more efficient application startup experience.
2025-01-25 01:49:28 +08:00
dd37e2835b style: add newline at end of schedule_tasks.rb
This commit adds a newline at the end of the 'schedule_tasks.rb' file to comply with the standard coding practices. Proper file formatting helps in maintaining code consistency across the project.
2025-01-25 01:38:35 +08:00
4e1fb58abf feat: prevent startup task from running in development environment
- Updated schedule_tasks.rb to include RAILS_BUILD check
- Improved application startup behavior in development mode

This change ensures the startup task does not run unnecessarily in development environment, reducing application startup time and resources usage.
2025-01-25 01:38:12 +08:00
5bc06007b2 refactor: improve countries find_resource method
- Update `find_resource` method to use `scoped_collection.friendly.find(params[:id])`

This refactoring improves the find_resource method to work with friendly URLs, making it more robust and user-friendly.
2025-01-25 01:29:50 +08:00
84c224cf8d feat: add startup scheduling for production
- Schedule RefreshSitemapWorker after initialization
- Implement error handling for scheduling task
- Use Rails cache to prevent multiple tasks running simultaneously

This commit introduces a mechanism that schedules the
RefreshSitemapWorker to run once after the application starts
in production. It ensures that the task does not run
multiple times concurrently by using a cache key.
Error handling is included to log any failures
in scheduling the task, improving overall reliability.
2025-01-25 01:23:04 +08:00
032ff0552a feat: add jQuery and UI support
- Introduce jQuery and jQuery UI libraries
- Create an add_jquery.js for global availability
- Import active_admin.js to initialize the setup

These changes integrate jQuery and jQuery UI into the project, which
will facilitate more interactive user interface features. The jQuery
object is made globally accessible to use within other scripts.
2025-01-25 01:11:15 +08:00
3203face6b feat: add country filter in city admin
- Introduce a new filter for selecting cities by country in the
  ActiveAdmin interface.
- Update the ransackable attributes to include country_id for
  searchable functionality.

This change enhances the Admin UI by allowing easier access to
city data based on country, improving the user experience for
administrators.
2025-01-25 00:54:23 +08:00
8364d42759 feat: add country input to city form
- Added country input field to the city form for better user experience.
- Removed unused region and weather-related fields to declutter the form.

This change improves the data captured for cities and enhances the form's usability by focusing on relevant information.
2025-01-25 00:50:43 +08:00
9ce473dddb fix: increase sleep duration in weather arts worker
- Change SLEEP_DURATION from 60 seconds to 120 seconds

This change is made to improve resource management
and allow for a more efficient operation of the batch
processing tasks performed by the worker. It helps in
avoiding potential overload on system resources.
2025-01-25 00:41:29 +08:00
fe55437c96 refactor: update generation and sleep intervals
- Change generation interval from 6 hours to 24 hours
- Increase sleep duration from 3 seconds to 60 seconds

These adjustments aim to optimize the worker's performance and reduce resource consumption. This change reflects a shift towards longer generation cycles, potentially improving throughput.
2025-01-25 00:40:51 +08:00
ec3669249f style: update site title in application layout
- Change the displayed title from 'Today AI Weather Art' to 'Today AI Weather'.

This change refines the branding by removing the word 'Art', making the title more concise and focused. This is a minor modification that does not impact functionality.
2025-01-25 00:34:20 +08:00
f6270b1ad4 feat: add weather icons and update city view
- Introduce `weather_description_icon` and `weather_stat_icon`
  helper methods for displaying SVG icons based on weather
  conditions and statistics.
- Enhance the city show view by using these icons to display
  visual weather information such as temperature, wind, humidity,
  visibility, pressure, and cloud cover.
- Optimize the visual styling and layout of the weather stats
  and cards for better user experience.
2025-01-24 10:01:43 +08:00
9dd7044a77 style: update title in application layout
- Change title from 'AI Weather Art' to 'Today AI Weather Art'

This change enhances the clarity of the application by specifying that the art relates to today's weather. It does not affect any functionality but improves the user interface.
2025-01-24 09:15:51 +08:00
dd6bb9972c feat: implement ads.txt configuration
- Add ads.txt configuration file
- Update public/ads.txt with Google AdMob publisher ID

This feature allows us to set up Google AdMob in our app and earn revenue from ads.
It includes proper configuration of the ads.txt file for future ad networks.
2025-01-24 09:14:13 +08:00
23fc14af59 style: format priority values for consistency
- Clean up the seed file for cities in China
- Ensure all priority values have a consistent formatting style

This change enhances the readability of the data seeding code without altering the underlying logic or functionality of the application.
2025-01-24 09:09:57 +08:00
fedb954d34 chore: add Google AdSense script to layout
- Include async script for Google AdSense in the application layout
- Ensure script loads with crossorigin attribute

This change will allow the application to serve ads from Google, potentially generating revenue. The implementation is non-intrusive and maintains existing functionality.
2025-01-24 09:04:41 +08:00
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
799dfc18ed fix: update weather fetch timing to days
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
- Changed 'last_weather_fetch' and 'last_image_generation'
  from 10.hours.ago to 10.days.ago for various cities
- Affects seed data for cities in multiple countries, including:
  - Australia
  - Bangladesh
  - Brazil
  - Canada
  - China
  - Egypt
  - France
  - Germany
  - India
  - Japan
  - Mexico
  - Nigeria
  - Pakistan
  - Russia
  - Saudi Arabia
  - Singapore
  - South Korea
  - Thailand
  - Turkey
  - UK
  - USA
  - Vietnam

This change ensures that the timestamps reflect a more refined time range for when last weather data was fetched, possibly to enhance performance or consistency in application behavior.
2025-01-23 01:50:04 +08:00
a2c75ba3c2 feat: cache Docker layers and update dependencies
- Removed unnecessary queue configurations from Sidekiq configuration
- Added 'whenever' gem and its necessary dependencies for adding cron jobs
- Updated Docker workflow to cache Docker layers using actions/cache

This update improves the efficiency of CI/CD pipelines by caching Docker layers, which will help in reducing the time taken for the build process. Additionally, the changes in the Gemfile and the config/sidekiq.yml configuration will enable the project to run cron jobs for tasks like generating weather arts.
2025-01-23 01:37:47 +08:00
e9095ece6e feat: improve Sidekiq scheduling configuration
- Enable dynamic scheduling only if the schedule file exists
- Update the sidekiq.yml to include a new job for batch generation
- Define a cron schedule for the new job to run every 2 hours

This update allows Sidekiq to conditionally enable its scheduler and
introduces a new job that processes weather art batches every two
hours. The change enhances the job management and scheduling
dynamics in the application.
2025-01-23 01:07:39 +08:00
c20ff296eb fix: update Active Job queue adapter to Sidekiq
- Comment out previous queue adapter setting for Solid Queue
- Set Active Job queue adapter to Sidekiq for better performance

This change updates the Active Job queue adapter from Solid Queue to
Sidekiq. This adjustment is intended to improve job processing
performance and reliability in production environments. The previous
adapter setting will be retained as a comment for reference.
2025-01-23 00:51:56 +08:00
f74f34ce82 chore: update admin user creation for seeds
- Remove environment condition for admin user creation

This change simplifies the admin user creation in the seeds file by
removing the conditional check for the development environment.
As a result, the admin user will be created regardless of the
environment, which may need to be addressed to avoid unintended
consequences in production setups.
2025-01-23 00:40:42 +08:00
a6cea6b80d chore: setup admin user
- Create admin user with email 'admin@example.com' and password 'password' for development environment
This change allows for easier development and testing by providing a default admin user.
2025-01-23 00:40:19 +08:00
08c584b85b chore: rename job classes and update Sidekiq config
- Renamed `BatchGenerateWeatherArtsJob` to `BatchGenerateWeatherArtsWorker`
- Renamed `GenerateWeatherArtJob` to `GenerateWeatherArtWorker`
- Updated Sidekiq configuration to set Redis URL and logger level
- Modified `sidekiq.yml` to use the new worker class names and added queue configurations

These changes help in aligning the class names with their purpose as
workers in Sidekiq, while also ensuring better configuration for
Redis and logging.
2025-01-22 18:47:41 +08:00
78722caeb9 chore: update gem dependencies and formatting
- Standardized quotation marks in Gemfile and Ruby files
- Improved consistency for Sidekiq and Sidekiq Scheduler
- Removed unnecessary blank line in GenerateWeatherArtWorker class

These changes ensure a consistent coding style across the project, making
it easier to read and maintain. Adjustments to the Gemfile update the
formatting without altering the functionality.
2025-01-22 17:58:46 +08:00
2bcfea30ee feat: add background job processing with Sidekiq
- Implement BatchGenerateWeatherArtsWorker to handle batch
  processing of weather art generation.
- Create GenerateWeatherArtWorker for individual weather art
  generation tasks.
- Update Dockerfile to include redis-tools for Sidekiq support.
- Modify Gemfile to add sidekiq and sidekiq-scheduler gems.
- Configure Sidekiq in initializers and set up routes for
  Sidekiq dashboard.
- Include a sidekiq.yml configuration for scheduling jobs.
- Create compose.yaml for Docker services including web,
  database, Redis, and Sidekiq workers.

These changes introduce background processing capabilities
using Sidekiq, allowing for efficient generation of weather
art through scheduled and managed job queues, optimizing
performance and scalability.
2025-01-22 17:58:25 +08:00
607fc9e8b8 chore: update asset precompilation command
- Change the precompilation command to include RAILS_BUILD=1
- This modification allows for environment-specific builds without altering the existing secret handling

This update enhances the flexibility of the asset precompilation process during deployment while maintaining the required application security measures.
2025-01-22 17:29:43 +08:00
494ae40088 chore: update storage configuration for production
- Change active storage service to use `:build` or `:amazon`
- Added a new `build` service for local disk storage

This update allows for better flexibility in managing file
storage based on the environment. The configuration now checks
the `RAILS_BUILD` environment variable to decide on the
storage service, making it easier to handle local testing and
production deployments without manual adjustments.
2025-01-22 17:26:06 +08:00
3748ea5215 chore: update AWS region defaults in storage config
- Change default region for amazon_dev to 'wnam'
- Change default region for amazon to 'wnam'

This commit updates the storage configuration for AWS S3 by
setting the default region to 'wnam' for both development
and production environments. The change ensures that a valid
region is used even when the environment variable is not set.
This is a preparatory step for deployments needing a specific
region if the configuration is incomplete.
2025-01-22 17:17:05 +08:00
fd910fb469 fix: update job scheduling interval
- Change job execution frequency from once every hour to every two hours.
- Ensure the `BatchGenerateWeatherArtsJob` runs as intended without unnecessary frequency.

This change addresses performance concerns by reducing the load on the system caused by frequent job executions.
2025-01-22 17:08:46 +08:00
b5c40f2e13 fix: update Dockerfile and weather service configuration
- Added 'libpq-dev' to the packages installed in the Dockerfile
- Updated the base_uri in WeatherService to use 'dig' for safer access

These changes improve the Docker environment by ensuring that
necessary PostgreSQL development headers are available during
installation. The weather service now safely accesses the
URI from the credentials, reducing the risk of errors when
fetching nested configuration data.
2025-01-22 17:07:04 +08:00
2d81dd91e7 feat: activate cities in seed files
- Update active status of various cities from false to true
- Ensures that all seeded cities are now enabled and available for usage

This change activates previously inactive cities in the seed files, which
facilitates their accessibility in subsequent deployments and testing.
The modification was made across numerous city seed files to ensure a
consistent state where all cities are now marked as active, improving
the readiness of the application.
2025-01-22 17:00:27 +08:00
2ab495897d chore: update Dockerfile to install libpq5
- Add libpq5 to the list of installed packages for
  database connections.
- Remove libpq-dev from the list as it is no longer
  necessary for the build process.

These adjustments enhance the Docker image configuration,
ensuring the application can connect to PostgreSQL databases
without unnecessary build dependencies.
2025-01-22 16:57:54 +08:00
853a1d03ce refactor: clean up ai_service code formatting
- Adjust formatting for message parameters in the chat call
- Eliminate unnecessary blank lines in generate_prompt_request method
- Ensure consistent styles for arrays and block indentation

These changes improve code readability and maintain consistency in
formatting for better maintainability without altering functionality.
2025-01-22 16:50:21 +08:00
d728d7f50e feat: add batch weather art generation jobs
- Created BatchGenerateWeatherArtsJob to process eligible
  cities and generate weather art.
- Introduced GenerateWeatherArtJob for generating weather
  art and image attachment.
- Added AiService for obtaining prompts and generating
  images with OpenAI API.
- Implemented WeatherService to fetch current weather
  data from the QWeather API.
- Updated Gemfile with necessary gems (whenever,
  ruby-openai, httparty, down, aws-sdk-s3).

This commit introduces a system to create and store
weather art images for various cities based on current
weather conditions, leveraging external APIs for data
and image generation.
2025-01-22 16:50:00 +08:00
6e387d1a8c feat: enhance city weather card layout
- Improve image and main info section for better aesthetics
- Add gradient overlay for text readability
- Include temperature display with feel-like temperature
- Enhance city name and location styling
- Add detailed weather information metrics (Humidity, Wind, Visibility)
- Provide a backup UI for when no image is available

This update enhances the user interface of the city weather card
by improving the layout and adding more detailed weather information.
The gradient overlays and reorganized elements improve readability and
usability. Additionally, a fallback display is included for cases where
weather images are not attached, ensuring consistent design.
2025-01-22 14:51:00 +08:00
f7d295b41b feat: enhance cities filtering interface
- Introduce dropdown menus for region and country selection
- Adjust padding on title section for better layout
- Update filter navigation structure to improve UX

This commit refines the user interface for filtering cities by
allowing users to select regions and countries from dropdown
menus. The filtering options are now easier to navigate and
maintain a consistent aesthetic with adjusted padding for
better visual hierarchies.
2025-01-22 14:12:29 +08:00
da7fca139c feat: add city card component and improve index view
- Create a new partial for city card display with weather image
- Update index page to show featured weather art with gradient
- Refactor city listing layout and navigation for improved clarity

This commit introduces a new component for displaying city cards that
includes weather artwork when available. The index page has been
refactored for better visual presentation and usability. Users can
now better navigate through cities and see relevant weather data.
2025-01-22 14:10:43 +08:00
9cb1467301 feat: enhance weather arts and cities features
- Add slug column to weather_arts for friendly URLs.
- Update weather arts retrieval in the controller to use slug.
- Implement region and country filtering in cities index view.
- Optimize city queries with scopes for active status and region/country.
- Improve UI layout and design for the cities index page.

These changes allow better user experience by enabling cleaner URLs for weather arts and facilitating efficient filtering of cities based on selected regions and countries.
2025-01-22 14:04:58 +08:00
be7856935f
Merge pull request #1 from songtianlun/dependabot/bundler/selenium-webdriver-4.28.0
chore(deps-dev): bump selenium-webdriver from 4.27.0 to 4.28.0
2025-01-22 09:45:08 +08:00
e70763dbe0 chore: update workflow name for Docker build
- Change workflow name from 'Build and Publish Docker Image' to 'Docker'

This change simplifies the workflow name for better readability and
identification in the CI/CD pipeline.
2025-01-22 09:39:04 +08:00
f83600412b chore: update docker workflow to include tags
- Add support for tagging workflow triggers on pushes
- Include version tags following the pattern 'v*'

This change enhances the Docker workflow by allowing the CI/CD
pipeline to respond to version tags, which is beneficial for
versioned releases and better management of deployment processes.
2025-01-22 09:37:35 +08:00
93b24ff50c chore: update Dockerfile to include libpq-dev
- Add libpq-dev to the list of packages installed for the build
- Ensure that the environment has the necessary dependencies

This change updates the Dockerfile to include the libpq-dev package,
which is required for compiling certain gem dependencies. The inclusion
of this package improves the compatibility of the environment with
database-related gems during the build process, ensuring a smoother
setup for development and deployment.
2025-01-22 09:29:37 +08:00
c9aef3ddfe build: update Docker workflow for versioning
- Add checkout step to retrieve full git history
- Introduce a step to get the current version of the project
- Modify login step to use the actor's username instead of a constant
- Update the tags to use dynamic versioning in Docker image push

These changes improve the Docker workflow by ensuring that the
correct version tag is used when building and pushing Docker
images. The modified steps enhance traceability and allow
the workflow to handle versioning accurately based on current
git tags, addressing the need for specific version control in
the images produced.
2025-01-22 09:23:59 +08:00
48f3da8913 chore: update Docker workflow for tagging
- Remove unnecessary steps for version extraction
- Simplify tag assignment using environment variables

This update cleans up the Docker workflow by eliminating steps
that were not crucial for the tagging process. The tagging
is now done directly with the repository name, reducing redundancy
and improving clarity in the workflow configuration.
2025-01-22 09:19:28 +08:00
a988c49ae0 chore: update docker tags formatting
- Change Docker tags configuration to be multiline
- Improve clarity and maintainability of the workflow

This change enhances the readability of the tags in the
Docker workflow, facilitating easier updates and understanding
of the tagging process.
2025-01-22 09:07:52 +08:00
1ab860c739 chore: fix output redirection in docker workflow
Some checks are pending
CI / scan_ruby (push) Waiting to run
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
Build and Publish Docker Image / docker (push) Waiting to run
- Corrected syntax errors in output redirection
- Ensured that LATEST_TAG and VERSION_TAG are set correctly in the GitHub Actions output

These changes fix an issue where the command was improperly formatted,
which could lead to unexpected behavior during the workflow execution.
2025-01-22 01:05:13 +08:00
24cbf13a26 feat: enhance Docker workflow version tagging
- Store latest and version tags in variables
- Clean up output by using variables for tags

This update improves the clarity of the tagging process in the
Docker workflow configuration. The tags are now stored in
variables for better manageability and consistency in
environment output.
2025-01-22 01:02:22 +08:00
5bb91f16fd fix: correct LATEST_TAG output variable
- Remove extraneous dollar sign from LATEST_TAG
- Ensure correct evaluation during workflow execution

This fix resolves an issue that could cause the incorrect
setting of the LATEST_TAG environment variable in the
GitHub Actions workflow, ensuring proper deployment tagging.
2025-01-22 00:58:37 +08:00
04595824e6 feat: configure docker image prefix
Update Docker workflow to include image prefix in version and latest tag

Change the Docker image prefix in the GitHub workflow to include the registry, username and image name.
Update the environment variables to use the new format for the image prefix.
2025-01-22 00:56:58 +08:00
2b4275055c feat: update fetch-depth in docker workflow
- Update fetch-depth in Docker workflow configuration
- No additional dependencies added

This change updates the fetch-depth setting in the GitHub Actions Docker workflow. This ensures that we get the full Git history for the code checkout. There are no additional dependencies added to this change.
2025-01-22 00:51:55 +08:00
0f9b4efc80 chore: add checkout step in docker workflow
- Include checkout code step using actions/checkout@v3
- Set fetch-depth to 0 to retrieve the full git history

This change ensures that the workflow has access to the complete
commit history, which may be necessary for certain operations
within the Docker build process.
2025-01-22 00:51:08 +08:00
ec2c92e6c9 chore: update Docker workflow to use secrets
- Change hardcoded Docker Hub username to use secrets
- Update workflow to securely reference DockerHub credentials

This update enhances the security of the GitHub Actions workflow
by using secrets instead of exposing sensitive information
in the codebase. This ensures that the Docker Hub username is
kept private and reduces the risk of accidental exposure.
2025-01-22 00:47:33 +08:00
54133b2f87 feat: add GitHub Actions workflow for building and publishing Docker image
- Add Docker login step
- Set up QEMU
- Set up Docker Buildx
- Build and push Docker image

This feature allows for automated building and publishing of Docker images to Docker Hub. It includes proper setup of QEMU and Docker Buildx for cross-platform compatibility and efficient image building.
2025-01-22 00:45:11 +08:00
c11d10c86a feat: update city display and database schema
- Change city name display to localized name in both index and show views
- Modify schema to use bigint for certain foreign keys
- Correct country codes in seeds for accurate data representation

These changes improve the handling of city names, ensuring they are displayed in the appropriate localized format and ensuring well-typed foreign key relationships in the schema.
2025-01-21 21:29:26 +08:00
ebaf7a3f34 feat: add countries and regions management
- Implement Country and Region models
- Establish relationships between City, Country, and Region
- Update ActiveAdmin setup for managing countries and regions
- Add localization support for cities and countries in multiple languages
- Create necessary migrations to support the new schema

This update allows for better categorization of cities under their respective countries and regions, enhancing geographical structure and support for multilingual features.
2025-01-21 18:27:26 +08:00
dependabot[bot]
e2e5e38a7d
chore(deps-dev): bump selenium-webdriver from 4.27.0 to 4.28.0
Bumps [selenium-webdriver](https://github.com/SeleniumHQ/selenium) from 4.27.0 to 4.28.0.
- [Release notes](https://github.com/SeleniumHQ/selenium/releases)
- [Changelog](https://github.com/SeleniumHQ/selenium/blob/trunk/rb/CHANGES)
- [Commits](https://github.com/SeleniumHQ/selenium/compare/selenium-4.27.0...selenium-4.28.0)

---
updated-dependencies:
- dependency-name: selenium-webdriver
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-21 06:36:52 +00:00
ac199af963 style: standardize quoting for pg gem
- Changed single quotes to double quotes for the 'pg' gem in the production group of the Gemfile.

This change enhances consistency in the Gemfile by standardizing the quoting style.
2025-01-21 09:11:00 +08:00
5fd9573cf2 chore: add pg gem for production environment
Some checks are pending
CI / scan_ruby (push) Waiting to run
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
- Introduced the 'pg' gem for PostgreSQL support in the production
  environment.
- Updated Gemfile to include it in the production group.
- Corresponding update in Gemfile.lock to ensure version 1.5.9 is
  used.

This change prepares the application to use PostgreSQL as the
primary database in production, which is essential for running
production-grade applications effectively.
2025-01-20 18:47:06 +08:00
34c05232ee chore: clean up code style and unused fixtures
- Standardize string quotes in various files for consistency.
- Remove commented-out tests from cities and weather_arts controllers to improve readability.
- Clean up admin_users and weather_arts fixture files by commenting out unused entries instead of removing them.

These changes are aimed at enhancing code readability and maintainability, ensuring that code adheres to style guidelines without removing useful comments for future reference.
2025-01-20 18:08:55 +08:00
d570f43f95 feat: add font styling and enhance city views
- Import Playfair Display and Raleway fonts for better typography
- Add a method to `City` model for fetching the latest weather art
- Revamp city index and show pages for improved layout and usability
- Update styles in the layout and main pages to enhance user experience

These changes promote visual consistency and enhance user interaction within the platform, providing a more engaging experience.
2025-01-20 18:02:28 +08:00
a533390356 feat: add weather art records for Guangzhou
- Update weather date for existing record to 5 days ago
- Add new weather art record for 2 days ago with relevant details
- Include image attachment for the new weather art

This commit adds a second weather art record for Guangzhou reflecting the
weather conditions from 2 days ago, along with relevant data including
humidity, wind speed, and an updated visual representation.
2025-01-20 17:37:24 +08:00
38884bdaa7 chore: cleanup IDE configuration files
- Remove .idea directory and its contents
- Add .idea to .gitignore to prevent tracking

This commit removes the IDE configuration files which are not
necessary for the repository. This keeps the repository clean and
prevents potential conflicts for different developers using different
IDEs. The .idea directory is now also ignored in the .gitignore
file to prevent future commits of these files.
2025-01-20 13:27:29 +08:00
149 changed files with 4567 additions and 2536 deletions

48
.github/workflows/docker-dev.yml vendored Normal file
View File

@ -0,0 +1,48 @@
name: Docker
on:
push:
branches:
- dev
env:
# Use docker.io for Docker Hub if empty
REGISTRY: docker.io
IMAGE_NAME: ${{ github.event.repository.name }}
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
with:
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 }}
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
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: |
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:dev

51
.github/workflows/docker-main.yml vendored Normal file
View File

@ -0,0 +1,51 @@
name: Docker
on:
push:
branches:
- main
tags:
- v*
env:
# Use docker.io for Docker Hub if empty
REGISTRY: docker.io
IMAGE_NAME: ${{ github.event.repository.name }}
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
with:
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 }}
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
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: |
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:main
${{ env.REGISTRY }}/${{github.actor}}/${{ github.event.repository.name }}:${{ steps.get_version.outputs.VERSION }}

3
.gitignore vendored
View File

@ -37,3 +37,6 @@
!/app/assets/builds/.keep
/node_modules
.idea
public/sitemap.xml.gz

8
.idea/.gitignore generated vendored
View File

@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

4
.idea/misc.xml generated
View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="mise: 3.3.5" project-jdk-type="RUBY_SDK" />
</project>

8
.idea/modules.xml generated
View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/today_ai_weather.iml" filepath="$PROJECT_DIR$/.idea/today_ai_weather.iml" />
</modules>
</component>
</project>

View File

@ -1,367 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="RUBY_MODULE" version="4">
<component name="FacetManager">
<facet type="RailsFacetType" name="Ruby on Rails">
<configuration>
<RAILS_FACET_CONFIG_ID NAME="RAILS_FACET_SUPPORT_REMOVED" VALUE="false" />
<RAILS_FACET_CONFIG_ID NAME="RAILS_TESTS_SOURCES_PATCHED" VALUE="true" />
<RAILS_FACET_CONFIG_ID NAME="RAILS_FACET_APPLICATION_ROOT" VALUE="$MODULE_DIR$" />
</configuration>
</facet>
</component>
<component name="ModuleRunConfigurationManager">
<shared />
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/features" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/.bundle" />
<excludeFolder url="file://$MODULE_DIR$/components" />
<excludeFolder url="file://$MODULE_DIR$/log" />
<excludeFolder url="file://$MODULE_DIR$/public/packs" />
<excludeFolder url="file://$MODULE_DIR$/public/system" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
<excludeFolder url="file://$MODULE_DIR$/vendor/bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/cache" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" scope="PROVIDED" name="actioncable (v8.0.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="actionmailbox (v8.0.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="actionmailer (v8.0.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="actionpack (v8.0.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="actiontext (v8.0.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="actionview (v8.0.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="activeadmin (v3.2.5, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="activejob (v8.0.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="activemodel (v8.0.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="activerecord (v8.0.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="activestorage (v8.0.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="activesupport (v8.0.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="addressable (v2.8.7, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="arbre (v1.7.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="ast (v2.4.2, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="base64 (v0.2.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="bcrypt (v3.1.20, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="bcrypt_pbkdf (v1.1.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="benchmark (v0.4.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="bigdecimal (v3.1.9, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="bindex (v0.8.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="bootsnap (v1.18.4, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="brakeman (v7.0.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="builder (v3.3.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="bundler (v2.6.2, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="capybara (v3.40.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.3.5, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="connection_pool (v2.5.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="crass (v1.0.6, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="cssbundling-rails (v1.4.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="csv (v3.3.2, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="date (v3.4.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="debug (v1.10.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="devise (v4.9.4, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="dotenv (v3.1.7, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="drb (v2.2.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="ed25519 (v1.3.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="erubi (v1.13.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="et-orbi (v1.2.11, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="formtastic (v5.0.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="formtastic_i18n (v0.7.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="friendly_id (v5.5.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="fugit (v1.11.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="globalid (v1.2.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="has_scope (v0.8.2, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="i18n (v1.14.6, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="inherited_resources (v1.14.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="io-console (v0.8.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="irb (v1.14.3, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="jbuilder (v2.13.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="jquery-rails (v4.6.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="jsbundling-rails (v1.3.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="json (v2.9.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="kamal (v2.4.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="kaminari (v1.2.2, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="kaminari-actionview (v1.2.2, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="kaminari-activerecord (v1.2.2, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="kaminari-core (v1.2.2, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="language_server-protocol (v3.17.0.3, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="logger (v1.6.5, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="loofah (v2.24.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="mail (v2.8.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="marcel (v1.0.4, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="matrix (v0.4.2, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="mini_mime (v1.1.5, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="minitest (v5.25.4, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="msgpack (v1.7.5, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="net-imap (v0.5.5, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="net-pop (v0.1.2, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="net-protocol (v0.2.2, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="net-scp (v4.0.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="net-sftp (v4.0.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="net-smtp (v0.5.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="net-ssh (v7.3.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="nio4r (v2.7.4, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="nokogiri (v1.18.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="orm_adapter (v0.5.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="ostruct (v0.6.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="parallel (v1.26.3, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="parser (v3.3.7.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="propshaft (v1.1.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="psych (v5.2.3, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="public_suffix (v6.0.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="puma (v6.5.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="raabro (v1.4.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="racc (v1.8.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rack (v3.1.8, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rack-session (v2.1.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rack-test (v2.2.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rackup (v2.2.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rails (v8.0.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rails-dom-testing (v2.2.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rails-html-sanitizer (v1.6.2, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="railties (v8.0.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rainbow (v3.1.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rake (v13.2.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="ransack (v4.2.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rdoc (v6.11.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="regexp_parser (v2.10.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="reline (v0.6.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="responders (v3.1.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rexml (v3.4.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rubocop (v1.70.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rubocop-ast (v1.37.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rubocop-minitest (v0.36.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rubocop-performance (v1.23.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rubocop-rails (v2.29.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rubocop-rails-omakase (v1.0.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.13.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="ruby2_keywords (v0.0.5, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="rubyzip (v2.4.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="securerandom (v0.4.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="selenium-webdriver (v4.27.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="solid_cable (v3.0.5, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="solid_cache (v1.0.6, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="solid_queue (v1.1.2, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="sqlite3 (v2.5.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="sshkit (v1.23.2, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="stimulus-rails (v1.3.4, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="stringio (v3.1.2, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="thor (v1.3.2, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="thruster (v0.1.10, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="timeout (v0.4.3, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="turbo-rails (v2.0.11, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="tzinfo (v2.0.6, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="unicode-display_width (v3.1.4, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="unicode-emoji (v4.0.4, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="uri (v1.0.2, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="useragent (v0.16.11, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="warden (v1.2.9, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="web-console (v4.2.1, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="websocket (v1.2.11, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="websocket-driver (v0.7.7, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="websocket-extensions (v0.1.5, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="xpath (v3.2.0, mise: 3.3.5) [gem]" level="application" />
<orderEntry type="library" scope="PROVIDED" name="zeitwerk (v2.7.1, mise: 3.3.5) [gem]" level="application" />
</component>
<component name="RModuleSettingsStorage">
<LOAD_PATH number="0" />
<I18N_FOLDERS number="1" string0="$MODULE_DIR$/config/locales" />
</component>
<component name="RailsGeneratorsCache">
<option name="generators">
<list>
<option value="active_record:application_record" />
<option value="active_record:multi_db" />
<option value="application_record" />
<option value="authentication" />
<option value="benchmark" />
<option value="channel" />
<option value="controller" />
<option value="erb:authentication" />
<option value="erb:controller" />
<option value="erb:mailer" />
<option value="erb:scaffold" />
<option value="generator" />
<option value="helper" />
<option value="integration_test" />
<option value="jbuilder" />
<option value="job" />
<option value="kaminari:config" />
<option value="kaminari:views" />
<option value="mailbox" />
<option value="mailer" />
<option value="migration" />
<option value="model" />
<option value="resource" />
<option value="scaffold" />
<option value="scaffold_controller" />
<option value="script" />
<option value="solid_cable:install" />
<option value="solid_cable:update" />
<option value="solid_cache:install" />
<option value="solid_queue:install" />
<option value="stimulus" />
<option value="system_test" />
<option value="tailwindcss:authentication" />
<option value="task" />
<option value="test_unit:authentication" />
<option value="test_unit:channel" />
<option value="test_unit:generator" />
<option value="test_unit:install" />
<option value="test_unit:mailbox" />
<option value="test_unit:plugin" />
</list>
</option>
<option name="myGenerators">
<list>
<option value="active_record:application_record" />
<option value="active_record:multi_db" />
<option value="application_record" />
<option value="authentication" />
<option value="benchmark" />
<option value="channel" />
<option value="controller" />
<option value="erb:authentication" />
<option value="erb:controller" />
<option value="erb:mailer" />
<option value="erb:scaffold" />
<option value="generator" />
<option value="helper" />
<option value="integration_test" />
<option value="jbuilder" />
<option value="job" />
<option value="kaminari:config" />
<option value="kaminari:views" />
<option value="mailbox" />
<option value="mailer" />
<option value="migration" />
<option value="model" />
<option value="resource" />
<option value="scaffold" />
<option value="scaffold_controller" />
<option value="script" />
<option value="solid_cable:install" />
<option value="solid_cable:update" />
<option value="solid_cache:install" />
<option value="solid_queue:install" />
<option value="stimulus" />
<option value="system_test" />
<option value="tailwindcss:authentication" />
<option value="task" />
<option value="test_unit:authentication" />
<option value="test_unit:channel" />
<option value="test_unit:generator" />
<option value="test_unit:install" />
<option value="test_unit:mailbox" />
<option value="test_unit:plugin" />
</list>
</option>
</component>
<component name="RailsPaths" isManagedAutomatically="true">
<entry key="app">
<value>file://$MODULE_DIR$/app</value>
</entry>
<entry key="app/assets">
<value>file://$MODULE_DIR$/app/assets</value>
</entry>
<entry key="app/channels">
<value>file://$MODULE_DIR$/app/channels</value>
</entry>
<entry key="app/controllers">
<value>file://$MODULE_DIR$/app/controllers</value>
</entry>
<entry key="app/helpers">
<value>file://$MODULE_DIR$/app/helpers</value>
</entry>
<entry key="app/mailers">
<value>file://$MODULE_DIR$/app/mailers</value>
</entry>
<entry key="app/models">
<value>file://$MODULE_DIR$/app/models</value>
</entry>
<entry key="app/views">
<value>file://$MODULE_DIR$/app/views</value>
</entry>
<entry key="config">
<value>file://$MODULE_DIR$/config</value>
</entry>
<entry key="config/cable">
<value>file://$MODULE_DIR$/config/cable.yml</value>
</entry>
<entry key="config/cache">
<value>file://$MODULE_DIR$/config/cache.yml</value>
</entry>
<entry key="config/database">
<value>file://$MODULE_DIR$/config/database.yml</value>
</entry>
<entry key="config/environment">
<value>file://$MODULE_DIR$/config/environment.rb</value>
</entry>
<entry key="config/environments">
<value>file://$MODULE_DIR$/config/environments</value>
</entry>
<entry key="config/initializers">
<value>file://$MODULE_DIR$/config/initializers</value>
</entry>
<entry key="config/locales">
<value>file://$MODULE_DIR$/config/locales</value>
</entry>
<entry key="config/routes">
<value>file://$MODULE_DIR$/config/routes</value>
</entry>
<entry key="config/routes.rb">
<value>file://$MODULE_DIR$/config/routes.rb</value>
</entry>
<entry key="config/solid_cache">
<value>file://$MODULE_DIR$/config/solid_cache.yml</value>
</entry>
<entry key="db">
<value>file://$MODULE_DIR$/db</value>
</entry>
<entry key="db/migrate">
<value>file://$MODULE_DIR$/db/migrate</value>
</entry>
<entry key="db/seeds.rb">
<value>file://$MODULE_DIR$/db/seeds.rb</value>
</entry>
<entry key="lib">
<value>file://$MODULE_DIR$/lib</value>
</entry>
<entry key="lib/assets">
<value>file://$MODULE_DIR$/lib/assets</value>
</entry>
<entry key="lib/tasks">
<value>file://$MODULE_DIR$/lib/tasks</value>
</entry>
<entry key="lib/templates">
<value>file://$MODULE_DIR$/lib/templates</value>
</entry>
<entry key="log">
<value>file://$MODULE_DIR$/log/development.log</value>
</entry>
<entry key="public">
<value>file://$MODULE_DIR$/public</value>
</entry>
<entry key="public/javascripts">
<value>file://$MODULE_DIR$/public/javascripts</value>
</entry>
<entry key="public/stylesheets">
<value>file://$MODULE_DIR$/public/stylesheets</value>
</entry>
<entry key="test/mailers/previews">
<value>file://$MODULE_DIR$/test/mailers/previews</value>
<value>file://$MODULE_DIR$/test/mailers/previews</value>
</entry>
<entry key="tmp">
<value>file://$MODULE_DIR$/tmp</value>
</entry>
<entry key="vendor">
<value>file://$MODULE_DIR$/vendor</value>
</entry>
<entry key="vendor/assets">
<value>file://$MODULE_DIR$/vendor/assets</value>
</entry>
</component>
</module>

6
.idea/vcs.xml generated
View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -16,7 +16,7 @@ WORKDIR /rails
# Install base packages
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 libpq5 redis-tools && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Set production environment
@ -30,7 +30,7 @@ FROM base AS build
# Install packages needed to build gems and node modules
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential git node-gyp pkg-config python-is-python3 && \
apt-get install --no-install-recommends -y build-essential git node-gyp pkg-config python-is-python3 libpq-dev && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Install JavaScript dependencies
@ -59,7 +59,7 @@ COPY . .
RUN bundle exec bootsnap precompile app/ lib/
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
RUN RAILS_BUILD=1 SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
RUN rm -rf node_modules
@ -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.

29
Gemfile
View File

@ -41,9 +41,28 @@ gem "thruster", require: false
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"
gem 'devise', '~> 4.9'
gem 'activeadmin', '~> 3.2'
gem 'friendly_id', '~> 5.5'
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 "ahoy_matey", "~> 5.2"
# gem "whenever", "~> 1.0"
gem "ruby-openai", "~> 7.3"
gem "httparty", "~> 0.22.0"
gem "down", "~> 5.4"
gem "aws-sdk-s3", "~> 1.177"
gem "sidekiq", "~> 7.3"
gem "sidekiq-scheduler", "~> 5.0"
gem "image_processing", "~> 1.13"
gem "ruby-vips", "~> 2.2"
gem "mini_magick", "~> 4.13.2"
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
@ -61,6 +80,10 @@ group :development do
gem "web-console"
end
group :production do
gem "pg", "~> 1.5"
end
group :test do
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
gem "capybara"

View File

@ -84,10 +84,30 @@ GEM
uri (>= 0.13.1)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ahoy_matey (5.3.0)
activesupport (>= 7)
device_detector (>= 1)
safely_block (>= 0.4)
arbre (1.7.0)
activesupport (>= 3.0.0)
ruby2_keywords (>= 0.0.2)
ast (2.4.2)
aws-eventstream (1.3.0)
aws-partitions (1.1044.0)
aws-sdk-core (3.217.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.97.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.179.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2)
base64 (0.2.0)
bcrypt (3.1.20)
bcrypt_pbkdf (1.1.1)
@ -120,6 +140,7 @@ GEM
debug (1.10.0)
irb (~> 1.10)
reline (>= 0.3.8)
device_detector (1.1.3)
devise (4.9.4)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
@ -127,11 +148,30 @@ GEM
responders
warden (~> 1.2.3)
dotenv (3.1.7)
down (5.4.2)
addressable (~> 2.8)
drb (2.2.1)
ed25519 (1.3.0)
erubi (1.13.1)
et-orbi (1.2.11)
tzinfo
event_stream_parser (1.0.0)
faraday (2.12.2)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-multipart (1.1.0)
multipart-post (~> 2.0)
faraday-net_http (3.4.0)
net-http (>= 0.5.0)
ffi (1.17.1-aarch64-linux-gnu)
ffi (1.17.1-aarch64-linux-musl)
ffi (1.17.1-arm-linux-gnu)
ffi (1.17.1-arm-linux-musl)
ffi (1.17.1-arm64-darwin)
ffi (1.17.1-x86_64-darwin)
ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl)
formtastic (5.0.0)
actionpack (>= 6.0.0)
formtastic_i18n (0.7.0)
@ -145,20 +185,29 @@ GEM
has_scope (0.8.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
i18n (1.14.6)
httparty (0.22.0)
csv
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
image_processing (1.13.0)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
inherited_resources (1.14.0)
actionpack (>= 6.0)
has_scope (>= 0.6)
railties (>= 6.0)
responders (>= 2)
io-console (0.8.0)
irb (1.14.3)
irb (1.15.1)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jbuilder (2.13.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
jmespath (1.6.2)
jquery-rails (4.6.0)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
@ -189,7 +238,7 @@ GEM
activerecord
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
language_server-protocol (3.17.0.3)
language_server-protocol (3.17.0.4)
logger (1.6.5)
loofah (2.24.0)
crass (~> 1.0.2)
@ -201,9 +250,17 @@ GEM
net-smtp
marcel (1.0.4)
matrix (0.4.2)
meta-tags (2.22.1)
actionpack (>= 6.0.0, < 8.1)
mini_magick (4.13.2)
mini_mime (1.1.5)
minitest (5.25.4)
msgpack (1.7.5)
multi_xml (0.7.1)
bigdecimal (~> 3.1)
multipart-post (2.4.1)
net-http (0.6.0)
uri
net-imap (0.5.5)
date
net-protocol
@ -211,7 +268,7 @@ GEM
net-protocol
net-protocol (0.2.2)
timeout
net-scp (4.0.0)
net-scp (4.1.0)
net-ssh (>= 2.6.5, < 8.0.0)
net-sftp (4.0.0)
net-ssh (>= 5.0.0, < 8.0.0)
@ -219,21 +276,21 @@ GEM
net-protocol
net-ssh (7.3.0)
nio4r (2.7.4)
nokogiri (1.18.1-aarch64-linux-gnu)
nokogiri (1.18.2-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.1-aarch64-linux-musl)
nokogiri (1.18.2-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.1-arm-linux-gnu)
nokogiri (1.18.2-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.1-arm-linux-musl)
nokogiri (1.18.2-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.1-arm64-darwin)
nokogiri (1.18.2-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.1-x86_64-darwin)
nokogiri (1.18.2-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.1-x86_64-linux-gnu)
nokogiri (1.18.2-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.1-x86_64-linux-musl)
nokogiri (1.18.2-x86_64-linux-musl)
racc (~> 1.4)
orm_adapter (0.5.0)
ostruct (0.6.1)
@ -241,6 +298,10 @@ GEM
parser (3.3.7.0)
ast (~> 2.4.1)
racc
pg (1.5.9)
pp (0.6.2)
prettyprint
prettyprint (0.2.0)
propshaft (1.1.0)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
@ -250,11 +311,11 @@ GEM
date
stringio
public_suffix (6.0.1)
puma (6.5.0)
puma (6.6.0)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.1.8)
rack (3.1.9)
rack-session (2.1.0)
base64 (>= 0.1.0)
rack (>= 3.0.0)
@ -299,6 +360,8 @@ GEM
i18n
rdoc (6.11.0)
psych (>= 4.0.0)
redis-client (0.23.2)
connection_pool
regexp_parser (2.10.0)
reline (0.6.0)
io-console (~> 0.5)
@ -306,17 +369,17 @@ GEM
actionpack (>= 5.2)
railties (>= 5.2)
rexml (3.4.0)
rubocop (1.70.0)
rubocop (1.71.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.36.2, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.37.0)
rubocop-ast (1.38.0)
parser (>= 3.3.1.0)
rubocop-minitest (0.36.0)
rubocop (>= 1.61, < 2.0)
@ -324,7 +387,7 @@ GEM
rubocop-performance (1.23.1)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.29.0)
rubocop-rails (2.29.1)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.52.0, < 2.0)
@ -334,17 +397,39 @@ GEM
rubocop-minitest
rubocop-performance
rubocop-rails
ruby-openai (7.3.1)
event_stream_parser (>= 0.3.0, < 2.0.0)
faraday (>= 1)
faraday-multipart (>= 1)
ruby-progressbar (1.13.0)
ruby-vips (2.2.2)
ffi (~> 1.12)
logger
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1)
safely_block (0.4.1)
securerandom (0.4.1)
selenium-webdriver (4.27.0)
selenium-webdriver (4.28.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
solid_cable (3.0.5)
sidekiq (7.3.8)
base64
connection_pool (>= 2.3.0)
logger
rack (>= 2.2.4)
redis-client (>= 0.22.2)
sidekiq-scheduler (5.0.6)
rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8)
tilt (>= 1.4.0, < 3)
sitemap_generator (6.3.0)
builder (~> 3.0)
solid_cable (3.0.7)
actioncable (>= 7.2)
activejob (>= 7.2)
activerecord (>= 7.2)
@ -353,7 +438,7 @@ GEM
activejob (>= 7.2)
activerecord (>= 7.2)
railties (>= 7.2)
solid_queue (1.1.2)
solid_queue (1.1.3)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1)
@ -383,6 +468,7 @@ GEM
thruster (0.1.10-arm64-darwin)
thruster (0.1.10-x86_64-darwin)
thruster (0.1.10-x86_64-linux)
tilt (2.6.0)
timeout (0.4.3)
turbo-rails (2.0.11)
actionpack (>= 6.0.0)
@ -424,21 +510,35 @@ PLATFORMS
DEPENDENCIES
activeadmin (~> 3.2)
ahoy_matey (~> 5.2)
aws-sdk-s3 (~> 1.177)
bootsnap
brakeman
capybara
cssbundling-rails
debug
devise (~> 4.9)
down (~> 5.4)
friendly_id (~> 5.5)
httparty (~> 0.22.0)
image_processing (~> 1.13)
jbuilder
jsbundling-rails
kamal
kaminari (~> 1.2)
meta-tags (~> 2.22)
mini_magick (~> 4.13.2)
pg (~> 1.5)
propshaft
puma (>= 5.0)
rails (~> 8.0.1)
rubocop-rails-omakase
ruby-openai (~> 7.3)
ruby-vips (~> 2.2)
selenium-webdriver
sidekiq (~> 7.3)
sidekiq-scheduler (~> 5.0)
sitemap_generator (~> 6.3)
solid_cable
solid_cache
solid_queue

View File

@ -1,4 +1,5 @@
ActiveAdmin.register AdminUser do
menu label: "AdminUser Manager", parent: "系统管理"
permit_params :email, :password, :password_confirmation
index do
@ -24,5 +25,4 @@ ActiveAdmin.register AdminUser do
end
f.actions
end
end

View File

@ -0,0 +1,83 @@
ActiveAdmin.register_page "Ahoy Dashboard" do
menu label: "总览", parent: "数据统计", priority: 1
page_action :toggle_city_status, method: :post do
city = City.find(params[:city_id])
city.update(active: !city.active)
redirect_back(fallback_location: admin_dashboard_path, notice: "城市状态已更新")
end
content title: "总览" do
columns do
column do
panel "访问统计" do
para "总访问量: #{Ahoy::Visit.count}"
para "总事件数: #{Ahoy::Event.count}"
para "独立访客数: #{Ahoy::Visit.distinct.count(:visitor_token)}"
end
end
column do
panel "热门城市" do
table_for City.by_popularity.limit(10) do
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
column("访问量") { |city| city.view_count }
end
end
end
column do
panel "热门天气艺术" do
table_for WeatherArt.by_popularity.limit(10) do
column("作品") { |art| link_to(art.to_s, admin_weather_art_path(art)) }
column("访问量") { |art| art.view_count }
end
end
end
end
columns do
column do
panel "最冷门活跃城市" do
table_for City.least_popular_active.limit(10) do
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
column("访问量") { |city| city.view_count }
column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") }
# column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") }
column("操作") { |city|
button_to "停用",
admin_ahoy_dashboard_toggle_city_status_path(city_id: city.id),
method: :post,
data: { confirm: "确定要停用 #{city.name} 吗?" }
}
end
end
end
column do
panel "热门未活跃城市" do
table_for City.most_popular_inactive.limit(10) do
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
column("访问量") { |city| city.view_count }
column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") }
column("所属区域") { |city| city.country.region.name }
column("操作") { |city|
button_to "激活",
admin_ahoy_dashboard_toggle_city_status_path(city_id: city.id),
method: :post,
data: { confirm: "确定要激活 #{city.name} 吗?" }
}
end
end
end
end
# 添加一个事件列表面板
panel "最近事件" do
table_for Ahoy::Event.order(time: :desc).limit(10) do
column :time
column :name
column :properties
end
end
end
end

32
app/admin/ahoy_events.rb Normal file
View File

@ -0,0 +1,32 @@
ActiveAdmin.register Ahoy::Event do
menu label: "事件统计", parent: "数据统计"
# See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
#
# Uncomment all parameters which should be permitted for assignment
#
# permit_params :visit_id, :user_id, :name, :properties, :time
#
# or
#
# permit_params do
# permitted = [:visit_id, :user_id, :name, :properties, :time]
# permitted << :other if params[:action] == 'create' && current_user.admin?
# permitted
# end
# menu priority: 101, label: "事件统计"
actions :index
index do
column :id
column :name
column :time
column :properties
column :user_id
end
filter :name
filter :time
filter :properties
end

View File

@ -0,0 +1,34 @@
# app/admin/ahoy_management.rb
ActiveAdmin.register_page "Ahoy Management" do
menu label: "访问数据管理", parent: "系统管理"
content title: "访问数据管理" do
columns do
column do
panel "数据统计" do
attributes_table_for :ahoy do
row("总事件数") { Ahoy::Event.count }
row("总访问数") { Ahoy::Visit.count }
row("最早事件") { Ahoy::Event.minimum(:time)&.strftime("%Y-%m-%d %H:%M:%S") }
row("最早访问") { Ahoy::Visit.minimum(:started_at)&.strftime("%Y-%m-%d %H:%M:%S") }
end
end
end
column do
panel "操作" do
div class: "buttons" do
button_to "立即清理旧数据", admin_ahoy_management_cleanup_path, method: :post,
data: { confirm: "确定要清理3个月前的数据吗" },
class: "button"
end
end
end
end
end
page_action :cleanup, method: :post do
CleanAhoyDataWorker.perform_async
redirect_to admin_ahoy_management_path, notice: "清理任务已加入队列"
end
end

36
app/admin/ahoy_visits.rb Normal file
View File

@ -0,0 +1,36 @@
ActiveAdmin.register Ahoy::Visit do
menu label: "访客统计", parent: "数据统计"
# See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
#
# Uncomment all parameters which should be permitted for assignment
#
# permit_params :visit_token, :visitor_token, :user_id, :ip, :user_agent, :referrer, :referring_domain, :landing_page, :browser, :os, :device_type, :country, :region, :city, :latitude, :longitude, :utm_source, :utm_medium, :utm_term, :utm_content, :utm_campaign, :app_version, :os_version, :platform, :started_at
#
# or
#
# permit_params do
# permitted = [:visit_token, :visitor_token, :user_id, :ip, :user_agent, :referrer, :referring_domain, :landing_page, :browser, :os, :device_type, :country, :region, :city, :latitude, :longitude, :utm_source, :utm_medium, :utm_term, :utm_content, :utm_campaign, :app_version, :os_version, :platform, :started_at]
# permitted << :other if params[:action] == 'create' && current_user.admin?
# permitted
# end
# menu priority: 100, label: "访问统计"
actions :index
index do
column :id
column :visitor_token
column :ip
column :user_agent
column :started_at
column :city
column :country
column :region
end
filter :started_at
filter :city
filter :country
end

View File

@ -1,4 +1,5 @@
ActiveAdmin.register City do
menu label: "City Manager", parent: "系统管理"
controller do
def find_resource
scoped_collection.friendly.find(params[:id])
@ -10,7 +11,7 @@ ActiveAdmin.register City do
#
# Uncomment all parameters which should be permitted for assignment
#
permit_params :name, :country, :latitude, :longitude, :active, :priority, :timezone, :region, :last_weather_fetch, :last_image_generation, :slug
permit_params :name, :country_id, :latitude, :longitude, :active, :priority, :timezone, :last_weather_fetch, :last_image_generation, :slug
#
# or
#
@ -24,31 +25,30 @@ ActiveAdmin.register City do
selectable_column
id_column
column :name
column :slug
column :country
column :region do |city|
city.region
end
column :latitude
column :longitude
column :active
column :created_at
actions
end
filter :name
filter :active
filter :country, as: :select
form do |f|
f.inputs do
f.input :active
f.input :name
f.input :country, as: :String
f.input :country
f.input :latitude
f.input :longitude
f.input :priority
f.input :timezone
f.input :region
f.input :last_weather_fetch
f.input :last_image_generation
end
f.actions
end
end

42
app/admin/countries.rb Normal file
View File

@ -0,0 +1,42 @@
ActiveAdmin.register Country do
menu label: "Country Manager", parent: "系统管理"
controller do
def find_resource
scoped_collection.friendly.find(params[:id])
end
end
# See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
#
# Uncomment all parameters which should be permitted for assignment
#
permit_params :name, :code, :region_id
#
# or
#
# permit_params do
# permitted = [:name, :code, :slug, :region_id]
# permitted << :other if params[:action] == 'create' && current_user.admin?
# permitted
# end
index do
selectable_column
id_column
column :name
column :code
column :region
column :cities_count do |country|
country.cities.count
end
actions
end
form do |f|
f.inputs do
f.input :region
f.input :name
f.input :code
end
f.actions
end
end

View File

@ -1,4 +1,5 @@
# frozen_string_literal: true
ActiveAdmin.register_page "Dashboard" do
menu priority: 1, label: proc { I18n.t("active_admin.dashboard") }
@ -10,6 +11,66 @@ ActiveAdmin.register_page "Dashboard" do
end
end
columns do
column do
panel "访问统计" do
para "总访问量: #{Ahoy::Visit.count}"
para "总事件数: #{Ahoy::Event.count}"
para "独立访客数: #{Ahoy::Visit.distinct.count(:visitor_token)}"
end
end
column do
panel "热门城市" do
table_for City.by_popularity.limit(10) do
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
column("访问量") { |city| city.view_count }
end
end
end
column do
panel "热门天气艺术" do
table_for WeatherArt.by_popularity.limit(10) do
column("作品") { |art| link_to(art.to_s, admin_weather_art_path(art)) }
column("访问量") { |art| art.view_count }
end
end
end
end
columns do
column do
panel "最冷门活跃城市" do
table_for City.least_popular_active.limit(10) do
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
column("访问量") { |city| city.view_count }
# column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") }
end
end
end
column do
panel "热门未活跃城市" do
table_for City.most_popular_inactive.limit(10) do
column("城市") { |city| link_to(city.name, admin_city_path(city)) }
column("访问量") { |city| city.view_count }
column("状态") { |city| status_tag(city.active? ? "活跃" : "停用") }
column("所属区域") { |city| city.country.region.name }
end
end
end
end
# 添加一个事件列表面板
panel "最近事件" do
table_for Ahoy::Event.order(time: :desc).limit(10) do
column :time
column :name
column :properties
end
end
# Here is an example of a simple dashboard with columns and panels.
#
# columns do

30
app/admin/regions.rb Normal file
View File

@ -0,0 +1,30 @@
ActiveAdmin.register Region do
menu label: "Region Manager", parent: "系统管理"
# See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
#
# Uncomment all parameters which should be permitted for assignment
#
permit_params :name, :code
#
# or
#
# permit_params do
# permitted = [:name, :code, :slug]
# permitted << :other if params[:action] == 'create' && current_user.admin?
# permitted
# end
index do
selectable_column
id_column
column :name
column :code
column :countries_count do |region|
region.countries.count
end
column :cities_count do |region|
region.cities.count
end
actions
end
end

98
app/admin/sidekiq_jobs.rb Normal file
View File

@ -0,0 +1,98 @@
# app/admin/sidekiq_tasks.rb
ActiveAdmin.register_page "Sidekiq Tasks" do
# menu label: "Sidekiq Tasks", priority: 99
menu label: "Sidekiq Tasks Manager", parent: "系统管理"
content title: "Sidekiq Tasks Management" do
div class: "sidekiq-tasks" do
panel "Manual Task Execution" do
div class: "task-buttons" do
div class: "task-button" do
h3 "Generate Weather Arts"
form action: admin_sidekiq_tasks_run_task_path, method: :post do
input type: "hidden", name: "authenticity_token", value: form_authenticity_token
input type: "hidden", name: "task", value: "GenerateWeatherArtsWorker"
select name: "city_id" do
City.all.map do |city|
option city.name, value: city.id
end
end
input type: "submit", value: "Run Task", class: "button"
end
end
div class: "task-button" do
h3 "Batch Generate Weather Arts"
form action: admin_sidekiq_tasks_run_task_path, method: :post do
input type: "hidden", name: "authenticity_token", value: form_authenticity_token
input type: "hidden", name: "task", value: "BatchGenerateWeatherArts"
input type: "submit", value: "Run Task", class: "button"
end
end
div class: "task-button" do
h3 "Refresh Sitemap"
form action: admin_sidekiq_tasks_run_task_path, method: :post do
input type: "hidden", name: "authenticity_token", value: form_authenticity_token
input type: "hidden", name: "task", value: "RefreshSitemapWorker"
input type: "submit", value: "Run Task", class: "button"
end
end
div class: "task-button" do
h3 "Clean Ahoy Data"
form action: admin_sidekiq_tasks_run_task_path, method: :post do
input type: "hidden", name: "authenticity_token", value: form_authenticity_token
input type: "hidden", name: "task", value: "CleanAhoyDataWorker"
input type: "submit", value: "Run Task", class: "button"
end
end
end
end
panel "Sidekiq Statistics" do
stats = Sidekiq::Stats.new
table class: "sidekiq-stats" do
tr do
th "Processed Jobs"
td stats.processed
end
tr do
th "Failed Jobs"
td stats.failed
end
tr do
th "Enqueued Jobs"
td stats.enqueued
end
tr do
th "Scheduled Jobs"
td stats.scheduled_size
end
tr do
th "Retry Set Size"
td stats.retry_size
end
end
end
end
end
page_action :run_task, method: :post do
task_name = params[:task]
city_id = params[:city_id]
case task_name
when "BatchGenerateWeatherArts"
BatchGenerateWeatherArtsWorker.perform_async
when "GenerateWeatherArtsWorker"
GenerateWeatherArtWorker.perform_async(city_id)
when "RefreshSitemapWorker"
RefreshSitemapWorker.perform_async
when "CleanAhoyDataWorker"
CleanAhoyDataWorker.perform_async
end
redirect_to admin_sidekiq_tasks_path, notice: "Task #{task_name} has been queued"
end
end

View File

@ -1,5 +1,10 @@
ActiveAdmin.register WeatherArt do
menu label: "WeatherArt Manager", parent: "系统管理"
controller do
def find_resource
scoped_collection.friendly.find(params[:id])
end
end
# See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
#
@ -17,9 +22,11 @@ ActiveAdmin.register WeatherArt do
permit_params :city_id, :weather_date, :description, :temperature,
:feeling_temp, :humidity, :wind_scale, :wind_speed,
:precipitation, :pressure, :visibility, :cloud,
:prompt, :image
:prompt, :image, :slug
remove_filter :image_attachment, :image_blob
filter :city_id
filter :weather_data
index do
selectable_column
@ -29,7 +36,7 @@ ActiveAdmin.register WeatherArt do
column :description
column :temperature
column :image do |weather_art|
image_tag(weather_art.image, size: '100x100') if weather_art.image.attached?
image_tag(weather_art.image, size: "100x100") if weather_art.image.attached?
end
actions
end
@ -74,5 +81,4 @@ ActiveAdmin.register WeatherArt do
end
f.actions
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -1,2 +0,0 @@
//= require active_admin/base
import "@activeadmin/activeadmin";

View File

@ -1,3 +1,4 @@
@import "photoswipe/dist/photoswipe.css";
@tailwind base;
@tailwind components;

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,74 @@
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 :log_browser_info
# allow_browser versions: :modern
# allow_browser versions: :modern,
# patterns: [
# # 鸿蒙系统相关
# /OpenHarmony/, # 鸿蒙系统标识
# /ArkWeb\/[\d.]+/, # 鸿蒙浏览器内核
# /Mobile HuaweiBrowser/, # 华为浏览器(新格式)
# /HuaweiBrowser\/[\d.]+/, # 华为浏览器(旧格式)
#
# # 夸克浏览器(更宽松的匹配)
# /Quark[\s\/][\d.]+/, # 匹配 "Quark/7.4.6.681" 或 "Quark 7.4.6.681"
#
# /Mobile Safari/,
# /Chrome\/[\d.]+/,
# /Quark\/[\d.]+/,
# /HuaweiBrowser\/[\d.]+/,
# /MiuiBrowser\/[\d.]+/,
# /VivoBrowser\/[\d.]+/,
# /OppoBrowser\/[\d.]+/,
# /UCBrowser\/[\d.]+/,
# /QQBrowser\/[\d.]+/,
# /MicroMessenger\/[\d.]+/,
# /Alipay/,
# /BaiduBoxApp/,
# /baiduboxapp/i,
# /SogouMobile/,
# /Weibo/,
# /DingTalk/,
# /ToutiaoMicroApp/,
# /BytedanceWebview/,
# /ArkWeb/
# ],
# on_failure: ->(browser) {
# Rails.logger.warn <<~BROWSER_INFO
# Browser Blocked:
# User Agent: #{browser.ua}
# Name: #{browser.name}
# Version: #{browser.version}
# Platform: #{browser.platform.name}
# Device: #{browser.device.name}
# Mobile: #{browser.mobile?}
# Modern: #{browser.modern?}
# Bot: #{browser.bot?}
# BROWSER_INFO
# }
before_action :set_locale
after_action :track_action
def log_browser_info
# 构建详细的浏览器信息
Rails.logger.debug "User Agent: #{request.user_agent}"
# 如果是被拦截的浏览器,记录额外信息
# unless browser_allowed?
# Rails.logger.info "User Agent: #{request.user_agent}"
# end
end
protected
def track_action
ahoy.track "Viewed Application", request.path_parameters
end
private
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
end
end

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,9 +1,42 @@
class CitiesController < ApplicationController
def index
@cities = City.all.order(:name)
@regions = Region.includes(:countries).order(:name)
@cities = City.includes(:country, country: :region).order(:name)
if params[:region]
@current_region = Region.friendly.find(params[:region])
@cities = @cities.by_region(@current_region.id)
end
if params[:country]
@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])
ahoy.track "View City", {
city_id: @city.id,
name: @city.name,
event_type: "city_view"
}
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

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

@ -0,0 +1,33 @@
class SitemapsController < ApplicationController
def show
path = params[:path]
bucket_name =
Rails.env.production? ?
ENV.fetch("AWS_BUCKET", Rails.application.credentials.dig(:aws, :bucket)) :
ENV.fetch("AWS_DEV_BUCKET", Rails.application.credentials.dig(:aws_dev, :bucket))
Rails.logger.info "Sitemap: #{path}"
begin
s3_client = Aws::S3::Client.new
response = s3_client.get_object(
bucket: bucket_name,
key: "sitemaps/#{path}"
)
expires_in 12.hours, public: true
content_type = response.content_type || "application/xml"
send_data(
response.body.read,
filename: path,
type: content_type,
disposition: "inline"
)
rescue Aws::S3::Errors::NoSuchKey
render status: :not_found
rescue Aws::S3::Errors::ServiceError => e
Rails.logger.error "S3 Error: #{e.message}"
render status: :internal_server_error
end
end
end

View File

@ -1,6 +1,36 @@
class WeatherArtsController < ApplicationController
def show
@city = City.friendly.find(params[:city_id])
@weather_art = @city.weather_arts.find(params[:id])
@weather_art = @city.weather_arts.friendly.find(params[:slug])
@previous_weather_art = @city.weather_arts
.where("id < ?", @weather_art.id)
.order(id: :desc)
.first
@next_weather_art = @city.weather_arts
.where("id > ?", @weather_art.id)
.order(id: :asc)
.first
ahoy.track "View Weather Art", {
weather_art_id: @weather_art.id,
city_id: @weather_art.city_id,
event_type: "weather_art_view"
}
ahoy.track "View City", {
city_id: @city.id,
name: @city.name,
event_type: "city_view"
}
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

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

View File

@ -1,2 +1,51 @@
module WeatherArtsHelper
def weather_description_icon(description)
case description&.downcase
when /rain/
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg>'.html_safe
when /cloud/
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" 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>'.html_safe
when /sun|clear/
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>'.html_safe
else
'<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>'.html_safe
end
end
def weather_stat_icon(type)
case type
when "temperature"
'<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 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>'.html_safe
when "wind"
'<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>'.html_safe
when "humidity"
'<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="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
</svg>'.html_safe
when "visibility"
'<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 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>'.html_safe
when "pressure"
'<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="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>'.html_safe
when "cloud"
'<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>'.html_safe
end
end
end

View File

@ -0,0 +1,6 @@
import './add_jquery'
import "jquery/dist/jquery"
import "jquery-ui/dist/jquery-ui"
import "jquery-ujs"
import "@activeadmin/activeadmin"

View File

@ -0,0 +1,4 @@
import jquery from 'jquery'
import $ from 'jquery'
window.jQuery = jquery
window.$ = $

View File

@ -1,3 +1,10 @@
// Entry point for the build script in your package.json
import "@hotwired/turbo-rails"
import "@hotwired/stimulus"
import "@fontsource/playfair-display/400.css";
import "@fontsource/playfair-display/700.css";
import "@fontsource/raleway/400.css";
import "@fontsource/raleway/600.css";
import "./controllers"
import "./active_admin"

View File

@ -6,3 +6,9 @@ import { application } from "./application"
import HelloController from "./hello_controller"
application.register("hello", HelloController)
import PhotoSwipeLightBoxController from "./photo_swipe_lightbox_controller"
console.log("ready to register photo-swipe")
application.register("photo-swipe-lightbox", PhotoSwipeLightBoxController)
console.log("successful to register photo-swipe")

View File

@ -0,0 +1,42 @@
import { Controller } from "@hotwired/stimulus"
import PhotoSwipeLightbox from 'photoswipe/lightbox'
import PhotoSwipe from 'photoswipe'
import 'photoswipe/dist/photoswipe.css'
export default class extends Controller {
static targets = ['image', 'gallery']
connect() {
this.initPhotoSwipeLightbox()
}
initPhotoSwipeLightbox() {
const lightbox = new PhotoSwipeLightbox({
gallery: this.galleryTarget,
children: 'a',
pswpModule: PhotoSwipe,
initialZoomInEndEvent: 'mousedown',
dataSource: (items) => {
return items.map((item) => ({
src: item.dataset.pswpSrc,
w: parseInt(item.dataset.pswpWidth, 10),
h: parseInt(item.dataset.pswpHeight, 10),
title: item.dataset.pswpCaption,
}))
},
padding: { top: 0, bottom: 0, left: 0, right: 0 }, // 自定义图片与页面边界的填充
closeOnScroll: false,
zoom: true, // 启用缩放功能
bgOpacity: 0.9, // 背景透明度
pswpUIOptions: {
arrowPrev: true,
arrowNext: true,
zoom: true, // 添加缩放按钮
fullscreen: true, // 添加全屏按钮
counter: true, // 显示当前图片编号
}
})
lightbox.init()
// console.log('PhotoSwipeLightbox instance:', lightbox);
}
}

View File

@ -1,7 +0,0 @@
class ApplicationJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock
# retry_on ActiveRecord::Deadlocked
# Most jobs are safe to ignore if the underlying records are no longer available
# discard_on ActiveJob::DeserializationError
end

View File

@ -1,7 +1,7 @@
class AdminUser < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable,
devise :database_authenticatable,
:recoverable, :rememberable, :validatable
def self.ransackable_attributes(auth_object = nil)

14
app/models/ahoy/event.rb Normal file
View File

@ -0,0 +1,14 @@
class Ahoy::Event < ApplicationRecord
# include Ahoy::QueryMethods
self.table_name = "ahoy_events"
belongs_to :visit
belongs_to :user, optional: true
serialize :properties, coder: JSON
def self.ransackable_attributes(auth_object = nil)
[ "id", "id_value", "name", "properties", "time", "user_id", "visit_id" ]
end
end

10
app/models/ahoy/visit.rb Normal file
View File

@ -0,0 +1,10 @@
class Ahoy::Visit < ApplicationRecord
self.table_name = "ahoy_visits"
has_many :events, class_name: "Ahoy::Event"
belongs_to :user, optional: true
def self.ransackable_attributes(auth_object = nil)
[ "app_version", "browser", "city", "country", "device_type", "id", "ip", "landing_page", "latitude", "longitude", "os", "os_version", "platform", "referrer", "referring_domain", "region", "started_at", "user_agent", "user_id", "utm_campaign", "utm_content", "utm_medium", "utm_source", "utm_term", "visit_token", "visitor_token" ]
end
end

View File

@ -1,13 +1,96 @@
class City < ApplicationRecord
extend FriendlyId
friendly_id :name, use: :slugged
friendly_id :slug_candidates, use: :slugged
belongs_to :country
has_many :weather_arts, dependent: :destroy
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :city_id
has_many :events, class_name: "Ahoy::Event", foreign_key: :city_id
delegate :region, to: :country
validates :name, presence: true
validates :latitude, presence: true
validates :longitude, presence: true
delegate :region, to: :country
scope :by_region, ->(region_id) { joins(:country).where(countries: { region_id: region_id }) }
scope :by_country, ->(country_id) { where(country_id: country_id) }
scope :active, -> { where(active: true) }
scope :by_popularity, -> {
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.city_id') = cities.id
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count DESC")
else
joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'city_id')::integer = cities.id
AND ahoy_events.properties::jsonb->>'event_type' = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count DESC")
end
}
scope :least_popular_active, -> {
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
active
.joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.city_id') = cities.id
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count ASC, cities.name ASC")
else
active
.joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'city_id')::integer = cities.id
AND ahoy_events.properties::jsonb->>'event_type' = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count ASC, cities.name ASC")
end
}
scope :most_popular_inactive, -> {
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
where(active: false)
.joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.city_id') = cities.id
AND json_extract(ahoy_events.properties, '$.event_type') = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("COUNT(ahoy_events.id) DESC, cities.name ASC")
else
where(active: false)
.joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'city_id')::integer = cities.id
AND ahoy_events.properties::jsonb->>'event_type' = 'city_view'")
.group("cities.id")
.select("cities.*, COUNT(ahoy_events.id) as visit_count")
.order("COUNT(ahoy_events.id) DESC, cities.name ASC")
end
}
def to_s
name
end
def slug_candidates
[
:name,
[ :country, :name ]
]
end
def localized_name
I18n.t("cities.#{name.parameterize.underscore}", default: name)
end
def full_name
"#{name}, #{country}"
end
def should_generate_new_friendly_id?
name_changed? || super
end
@ -17,7 +100,32 @@ class City < ApplicationRecord
end
def self.ransackable_attributes(auth_object = nil)
["active", "country", "created_at", "id", "id_value", "last_image_generation", "last_weather_fetch", "latitude", "longitude", "name", "priority", "region", "slug", "timezone", "updated_at"]
[ "active", "country_id", "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
def view_count
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
Ahoy::Event.where("json_extract(properties, '$.event_type') = 'city_view' AND json_extract(properties, '$.city_id') = ?", self.id).count
else
Ahoy::Event.where("properties::jsonb->>'event_type' = 'city_view' AND (properties::jsonb->>'city_id')::integer = ?", self.id).count
end
end
end

26
app/models/country.rb Normal file
View File

@ -0,0 +1,26 @@
class Country < ApplicationRecord
extend FriendlyId
friendly_id :name, use: :slugged
belongs_to :region
has_many :cities, dependent: :restrict_with_error
validates :name, presence: true
validates :code, presence: true, uniqueness: true
def to_s
name
end
def localized_name
I18n.t("countries.#{code}")
end
def self.ransackable_attributes(auth_object = nil)
[ "code", "created_at", "id", "id_value", "name", "region_id", "slug", "updated_at" ]
end
def self.ransackable_associations(auth_object = nil)
[ "cities", "region" ]
end
end

28
app/models/region.rb Normal file
View File

@ -0,0 +1,28 @@
class Region < ApplicationRecord
extend FriendlyId
friendly_id :name, use: :slugged
has_many :countries, dependent: :restrict_with_error
has_many :cities, through: :countries
validates :name, presence: true
validates :code, presence: true, uniqueness: true
def to_s
name
end
def localized_name
I18n.t("regions.#{code}")
end
# 模型中允许被搜索的关联
def self.ransackable_associations(auth_object = nil)
[ "countries", "cities" ]
end
# 允许被搜索的属性列表
def self.ransackable_attributes(auth_object = nil)
[ "code", "created_at", "id", "id_value", "name", "slug", "updated_at" ]
end
end

View File

@ -1,16 +1,58 @@
class WeatherArt < ApplicationRecord
belongs_to :city
extend FriendlyId
friendly_id :weather_date, use: :slugged
belongs_to :city
has_one_attached :image
has_one_attached :image_with_watermark
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :weather_art_id
has_many :events, class_name: "Ahoy::Event", foreign_key: :weather_art_id
validates :weather_date, presence: true
validates :city_id, presence: true
scope :by_popularity, -> {
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.weather_art_id') = weather_arts.id
AND json_extract(ahoy_events.properties, '$.event_type') = 'weather_art_view'")
.group("weather_arts.id")
.select("weather_arts.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count DESC")
else
joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'weather_art_id')::integer = weather_arts.id
AND ahoy_events.properties::jsonb->>'event_type' = 'weather_art_view'")
.group("weather_arts.id")
.select("weather_arts.*, COUNT(ahoy_events.id) as visit_count")
.order("visit_count DESC")
end
}
def should_generate_new_friendly_id?
weather_date_changed? || city_id_changed? || super
end
def to_s
"#{city.name} - #{weather_date.strftime('%Y-%m-%d')}"
end
def self.ransackable_associations(auth_object = nil)
["city", "image_attachment", "image_blob"]
[ "city", "image_attachment", "image_blob" ]
end
def self.ransackable_attributes(auth_object = nil)
[ "city_id", "cloud", "created_at", "description", "feeling_temp", "humidity", "id", "id_value", "precipitation", "pressure", "prompt", "temperature", "updated_at", "visibility", "weather_date", "wind_scale", "wind_speed" ]
end
def view_count
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
Ahoy::Event.where("json_extract(properties, '$.event_type') = 'weather_art_view' AND json_extract(properties, '$.weather_art_id') = ?", self.id).count
else
Ahoy::Event.where("properties::jsonb->>'event_type' = 'weather_art_view' AND (properties::jsonb->>'weather_art_id')::integer = ?", self.id).count
end
end
def image_url
image.attached? ? image.blob : nil
end
end

View File

@ -0,0 +1,64 @@
class AiService
def initialize
@client = OpenAI::Client.new(
access_token: Rails.application.credentials.openai.token,
uri_base: Rails.application.credentials.openai.uri,
request_timeout: 240
)
end
def generate_prompt(city, weather_data)
response = @client.chat(
parameters: {
model: "gpt-4",
messages: [ {
role: "system",
content: "You are a professional artist creating prompts for DALL-E 3. Create realistic, artistic weather scenes featuring iconic landmarks."
}, {
role: "user",
content: generate_prompt_request(city, weather_data)
} ],
temperature: 0.7,
max_tokens: 300
}
)
response.dig("choices", 0, "message", "content")
end
def generate_image(prompt)
response = @client.images.generate(
parameters: {
model: "dall-e-3",
prompt: prompt,
size: "1792x1024",
quality: "standard",
n: 1
}
)
response.dig("data", 0, "url")
end
private
def generate_prompt_request(city, weather_data)
<<~PROMPT
Create a DALL-E 3 prompt for a weather scene in #{city.name}, #{city.country.name}.
Weather conditions:
- Temperature: #{weather_data[:temperature]}°C
- Weather: #{weather_data[:description]}
- Cloud cover: #{weather_data[:cloud]}%
- Time: #{weather_data[:time]}
Requirements:
- Feature iconic landmarks or architecture from #{city.name}
- Realistic style
- Weather conditions should be clearly visible
- Atmospheric and artistic composition
Generate a detailed, creative prompt that will produce a beautiful and realistic image.
PROMPT
end
end

View File

@ -0,0 +1,38 @@
# app/services/weather_service.rb
class WeatherService
include HTTParty
base_uri Rails.application.credentials.dig(:qweather, :uri)
def initialize
@api_key = Rails.application.credentials.qweather.token
end
def get_weather(latitude, longitude)
Rails.logger.debug "Get Weather for #{latitude},#{longitude}"
response = self.class.get(
"/weather/now",
headers: {
"X-QW-Api-Key" => "#{@api_key}"
},
query: {
location: "#{longitude},#{latitude}"
})
return nil unless response.success?
data = response["now"]
{
temperature: data["temp"].to_f,
feeling_temp: data["feelsLike"].to_f,
humidity: data["humidity"].to_f,
wind_scale: "#{data['windScale']}",
wind_speed: data["windSpeed"].to_f,
precipitation: data["precip"].to_f,
pressure: data["pressure"].to_f,
visibility: data["vis"].to_f,
cloud: data["cloud"].to_f,
description: data["text"],
time: response["updateTime"]
}
end
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

@ -0,0 +1,126 @@
<!-- app/views/cities/_city.html.erb -->
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-all duration-300 group overflow-hidden">
<% if city.latest_weather_art&.image&.attached? %>
<!-- 图片和主要信息区域 -->
<div class="relative">
<!-- 图片 -->
<figure class="aspect-[16/9] overflow-hidden">
<%= image_tag city.latest_weather_art.image,
class: "w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-500" %>
</figure>
<!-- 渐变遮罩 - 使用多层渐变提供更好的文字可读性 -->
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent"></div>
<div class="absolute inset-0 bg-gradient-to-b from-black/30 via-transparent to-transparent"></div>
<!-- 温度显示 - 放在右上角 -->
<div class="absolute top-4 right-4 bg-black/30 backdrop-blur-sm rounded-xl px-3 py-2 text-white">
<div class="text-2xl font-bold leading-none">
<%= city.latest_weather_art.temperature %>°C
</div>
<div class="text-xs text-white/80">
Feels <%= city.latest_weather_art.feeling_temp %>°C
</div>
</div>
<!-- 城市名称和位置 - 放在底部 -->
<div class="absolute bottom-0 left-0 right-0 p-6">
<h3 class="text-2xl font-display text-white font-bold mb-2 drop-shadow-lg">
<%= city.name %>
</h3>
<div class="flex items-center gap-2 text-white/90 text-sm drop-shadow-lg">
<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="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
</svg>
<%= city.country.name %>, <%= city.region.name %>
</div>
</div>
</div>
<!-- 底部信息区域 -->
<div class="card-body bg-base-100">
<!-- 天气信息和更新时间 -->
<div class="flex items-center justify-between text-sm text-base-content/70 mb-3">
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary/70" 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>
<%= city.latest_weather_art.description %>
</div>
<div class="flex items-center gap-1">
<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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<%= city.latest_weather_art.weather_date.strftime("%b %d, %Y") %>
</div>
</div>
<!-- 天气详情 -->
<div class="grid grid-cols-3 gap-2 mb-4">
<div class="stat bg-base-200/50 rounded-lg p-2">
<div class="stat-title text-xs">Humidity</div>
<div class="stat-value text-lg"><%= city.latest_weather_art.humidity %>%</div>
</div>
<div class="stat bg-base-200/50 rounded-lg p-2">
<div class="stat-title text-xs">Wind</div>
<div class="stat-value text-lg"><%= city.latest_weather_art.wind_scale %></div>
</div>
<div class="stat bg-base-200/50 rounded-lg p-2">
<div class="stat-title text-xs">Visibility</div>
<div class="stat-value text-lg"><%= city.latest_weather_art.visibility %>km</div>
</div>
</div>
<!-- 按钮 -->
<div class="card-actions justify-end">
<%= link_to city_path(city),
class: "btn btn-primary btn-sm gap-2" 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>
<% else %>
<!-- 无图片时的备用显示 -->
<div class="card-body">
<h3 class="card-title font-display text-2xl mb-3"><%= city.name %></h3>
<div class="flex items-center gap-2 text-base-content/70 mb-4">
<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="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
</svg>
<%= city.country.name %>, <%= city.region.name %>
</div>
<div class="bg-base-200 rounded-lg p-4 mb-4">
<div class="grid grid-cols-2 gap-4 text-sm">
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-primary/70" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
</svg>
<span>Lat: <%= city.latitude %></span>
</div>
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-primary/70" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
</svg>
<span>Long: <%= city.longitude %></span>
</div>
</div>
</div>
<div class="card-actions justify-end">
<%= link_to city_path(city),
class: "btn btn-primary btn-sm gap-2" 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>

View File

@ -1,18 +1,126 @@
<div class="space-y-8">
<h1 class="text-3xl font-bold">Cities</h1>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<% @cities.each do |city| %>
<div class="card bg-base-200">
<div class="card-body">
<h2 class="card-title"><%= city.name %></h2>
<p>Latitude: <%= city.latitude %></p>
<p>Longitude: <%= city.longitude %></p>
<div class="card-actions justify-end">
<%= link_to "View Weather Art", city_path(city), class: "btn btn-primary" %>
</div>
</div>
<!-- app/views/cities/index.html.erb -->
<div class="min-h-screen">
<!-- 页面标题和背景 -->
<% featured_art = WeatherArt.includes(:city).joins(:image_attachment).order(created_at: :desc).first %>
<div class="relative bg-base-100">
<!-- 背景图像和渐变 -->
<% if featured_art&.image&.attached? %>
<div class="absolute inset-0 h-[60vh] overflow-hidden">
<%= image_tag featured_art.image,
class: "w-full h-full object-cover object-center" %>
<div class="absolute inset-0 bg-gradient-to-b from-base-100/40 via-base-100/80 to-base-100"></div>
</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">
<h1 class="text-5xl md:text-6xl font-display font-bold leading-tight">
Explore Cities
</h1>
<p class="text-xl md:text-2xl text-base-content/70 font-light max-w-2xl mx-auto">
Discover AI-generated weather art from cities around the world
</p>
<!-- 特色图片信息 -->
<% if featured_art %>
<div class="inline-block mt-6 px-4 py-2 bg-base-100/80 backdrop-blur-sm rounded-full text-sm">
Latest from
<span class="font-semibold"><%= featured_art.city.name %></span>,
<%= 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="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">
<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 cities_path,
class: "#{@current_region ? '' : 'active'}" do %>
All Regions
<% end %>
</li>
<div class="divider my-1"></div>
<% @regions.each do |region| %>
<li>
<%= link_to region.name,
cities_path(region: region.slug),
class: "#{@current_region == region ? 'active' : ''}" %>
</li>
<% end %>
</ul>
</div>
<% if @current_region %>
<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 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9" />
</svg>
<%= @current_country&.name || "All Countries" %>
<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 in #{@current_region.name}",
cities_path(region: @current_region.slug),
class: "#{@current_country ? '' : 'active'}" %>
</li>
<div class="divider my-1"></div>
<% @current_region.countries.order(:name).each do |country| %>
<li>
<%= link_to country.name,
cities_path(region: @current_region.slug, country: country.slug),
class: "#{@current_country == country ? 'active' : ''}" %>
</li>
<% end %>
</ul>
</div>
<% end %>
</div>
<div class="text-sm text-base-content/70">
<%= @cities.count %> <%= 'city'.pluralize(@cities.count) %>
<% if @current_country %>
in <%= @current_country.name %>
<% elsif @current_region %>
in <%= @current_region.name %>
<% end %>
</div>
</div>
</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

@ -1,49 +1,149 @@
<div class="space-y-8">
<div class="flex justify-between items-center">
<h1 class="text-3xl font-bold"><%= @city.name %></h1>
<%= link_to "Back to Cities", cities_path, class: "btn btn-ghost" %>
</div>
<div class="stats shadow">
<div class="stat">
<div class="stat-title">Latitude</div>
<div class="stat-value"><%= @city.latitude %></div>
<div class="relative min-h-screen bg-base-200">
<!-- 背景效果 -->
<% if @city.latest_weather_art&.image&.attached? %>
<div class="fixed inset-0 -z-10">
<%= image_tag @city.latest_weather_art.image,
class: "absolute w-full h-full object-cover scale-110 filter blur-2xl opacity-25" %>
<div class="absolute inset-0 bg-gradient-to-b from-base-200/90 to-base-200/70 backdrop-blur-md"></div>
</div>
<div class="stat">
<div class="stat-title">Longitude</div>
<div class="stat-value"><%= @city.longitude %></div>
</div>
</div>
<% end %>
<div class="space-y-4">
<h2 class="text-2xl font-bold">Weather Art History</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<% @city.weather_arts.order(weather_date: :desc).each do |art| %>
<div class="card bg-base-200">
<figure>
<% if art.image.attached? %>
<%= image_tag art.image, class: "w-full h-48 object-cover" %>
<% end %>
</figure>
<div class="card-body">
<h3 class="card-title"><%= art.weather_date.strftime("%Y-%m-%d") %></h3>
<p><%= art.description %></p>
<div class="stats stats-vertical shadow">
<div class="stat">
<div class="stat-title">Temperature</div>
<div class="stat-value"><%= art.temperature %>°C</div>
</div>
<div class="stat">
<div class="stat-title">Humidity</div>
<div class="stat-value"><%= art.humidity %>%</div>
</div>
<!-- 主要内容 -->
<div class="relative z-10">
<!-- 返回导航 -->
<div class="container mx-auto px-4 py-6">
<%= link_to cities_path,
class: "btn btn-ghost btn-lg gap-2 bg-base-100/50 backdrop-blur-sm hover:bg-base-100/70 transition-all duration-300" 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>
Back to Cities
<% end %>
</div>
<!-- 城市信息头部 -->
<div class="container mx-auto px-4 mb-12">
<div class="max-w-4xl mx-auto text-center space-y-6">
<h1 class="text-4xl md:text-6xl font-display font-bold">
<span class="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
<%= @city.localized_name %>
</span>
</h1>
<div class="flex flex-wrap justify-center items-center gap-3">
<div class="badge badge-lg badge-primary gap-2">
<%= @city.country.name %>, <%= @city.region %>
</div>
<div class="badge badge-lg badge-secondary gap-2">
<%= @city.timezone.present? ? Time.current.in_time_zone(@city.timezone).strftime("%Y-%m-%d %H:%M") : "Timezone undefined" %>
</div>
</div>
<!-- 主要统计信息 -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-8">
<div class="stat bg-base-100/80 backdrop-blur-sm shadow-lg rounded-box hover:bg-base-100/90 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("temperature") %>
<div class="stat-title font-medium">Latest Weather</div>
</div>
<div class="card-actions justify-end">
<%= link_to "View Details", city_weather_art_path(@city, art), class: "btn btn-primary" %>
<div class="stat-value text-2xl"><%= @city.latest_weather_art&.temperature %>°C</div>
<div class="stat-desc mt-1"><%= @city.latest_weather_art&.description %></div>
</div>
<div class="stat bg-base-100/80 backdrop-blur-sm shadow-lg rounded-box hover:bg-base-100/90 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("location") %>
<div class="stat-title font-medium">Coordinates</div>
</div>
<div class="stat-value text-xl">
<%= @city.latitude %>°N,
<%= @city.longitude %>°E
</div>
<div class="stat-desc mt-1">Geographical Location</div>
</div>
<div class="stat bg-base-100/80 backdrop-blur-sm shadow-lg rounded-box hover:bg-base-100/90 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("history") %>
<div class="stat-title font-medium">Records</div>
</div>
<div class="stat-value text-2xl"><%= @city.weather_arts.count %></div>
<div class="stat-desc mt-1">Total Weather Arts</div>
</div>
</div>
</div>
</div>
<!-- 天气艺术历史记录 -->
<div class="container mx-auto px-4 pb-16">
<div class="max-w-7xl mx-auto space-y-8">
<!-- 标题和更新时间 -->
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<h2 class="text-3xl font-display font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Weather Art History
</h2>
<div class="card bg-base-100/80 backdrop-blur-sm shadow-lg p-4">
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" 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>
<span class="font-medium">Last Updated:</span>
<span class="text-base-content/70">
<%= time_ago_in_words(@city.last_weather_fetch) if @city.last_weather_fetch %>
</span>
</div>
</div>
</div>
<% end %>
<!-- 天气艺术卡片网格 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<% @city.weather_arts.order(weather_date: :desc).each do |art| %>
<div class="card bg-base-100/80 backdrop-blur-sm shadow-xl hover:shadow-2xl transition-all duration-300 hover:-translate-y-1">
<figure class="relative aspect-video overflow-hidden">
<% if art.image.attached? %>
<%= image_tag art.image,
class: "w-full h-full object-cover transform hover:scale-105 transition-transform duration-500" %>
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/70 to-transparent">
<div class="flex items-center justify-between text-white">
<div class="text-2xl font-bold"><%= art.temperature %>°C</div>
<div class="text-right">
<div class="font-medium"><%= art.weather_date.strftime("%H:%M") %></div>
<div class="text-sm opacity-80"><%= art.weather_date.strftime("%B %d, %Y") %></div>
</div>
</div>
</div>
<% end %>
</figure>
<div class="card-body">
<h3 class="card-title font-display">
<%= weather_description_icon(art.description) %>
<%= art.description %>
</h3>
<div class="grid grid-cols-2 gap-4 my-4">
<div class="flex items-center gap-2 text-sm">
<%= weather_stat_icon("humidity") %>
<span>Humidity: <%= art.humidity %>%</span>
</div>
<div class="flex items-center gap-2 text-sm">
<%= weather_stat_icon("wind") %>
<span>Wind: <%= art.wind_scale %></span>
</div>
</div>
<%= link_to city_weather_art_path(@city, art),
class: "btn btn-primary btn-block" do %>
View Details
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-2" 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 %>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,46 +1,68 @@
<div class="space-y-8">
<!-- 头部标题 -->
<div class="text-center space-y-4">
<h1 class="text-4xl font-bold">AI Weather Art</h1>
<p class="text-xl text-base-content/70">Discover the beauty of weather through AI-generated art</p>
</div>
<!-- 轮播图 -->
<div class="carousel w-full rounded-box">
<% WeatherArt.last(5).each_with_index do |art, index| %>
<div id="slide<%= index %>" class="carousel-item relative w-full">
<% if art.image.attached? %>
<%= image_tag art.image, class: "w-full aspect-video object-cover" %>
<% end %>
<div class="absolute flex justify-between transform -translate-y-1/2 left-5 right-5 top-1/2">
<a href="#slide<%= index == 0 ? 4 : index - 1 %>" class="btn btn-circle"></a>
<a href="#slide<%= index == 4 ? 0 : index + 1 %>" class="btn btn-circle"></a>
</div>
<div>
<!-- 首屏展示区 -->
<section class="h-screen-90 relative overflow-hidden">
<% if @featured_arts.first&.image&.attached? %>
<div class="absolute inset-0">
<%= image_tag @featured_arts.first.image, class: "w-full h-full object-cover" %>
<div class="absolute inset-0 bg-gradient-to-r from-base-100/90 to-base-100/50"></div>
</div>
<% end %>
</div>
<div class="container mx-auto px-4 h-full flex items-center relative">
<div class="max-w-2xl space-y-6">
<h1 class="text-5xl md:text-6xl font-display font-bold leading-tight">
Where Weather Meets<br>Artificial Intelligence
</h1>
<p class="text-xl text-base-content/70 font-sans">
Experience weather through the lens of AI-generated art,
bringing a new perspective to daily meteorological phenomena.
</p>
<%= link_to "Explore Cities", cities_path,
class: "btn btn-primary btn-lg mt-8 font-sans" %>
</div>
</div>
</section>
<!-- 最新天气艺术 -->
<div class="space-y-4">
<h2 class="text-2xl font-bold">Latest Weather Art</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<% WeatherArt.last(6).each do |art| %>
<div class="card bg-base-200">
<figure>
<section class="container mx-auto px-4 py-16 space-y-12">
<h2 class="text-3xl font-display font-bold text-center">Shuffle Latest Weather Art</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<% @latest_arts.each do |art| %>
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow duration-300">
<figure class="relative aspect-[4/3] overflow-hidden">
<% if art.image.attached? %>
<%= image_tag art.image, class: "w-full h-48 object-cover" %>
<%= image_tag art.image, class: "w-full h-full object-cover transform hover:scale-105 transition-transform duration-500" %>
<% end %>
</figure>
<div class="card-body">
<h3 class="card-title"><%= art.city.name %></h3>
<p><%= art.weather_date.strftime("%Y-%m-%d") %></p>
<p><%= art.description %></p>
<div class="card-actions justify-end">
<%= link_to "View Details", city_weather_art_path(art.city, art), class: "btn btn-primary" %>
<div class="flex justify-between items-start">
<div>
<h3 class="card-title font-display"><%= art.city.name %></h3>
<p class="text-base-content/70">
<%= art.weather_date.strftime("%B %d, %Y") %>
</p>
</div>
<div class="text-right">
<div class="text-2xl font-bold"><%= art.temperature %>°C</div>
<div class="text-sm text-base-content/70"><%= art.description %></div>
</div>
</div>
<div class="card-actions justify-end mt-4">
<%= link_to "View Details", city_weather_art_path(art.city, art),
class: "btn btn-primary btn-outline" %>
</div>
</div>
</div>
<% end %>
</div>
</div>
</section>
</div>
<div class="text-center mt-12 mb-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,28 +33,66 @@
<%# 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>
<script defer src="https://busuanzi.frytea.com/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>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7296634171837358"
crossorigin="anonymous"></script>
</head>
<body class="min-h-screen bg-base-100">
<div class="navbar bg-base-100">
<body class="min-h-screen bg-base-100 font-sans">
<!-- 导航栏 -->
<div class="navbar bg-base-100/80 backdrop-blur-sm fixed top-0 z-50">
<div class="container mx-auto">
<div class="flex-1">
<%= link_to "AI Weather Art", root_path, class: "btn btn-ghost normal-case text-xl" %>
<%= link_to root_path, class: "text-2xl font-display font-bold hover:text-primary transition-colors" do %>
Today AI Weather
<% end %>
</div>
<div class="flex-none">
<%= link_to "Cities", cities_path, class: "btn btn-ghost normal-case" %>
<%= 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>
<main class="container mx-auto px-4 py-8">
<!-- 主要内容 -->
<main class="pt-16">
<%= yield %>
</main>
<footer class="footer footer-center p-4 bg-base-200 text-base-content">
<div>
<p>Copyright © 2024 - All rights reserved by AI Weather Art</p>
<!-- 页脚 -->
<footer class="footer footer-center p-8 bg-base-200 text-base-content">
<div class="container mx-auto flex flex-col gap-2">
<div id="busuanzi_container" class="text-xs opacity-70">
<div class="space-x-2">
<span>Page Views: <span id="busuanzi_page_pv"></span></span>
<span>|</span>
<span>Page Visitors: <span id="busuanzi_page_uv"></span></span>
<span>|</span>
<span>Total Views: <span id="busuanzi_site_pv"></span></span>
<span>|</span>
<span>Total Visitors: <span id="busuanzi_site_uv"></span></span>
</div>
</div>
<p class="font-display opacity-80">
Copyright © 2024 - All rights reserved by AI Weather Art
</p>
</div>
</footer>
</body>
</html>

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 btn-xs #{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 btn-xs #{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-xs btn-ghost hover:bg-primary/5" %>
<% if start_page > 2 %>
<button class="join-item btn btn-xs 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-xs 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-xs 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-xs 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-xs 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 btn-xs #{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 btn-xs #{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

@ -0,0 +1,37 @@
<%# Partial _weather_stats.html.erb %>
<div class="stat bg-gradient-to-br from-primary/10 to-primary/20 hover:from-primary hover:to-primary/30 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Temperature</div>
<div class="stat-value text-3xl"><%= weather_art.temperature %>°C</div>
<div class="stat-desc">Feels like <%= weather_art.feeling_temp %>°C</div>
</div>
<div class="stat bg-gradient-to-br from-secondary/10 to-secondary/20 hover:from-secondary hover:to-secondary/30 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Wind</div>
<div class="stat-value text-3xl"><%= weather_art.wind_scale %></div>
<div class="stat-desc"><%= weather_art.wind_speed %> km/h</div>
</div>
<div class="stat bg-base-300 hover:bg-base-400 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Humidity</div>
<div class="stat-value text-3xl"><%= weather_art.humidity %>%</div>
<div class="stat-desc">Relative humidity</div>
</div>
<div class="stat bg-base-300 hover:bg-base-400 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Visibility</div>
<div class="stat-value text-3xl"><%= weather_art.visibility %> km</div>
<div class="stat-desc">Clear view distance</div>
</div>
<div class="stat bg-accent/10 hover:bg-accent p-4 rounded-lg">
<div class="stat-title font-medium text-base">Pressure</div>
<div class="stat-value text-3xl"><%= weather_art.pressure %> hPa</div>
<div class="stat-desc">Atmospheric pressure</div>
</div>
<div class="stat bg-base-200 hover:bg-base-100 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Cloud Cover</div>
<div class="stat-value text-3xl"><%= weather_art.cloud %>%</div>
<div class="stat-desc">Sky coverage</div>
</div>

View File

@ -1,46 +1,121 @@
<div class="space-y-8">
<div class="flex justify-between items-center">
<h1 class="text-3xl font-bold"><%= @weather_art.city.name %> - <%= @weather_art.weather_date.strftime("%Y-%m-%d") %></h1>
<%= link_to "Back to City", city_path(@weather_art.city), class: "btn btn-ghost" %>
</div>
<% content_for :head do %>
<script type="application/ld+json">
<%= weather_art_schema(@weather_art) %>
</script>
<% end %>
<div class="card lg:card-side bg-base-200 shadow-xl">
<figure class="lg:w-1/2">
<% if @weather_art.image.attached? %>
<%= image_tag @weather_art.image, class: "w-full h-full object-cover" %>
<div class="relative min-h-screen bg-white"> <!-- 使用更明快的背景颜色 -->
<div class="container mx-auto px-4 pt-12 pb-16">
<div class="max-w-6xl mx-auto space-y-6">
<!-- 返回导航 -->
<div class="flex items-center">
<%= link_to city_path(@weather_art.city),
class: "btn btn-ghost btn-md gap-2 bg-base-200 hover:bg-base-300 transition-all duration-300" 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>
Back to <%= @weather_art.city.name %>
<% end %>
</div>
<!-- 主要内容 -->
<div class="card bg-base-200/80 backdrop-blur-md shadow-lg overflow-hidden"> <!-- 调整透明度和阴影 -->
<div class="grid lg:grid-cols-2 gap-6 items-center">
<!-- 图片区域 -->
<% if @weather_art.image.attached? %>
<figure class="relative lg:h-[30rem] h-auto overflow-hidden rounded-lg"> <!-- 添加圆角 -->
<div class="gallery" data-controller="photo-swipe-lightbox">
<div data-photo-swipe-lightbox-target="gallery" class="h-full">
<% blob = @weather_art.image_blob %>
<%= link_to rails_blob_path(blob),
data: {
pswp_src: rails_blob_url(blob),
pswp_caption: 'Weather Art',
pswp_width: 1792,
pswp_height: 1024
} do %>
<%= image_tag @weather_art.image, class: "object-cover w-full h-full transition-transform transform hover:scale-105 ease-in-out" %>
<%#= image_tag @weather_art.watermarked_variant.processed , class: "object-cover w-full h-full transition-transform transform hover:scale-105 ease-in-out" %>
<% end %>
</div>
</div>
</figure>
<% end %>
<!-- 信息区域 -->
<div class="card-body p-8 lg:py-10 lg:px-12">
<div class="prose max-w-none">
<h1 class="font-display text-4xl md:text-5xl font-bold text-gradient mb-6">
<%= @weather_art.city.full_name %> Weather Art
</h1>
<div class="flex flex-wrap gap-4 mb-6">
<div class="badge badge-lg badge-primary">
<%= @weather_art.weather_date.strftime("%B %d, %Y") %>
</div>
<div class="badge badge-lg badge-secondary">
<%= @weather_art.weather_date.strftime("%H:%M") %>
</div>
</div>
<h2 class="text-2xl font-semibold mb-4">
<%= weather_description_icon(@weather_art.description) %>
<%= @weather_art.description %>
</h2>
<div class="divider"></div>
<div class="grid grid-cols-2 gap-4">
<%= render 'weather_stats', weather_art: @weather_art %> <!-- 使用局部渲染 -->
</div>
</div>
</div>
</div>
</div>
<!-- AI Prompt -->
<div class="bg-primary/10 backdrop-blur-md p-6 rounded-lg border border-primary/20">
<div class="flex items-center gap-3 mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<h3 class="font-display font-bold text-lg">AI Prompt</h3>
</div>
<p class="text-base-content/80 leading-relaxed">
<%= @weather_art.prompt %>
</p>
</div>
<!-- 上一个和下一个导航 -->
<div class="flex flex-col sm:flex-row justify-between items-center gap-4 mt-8">
<% if @previous_weather_art %>
<%= link_to city_weather_art_path(@city, @previous_weather_art),
class: "btn btn-outline btn-primary w-full sm:w-auto flex items-center justify-center gap-2" 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>
Previous Weather Art
<% end %>
<% end %>
<% if @next_weather_art %>
<%= link_to city_weather_art_path(@city, @next_weather_art),
class: "btn btn-outline btn-primary w-full sm:w-auto flex items-center justify-center gap-2" do %>
Next Weather Art
<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 %>
<% end %>
</div>
<% if @previous_weather_art.nil? && @next_weather_art.nil? %>
<div class="text-center text-base-content/70 py-4">
No more Weather Arts available
</div>
<% end %>
</figure>
<div class="card-body lg:w-1/2">
<h2 class="card-title"><%= @weather_art.description %></h2>
<div class="stats stats-vertical shadow">
<div class="stat">
<div class="stat-title">Temperature</div>
<div class="stat-value"><%= @weather_art.temperature %>°C</div>
<div class="stat-desc">Feels like <%= @weather_art.feeling_temp %>°C</div>
</div>
<div class="stat">
<div class="stat-title">Wind</div>
<div class="stat-value"><%= @weather_art.wind_scale %></div>
<div class="stat-desc"><%= @weather_art.wind_speed %> km/h</div>
</div>
<div class="stat">
<div class="stat-title">Humidity</div>
<div class="stat-value"><%= @weather_art.humidity %>%</div>
</div>
<div class="stat">
<div class="stat-title">Visibility</div>
<div class="stat-value"><%= @weather_art.visibility %> km</div>
</div>
</div>
<div class="mt-4">
<h3 class="font-bold">AI Prompt:</h3>
<p class="text-sm"><%= @weather_art.prompt %></p>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,53 @@
class AddWatermarkToWeatherArtWorker
include Sidekiq::Worker
def perform(weather_art_id)
@weather_art = WeatherArt.find_by(id: weather_art_id)
return unless @weather_art
add_watermark
rescue StandardError => e
Rails.logger.error "Error adding watermark to WeatherArt #{weather_art_id}: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
end
private
attr_reader :weather_art
def add_watermark
return if weather_art.image_with_watermark.attached?
watermark_path = Rails.root.join("app/assets/images/today_ai_weather_copyright_watermark1.png")
return unless File.exist?(watermark_path)
image_tempfile = nil
watermark_tempfile = nil
begin
image_tempfile = weather_art.image.download
return unless image_dimensions_are_sufficient?(image_tempfile.path)
image = ImageProcessing::Vips.source(image_tempfile.path)
watermark = ImageProcessing::Vips.source(watermark_path)
watermarked_image = image.composite(watermark, "overlay")
watermark_tempfile = Tempfile.new([ "watermarked_image", ".png" ])
watermarked_image.write_to_file(watermark_tempfile.path)
weather_art.image_with_watermark.attach(
io: File.open(watermark_tempfile.path),
filename: "#{generate_filename("watermarked")}",
content_type: "image/png"
)
ensure
watermark_tempfile.unlink if watermark_tempfile
end
end
def image_dimensions_are_sufficient?(image_path)
dimensions = ImageProcessing::Vips.source(image_path).sizes
dimensions.width >= 200 && dimensions.height >= 200
end
def generate_filename(prefix)
"#{prefix}_#{weather_art.image.filename.base}"
end
end

View File

@ -0,0 +1,32 @@
class BatchGenerateWeatherArtsWorker
include Sidekiq::Worker
GENERATION_INTERVAL = 24.hours
MAX_DURATION = 50.minutes
SLEEP_DURATION = 120.seconds
BATCH_SIZE = 20
def perform(*args)
start_time = Time.current
cities_to_process = get_eligible_cities.shuffle.take(BATCH_SIZE)
cities_to_process.each do |city|
break if Time.current - start_time > MAX_DURATION
Rails.logger.info "Generating weather art for #{city.name}"
GenerateWeatherArtWorker.perform_async(city.id)
sleep SLEEP_DURATION
end
end
private
def get_eligible_cities
cutoff_time = Time.current - GENERATION_INTERVAL
City.active
.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
end

View File

@ -0,0 +1,32 @@
# app/workers/clean_ahoy_data_worker.rb
class CleanAhoyDataWorker
include Sidekiq::Worker
sidekiq_options queue: :default, retry: false
def perform
cleanup_old_events
cleanup_old_visits
log_cleanup_results
end
private
def cleanup_old_events
cutoff_date = 3.months.ago
deleted_events_count = Ahoy::Event.where("time < ?", cutoff_date).delete_all
Rails.logger.info "Deleted #{deleted_events_count} old Ahoy events"
end
def cleanup_old_visits
cutoff_date = 3.months.ago
deleted_visits_count = Ahoy::Visit.where("started_at < ?", cutoff_date).delete_all
Rails.logger.info "Deleted #{deleted_visits_count} old Ahoy visits"
end
def log_cleanup_results
Rails.logger.info "Ahoy cleanup completed at #{Time.current}"
Rails.logger.info "Remaining events: #{Ahoy::Event.count}"
Rails.logger.info "Remaining visits: #{Ahoy::Visit.count}"
end
end

View File

@ -0,0 +1,68 @@
class GenerateWeatherArtWorker
include Sidekiq::Worker
def perform(city_id)
@city = City.find(city_id)
weather_data = fetch_weather_data
return unless weather_data
prompt = generate_prompt(weather_data)
return unless 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,
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

View File

@ -0,0 +1,58 @@
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
if Rails.env.production?
SitemapGenerator::Sitemap.adapter = SitemapGenerator::AwsSdkAdapter.new(
ENV.fetch("AWS_BUCKET", Rails.application.credentials.dig(:aws, :bucket)),
aws_access_key_id: ENV.fetch("AWS_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :access_key_id)),
aws_secret_access_key: ENV.fetch("AWS_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :secret_access_key)),
aws_region: ENV.fetch("AWS_REGION", "wnam"),
endpoint: ENV.fetch("AWS_ENDPOINT", Rails.application.credentials.dig(:aws, :endpoint)),
)
else
SitemapGenerator::Sitemap.adapter = SitemapGenerator::AwsSdkAdapter.new(
ENV.fetch("AWS_DEV_BUCKET", Rails.application.credentials.dig(:aws_dev, :bucket)),
aws_access_key_id: ENV.fetch("AWS_DEV_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :access_key_id)),
aws_secret_access_key: ENV.fetch("AWS_DEV_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :secret_access_key)),
aws_region: ENV.fetch("AWS_DEV_REGION", "wnam"),
endpoint: ENV.fetch("AWS_DEV_ENDPOINT", Rails.application.credentials.dig(:aws_dev, :endpoint)),
)
end
SitemapGenerator::Sitemap.sitemaps_path = "sitemaps/"
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 and uploaded to S3 successfully"
rescue => e
Rails.logger.error "Error refreshing sitemap: #{e.message}"
end
end

52
compose.yaml Normal file
View File

@ -0,0 +1,52 @@
services:
web:
image: songtianlun/today_ai_weather:latest
#ports:
# - "3000:3000"
pull_policy: always
environment:
- RAILS_ENV=production
- 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
sidekiq:
image: songtianlun/today_ai_weather:latest
command: bundle exec sidekiq
environment:
- RAILS_ENV=production
- 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
db:
image: postgres:16
volumes:
- ../taw_data/pg:/var/lib/postgresql/data
networks:
- dokploy-network
environment:
- POSTGRES_PASSWORD=${PG_PASSWORD}
- POSTGRES_DB=db
redis:
image: redis:7-alpine
volumes:
- ../taw_data/redis:/data
networks:
- dokploy-network
command: redis-server --appendonly yes
networks:
dokploy-network:
external: true

View File

@ -1 +1 @@
LCyDSpt/QvabmfGLE8GlLR1UPEz7QPdFj21uutpnQtJgwAbzfdWf+wh45UH97SLpnbfdy9uP0UT08j8t1bnn2danGnXDgdrhlb+NZeAGUjrV3OoNbv2ywktK6ENFNnmGyztKSrvj5Jnc8N3A+WE202cO6EpAmedhwJ1kl4WF/n2XHYbVrU/9+im91l8Meux+fNNheqXQ3MqpjQmtzN6XBidJFIVnvc2hX8c8XPkiWgrnPt0MJnoXgAUc4r4i9Jy3cXRYbidK+tKvYPPqQnI5b90vttM2bbxIKB/4Z2SdlcZNZK6UcEz5TTbVFF6utYTz8vjjsjFp5JpdzVX4q/Gc9zNPe8Wpkdz8rkoAwQ2OpJKB0T+AYNAuM+0XNycpx4N4AkOozJqK1Gd6OMlicmgIWTqUgWqljLMxbI+1WrEcZTAbeUfe9UajEfYuxHE8ksRFAnMUNCFpOJN348vRoxBv3fvan6rAnQMs2F+P9anRb8jPkJAzOFDct3ej--hNhQcVOfe5QH/0IE--Tk/qRKxDaUSCeZMW5hBwaA==
zSnch4RlzPEpqZOE+TyBVBnHu+wyiNfNZoe1zi+cGNcgN/0BMw3xVQNIdu5ZgZzklukyimLlvkic7/G3PZE5aooqKIPeaGIgVKwdBvIGZYkTbrLTPtiA2Mx2iay4u6XI3eLDZZdy9g446GTngz9beQFR9s7dBbRBewOdIuCjA17seywsv722x97wHCv1xoGBeknK0DrEaYQcnECcy3G3esAsGeoKCQRMNHIf5/IbB6J+ZsoyOpnwtjOXdfEUnmtieLqvyw1xjgOxQTp2GMJk8zq+dGUBR3nIzplzccJbotppWfUbd9nxuT0stxWdFtKRPceynUfJ4awQNOmx+qGXi2/sYZH9lEB0nnyuKUpte6/bf1kcxcXyqTcsjaw0f9DXC3z9jx0pPbf2+O1MLZSFmo7Sr18mXjHVWjSCXnuYmWWkkuKESl4DwW5KScguWdnsf+/Y1eHds2MpE9dRLynlf5ONvFMY495iXuN4bR7UE5utAmU3imAg8vhErDqiro2L0GKMY5boMg+9NIObSlxwI6uAer8HaIKBVioiXGHFoFofR0LpmIKTbHiRF9ZZM3KCunsrQXAvRB4X7jL18JR08cDGGML2m03u9fcn9VuCYgcr+TLnsBozoG+ATP03kXc6lxkxjDvOc2z/AKqPmHUejCvsTLibFuEgw34E/NnVgcrC/Cggh/pp2Vmc6Yg2aQN7NcUmz0enGpzaTcnwiiB/8U8ig5WUovZP/iG3JH8S4KtBhDVz4hzz3FDOFBZCHd0rDrLtcyPtOcI3/WTZ0BgV5SXxA6L1gmM+aSYXtiyKZ2iUUOkMnNPvmHtimd+IWJZn2qXswX8RuE5QTqL5R5PRIdawdMSf9QrBSmDllNCo3czzClBRSNMYk2+ueWtiyvRuxP/xyNVl5e8nGOhAFWjCA+DVVA1xGWvS+R+3nxMSMtNaqTc2RPHQ3u4hY4k/0UH2TmnnzIJHS8vYScO4EzasTb/9FMeUEDnx369D7gvkXW6OapIKxwV0oQR8a/ZYUpOVyV6rSUIvEKrlpXCG8fh/6LwprYTPrnWe8iIwGqxKcuecrmtRSijAHgI2ZzMpSN91tomBBn9Qs+5svRbcXCO9KUKtjB9V82J7xqT+LwOyf1wa+v9zcO1aQxel2npRTsg7a4eIyIx1iyP7hHLAAuYIeEj1pBjiY/LiGZw+ScrXu1sIFTpt2SIKUOCVSN1y52/zXkwTRHc3qf4IdeHX6DqzWmWUiEHwm9Lva1fJ6poJ3vqWi7OLWJqyDtiN2FHJqybQ9mrgWPb0WBYi3umnfPy6ZYMQ6jQ8YarK8aFHsVdjUL0I4DKigAuer47p2rUcaFvySGCoMaSu5krSBw3VkDR6y1J8wIAm7nKZOSyoKwRjrgwXOl8yTN+zoemiaUMNJI1mTu1JQeg5rX2yUvwLEu5hu/5eOuRCkoSiwcG1+LmcnZbYRgSRvDzRcKYKZ/5hzQkPCLRkwAhh1Zp7mtc=--dyeF3fjnBpbK0tnw--nWdBu1crWT0VX9aHfnoKRw==

View File

@ -29,7 +29,8 @@ Rails.application.configure do
config.cache_store = :memory_store
# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :local
# config.active_storage.service = :local
config.active_storage.service = :amazon_dev
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false

View File

@ -22,7 +22,7 @@ Rails.application.configure do
# config.asset_host = "http://assets.example.com"
# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :local
config.active_storage.service = ENV["RAILS_BUILD"] ? :build : :amazon
# Assume all access to the app is happening through a SSL-terminating reverse proxy.
config.assume_ssl = true
@ -50,7 +50,8 @@ Rails.application.configure do
config.cache_store = :solid_cache_store
# Replace the default in-process and non-durable queuing backend for Active Job.
config.active_job.queue_adapter = :solid_queue
# config.active_job.queue_adapter = :solid_queue
config.active_job.queue_adapter = :sidekiq
config.solid_queue.connects_to = { database: { writing: :queue } }
# Ignore bad email addresses and do not raise email delivery errors.

View File

@ -174,7 +174,7 @@ ActiveAdmin.setup do |config|
# You can exclude possibly sensitive model attributes from being displayed,
# added to forms, or exported by default by ActiveAdmin
#
config.filter_attributes = [:encrypted_password, :password, :password_confirmation]
config.filter_attributes = [ :encrypted_password, :password, :password_confirmation ]
# == Localize Date/Time Format
#

View File

@ -0,0 +1,14 @@
class Ahoy::Store < Ahoy::DatabaseStore
end
# set to true for JavaScript tracking
Ahoy.api = true
# set to true for geocoding (and add the geocoder gem to your Gemfile)
# we recommend configuring local geocoding as well
# see https://github.com/ankane/ahoy#geocoding
Ahoy.geocode = false
Ahoy.visit_duration = 30.minutes
Ahoy.server_side_visits = :when_needed
RETENTION_PERIOD = 3.months

View File

@ -0,0 +1,19 @@
if Rails.env.production?
Aws.config.update({
region: ENV.fetch("AWS_REGION", "wnam"),
credentials: Aws::Credentials.new(
ENV.fetch("AWS_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :access_key_id)),
ENV.fetch("AWS_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :secret_access_key))
),
endpoint: ENV.fetch("AWS_ENDPOINT", Rails.application.credentials.dig(:aws, :endpoint))
})
else
Aws.config.update({
region: ENV.fetch("AWS_DEV_REGION", "wnam"),
credentials: Aws::Credentials.new(
ENV.fetch("AWS_DEV_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :access_key_id)),
ENV.fetch("AWS_DEV_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :secret_access_key))
),
endpoint: ENV.fetch("AWS_DEV_ENDPOINT", Rails.application.credentials.dig(:aws_dev, :endpoint))
})
end

View File

@ -24,7 +24,7 @@ Devise.setup do |config|
# Configure the e-mail address which will be shown in Devise::Mailer,
# note that it will be overwritten if you use your own mailer class
# with default "from" parameter.
config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
config.mailer_sender = "please-change-me-at-config-initializers-devise@example.com"
# Configure the class responsible to send e-mails.
# config.mailer = 'Devise::Mailer'
@ -36,7 +36,7 @@ Devise.setup do |config|
# Load and configure the ORM. Supports :active_record (default) and
# :mongoid (bson_ext recommended) by default. Other ORMs may be
# available as additional gems.
require 'devise/orm/active_record'
require "devise/orm/active_record"
# ==> Configuration for any authentication mechanism
# Configure which keys are used when authenticating a user. The default is
@ -58,12 +58,12 @@ Devise.setup do |config|
# Configure which authentication keys should be case-insensitive.
# These keys will be downcased upon creating or modifying a user and when used
# to authenticate or find a user. Default is :email.
config.case_insensitive_keys = [:email]
config.case_insensitive_keys = [ :email ]
# Configure which authentication keys should have whitespace stripped.
# These keys will have whitespace before and after removed upon creating or
# modifying a user and when used to authenticate or find a user. Default is :email.
config.strip_whitespace_keys = [:email]
config.strip_whitespace_keys = [ :email ]
# Tell if authentication through request.params is enabled. True by default.
# It can be set to an array that will enable params authentication only for the
@ -97,7 +97,7 @@ Devise.setup do |config|
# Notice that if you are skipping storage for all authentication paths, you
# may want to disable generating routes to Devise's sessions controller by
# passing skip: :sessions to `devise_for` in your config/routes.rb
config.skip_session_storage = [:http_auth]
config.skip_session_storage = [ :http_auth ]
# By default, Devise cleans up the CSRF token on authentication to
# avoid CSRF token fixation attacks. This means that, when using AJAX

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

@ -0,0 +1,9 @@
# config/initializers/schedule_tasks.rb
Rails.application.config.after_initialize do
begin
RefreshSitemapWorker.perform_async
Rails.logger.info "Startup task (RefreshSitemapWorker) scheduled successfully"
rescue => e
Rails.logger.error "Error scheduling startup task: #{e.message}"
end
end

View File

@ -0,0 +1,20 @@
require "sidekiq"
require "sidekiq-scheduler"
Sidekiq.configure_server do |config|
config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379/1") }
config.logger.level = Logger::INFO
config.on(:startup) do
schedule_file = "config/sidekiq_scheduler.yml"
if File.exist?(schedule_file)
Sidekiq::Scheduler.enabled = true
Sidekiq::Scheduler.dynamic = true
Sidekiq.schedule = YAML.load_file(schedule_file)
Sidekiq::Scheduler.reload_schedule!
end
end
end
Sidekiq.configure_client do |config|
config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379/1") }
end

View File

@ -0,0 +1,118 @@
en:
cities:
# Australia
sydney: 'Sydney'
melbourne: 'Melbourne'
# Bangladesh
dhaka: 'Dhaka'
# Brazil
rio_de_janeiro: 'Rio de Janeiro'
# Canada
toronto: 'Toronto'
# China
shanghai: 'Shanghai'
beijing: 'Beijing'
shenzhen: 'Shenzhen'
guangzhou: 'Guangzhou'
chengdu: 'Chengdu'
tianjin: 'Tianjin'
wuhan: 'Wuhan'
dongguan: 'Dongguan'
chongqing: 'Chongqing'
xian: "Xi'an"
hangzhou: 'Hangzhou'
foshan: 'Foshan'
nanjing: 'Nanjing'
hong_kong: 'Hong Kong'
shenyang: 'Shenyang'
zhengzhou: 'Zhengzhou'
qingdao: 'Qingdao'
suzhou: 'Suzhou'
changsha: 'Changsha'
jinan: 'Jinan'
kunming: 'Kunming'
harbin: 'Harbin'
shijiazhuang: 'Shijiazhuang'
hefei: 'Hefei'
dalian: 'Dalian'
xiamen: 'Xiamen'
nanning: 'Nanning'
changchun: 'Changchun'
taiyuan: 'Taiyuan'
new_taipei_city: 'New Taipei City'
guiyang: 'Guiyang'
wuxi: 'Wuxi'
shantou: 'Shantou'
urumqi: 'Ürümqi'
zhongshan: 'Zhongshan'
ningbo: 'Ningbo'
fuzhou: 'Fuzhou'
nanchang: 'Nanchang'
# Democratic Republic of the Congo
kinshasa: 'Kinshasa'
# Egypt
alexandria: 'Alexandria'
# France
paris: 'Paris'
# Germany
frankfurt: 'Frankfurt'
berlin: 'Berlin'
# India
mumbai: 'Mumbai'
bengaluru: 'Bengaluru'
# Japan
tokyo: 'Tokyo'
yokohama: 'Yokohama'
# Mexico
mexico_city: 'Mexico City'
# Nigeria
lagos: 'Lagos'
# Pakistan
lahore: 'Lahore'
# Russia
moscow: 'Moscow'
sankt_petersburg: 'Sankt Petersburg'
# Saudi Arabia
riyadh: 'Riyadh'
# Singapore
singapore: 'Singapore'
# South Korea
seoul: 'Seoul'
# Thailand
bangkok: 'Bangkok'
# Turkey
istanbul: 'İstanbul'
ankara: 'Ankara'
# United Kingdom
london: 'London'
# United States
san_francisco: 'San Francisco'
chicago: 'Chicago'
new_york_city: 'New York City'
los_angeles: 'Los Angeles'
# Vietnam
ho_chi_minh_city: 'Ho Chi Minh City'
hanoi: 'Hanoi'

View File

@ -0,0 +1,118 @@
zh-CN:
cities:
# Australia
sydney: '悉尼'
melbourne: '墨尔本'
# Bangladesh
dhaka: '达卡'
# Brazil
rio_de_janeiro: '里约热内卢'
# Canada
toronto: '多伦多'
# China
shanghai: '上海'
beijing: '北京'
shenzhen: '深圳'
guangzhou: '广州'
chengdu: '成都'
tianjin: '天津'
wuhan: '武汉'
dongguan: '东莞'
chongqing: '重庆'
xian: '西安'
hangzhou: '杭州'
foshan: '佛山'
nanjing: '南京'
hong_kong: '香港'
shenyang: '沈阳'
zhengzhou: '郑州'
qingdao: '青岛'
suzhou: '苏州'
changsha: '长沙'
jinan: '济南'
kunming: '昆明'
harbin: '哈尔滨'
shijiazhuang: '石家庄'
hefei: '合肥'
dalian: '大连'
xiamen: '厦门'
nanning: '南宁'
changchun: '长春'
taiyuan: '太原'
new_taipei_city: '新北市'
guiyang: '贵阳'
wuxi: '无锡'
shantou: '汕头'
urumqi: '乌鲁木齐'
zhongshan: '中山'
ningbo: '宁波'
fuzhou: '福州'
nanchang: '南昌'
# Democratic Republic of the Congo
kinshasa: '金沙萨'
# Egypt
alexandria: '亚历山大'
# France
paris: '巴黎'
# Germany
frankfurt: '法兰克福'
berlin: '柏林'
# India
mumbai: '孟买'
bengaluru: '班加罗尔'
# Japan
tokyo: '东京'
yokohama: '横滨'
# Mexico
mexico_city: '墨西哥城'
# Nigeria
lagos: '拉各斯'
# Pakistan
lahore: '拉合尔'
# Russia
moscow: '莫斯科'
sankt_petersburg: '圣彼得堡'
# Saudi Arabia
riyadh: '利雅得'
# Singapore
singapore: '新加坡'
# South Korea
seoul: '首尔'
# Thailand
bangkok: '曼谷'
# Turkey
istanbul: '伊斯坦布尔'
ankara: '安卡拉'
# United Kingdom
london: '伦敦'
# United States
san_francisco: '旧金山'
chicago: '芝加哥'
new_york_city: '纽约'
los_angeles: '洛杉矶'
# Vietnam
ho_chi_minh_city: '胡志明市'
hanoi: '河内'

View File

@ -0,0 +1,55 @@
en:
countries:
# East Asia
CN: 'China'
JP: 'Japan'
KR: 'South Korea'
TW: 'Taiwan'
HK: 'Hong Kong'
# South Asia
IN: 'India'
PK: 'Pakistan'
BD: 'Bangladesh'
# Southeast Asia
ID: 'Indonesia'
VN: 'Vietnam'
TH: 'Thailand'
MM: 'Myanmar'
SG: 'Singapore'
# Middle East
TR: 'Turkey'
IR: 'Iran'
SA: 'Saudi Arabia'
IQ: 'Iraq'
# Africa
NG: 'Nigeria'
EG: 'Egypt'
CD: 'Democratic Republic of the Congo'
TZ: 'Tanzania'
ZA: 'South Africa'
KE: 'Kenya'
AO: 'Angola'
ML: 'Mali'
CI: 'Ivory Coast'
# Europe
RU: 'Russia'
GB: 'United Kingdom'
DE: 'Germany'
# North America
US: 'United States'
MX: 'Mexico'
# South America
BR: 'Brazil'
PE: 'Peru'
CO: 'Colombia'
CL: 'Chile'
# Oceania
AU: 'Australia'

View File

@ -0,0 +1,55 @@
zh-CN:
countries:
# East Asia
CN: '中国'
JP: '日本'
KR: '韩国'
TW: '台湾'
HK: '香港'
# South Asia
IN: '印度'
PK: '巴基斯坦'
BD: '孟加拉国'
# Southeast Asia
ID: '印度尼西亚'
VN: '越南'
TH: '泰国'
MM: '缅甸'
SG: '新加坡'
# Middle East
TR: '土耳其'
IR: '伊朗'
SA: '沙特阿拉伯'
IQ: '伊拉克'
# Africa
NG: '尼日利亚'
EG: '埃及'
CD: '刚果民主共和国'
TZ: '坦桑尼亚'
ZA: '南非'
KE: '肯尼亚'
AO: '安哥拉'
ML: '马里'
CI: '科特迪瓦'
# Europe
RU: '俄罗斯'
GB: '英国'
DE: '德国'
# North America
US: '美国'
MX: '墨西哥'
# South America
BR: '巴西'
PE: '秘鲁'
CO: '哥伦比亚'
CL: '智利'
# Oceania
AU: '澳大利亚'

View File

@ -28,4 +28,4 @@
# enabled: "ON"
en:
hello: "Hello world"
hello: "Hello world"

View File

@ -0,0 +1,15 @@
en:
regions:
AS: 'Asia'
SA: 'South Asia'
SEA: 'Southeast Asia'
EA: 'East Asia'
ME: 'Middle East'
AF: 'Africa'
NA: 'North Africa'
SSA: 'Sub-Saharan Africa'
EU: 'Europe'
NAM: 'North America'
SAM: 'South America'
CAM: 'Central America'
OC: 'Oceania'

View File

@ -0,0 +1,15 @@
zh-CN:
regions:
AS: '亚洲'
SA: '南亚'
SEA: '东南亚'
EA: '东亚'
ME: '中东'
AF: '非洲'
NA: '北非'
SSA: '撒哈拉以南非洲'
EU: '欧洲'
NAM: '北美洲'
SAM: '南美洲'
CAM: '中美洲'
OC: '大洋洲'

View File

@ -1,9 +1,12 @@
require "sidekiq/web"
Rails.application.routes.draw do
root "home#index"
resources :cities, only: [ :index, :show ] do
resources :weather_arts, only: [ :show ]
resources :weather_arts, path: "weather", only: [ :show ], param: :slug
end
resources :arts, only: [ :index ]
# namespace :admin do
# resources :cities
@ -15,8 +18,15 @@ Rails.application.routes.draw do
get "cities/index"
get "cities/show"
get "home/index"
get "sitemaps/*path", to: "sitemaps#show", format: false
devise_for :admin_users, ActiveAdmin::Devise.config
ActiveAdmin.routes(self)
# mount Sidekiq::Web => '/sidekiq'
authenticate :admin_user do
mount Sidekiq::Web => "/admin/tasks"
end
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.

23
config/schedule.rb Normal file
View File

@ -0,0 +1,23 @@
# Use this file to easily define all of your cron jobs.
#
# It's helpful, but not entirely necessary to understand cron before proceeding.
# http://en.wikipedia.org/wiki/Cron
# Example:
#
# set :output, "/path/to/my/cron_log.log"
#
# every 2.hours do
# command "/usr/bin/some_great_command"
# runner "MyModel.some_method"
# rake "some:great:rake:task"
# end
#
# every 4.days do
# runner "AnotherModel.prune_old_records"
# end
# every 2.hour do
# runner "BatchGenerateWeatherArtsJob.perform_later"
# end
# Learn more: http://github.com/javan/whenever

View File

@ -0,0 +1,18 @@
batch_generate_weather:
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
clean_ahoy_data:
cron: '0 0 * * 0'
class: CleanAhoyDataWorker
queue: default
enabled: true

58
config/sitemap.rb Normal file
View File

@ -0,0 +1,58 @@
# Set the host name for URL creation
host = Rails.env.production? ? "https://todayaiweather.com" : "http://127.0.0.1:3000"
Rails.application.routes.default_url_options[:host] = host
SitemapGenerator::Sitemap.adapter = SitemapGenerator::AwsSdkAdapter.new(
Rails.application.credentials.dig(:aws, :bucket),
aws_access_key_id: Rails.application.credentials.dig(:aws, :access_key_id),
aws_secret_access_key: Rails.application.credentials.dig(:aws, :secret_access_key),
aws_region: Rails.application.credentials.dig(:aws, :region)
)
SitemapGenerator::Sitemap.sitemaps_path = "sitemaps/"
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

@ -6,6 +6,10 @@ local:
service: Disk
root: <%= Rails.root.join("storage") %>
build:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
# service: S3
@ -13,6 +17,21 @@ local:
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
# region: us-east-1
# bucket: your_own_bucket-<%= Rails.env %>
amazon_dev:
service: S3
access_key_id: <%= ENV.fetch("AWS_DEV_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :access_key_id)) %>
secret_access_key: <%= ENV.fetch("AWS_DEV_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws_dev, :secret_access_key)) %>
region: <%= ENV.fetch("AWS_DEV_REGION", "wnam") %>
bucket: <%= ENV.fetch("AWS_DEV_BUCKET", Rails.application.credentials.dig(:aws_dev, :bucket)) %>
endpoint: <%= ENV.fetch("AWS_DEV_ENDPOINT", Rails.application.credentials.dig(:aws_dev, :endpoint)) %>
amazon:
service: S3
access_key_id: <%= ENV.fetch("AWS_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :access_key_id)) %>
secret_access_key: <%= ENV.fetch("AWS_SECRET_ACCESS_KEY_ID", Rails.application.credentials.dig(:aws, :secret_access_key)) %>
region: <%= ENV.fetch("AWS_REGION", "wnam") %>
bucket: <%= ENV.fetch("AWS_BUCKET", Rails.application.credentials.dig(:aws, :bucket)) %>
endpoint: <%= ENV.fetch("AWS_ENDPOINT", Rails.application.credentials.dig(:aws, :endpoint)) %>
# Remember not to checkin your GCS keyfile to a repository
# google:

View File

@ -7,7 +7,7 @@ class CreateActiveAdminComments < ActiveRecord::Migration[8.0]
t.references :author, polymorphic: true
t.timestamps
end
add_index :active_admin_comments, [:namespace]
add_index :active_admin_comments, [ :namespace ]
end
def self.down

View File

@ -14,8 +14,8 @@ class CreateFriendlyIdSlugs < MIGRATION_CLASS
t.string :scope
t.datetime :created_at
end
add_index :friendly_id_slugs, [:sluggable_type, :sluggable_id]
add_index :friendly_id_slugs, [:slug, :sluggable_type], length: {slug: 140, sluggable_type: 50}
add_index :friendly_id_slugs, [:slug, :sluggable_type, :scope], length: {slug: 70, sluggable_type: 50, scope: 70}, unique: true
add_index :friendly_id_slugs, [ :sluggable_type, :sluggable_id ]
add_index :friendly_id_slugs, [ :slug, :sluggable_type ], length: { slug: 140, sluggable_type: 50 }
add_index :friendly_id_slugs, [ :slug, :sluggable_type, :scope ], length: { slug: 70, sluggable_type: 50, scope: 70 }, unique: true
end
end

View File

@ -0,0 +1,13 @@
class CreateRegions < ActiveRecord::Migration[8.0]
def change
create_table :regions do |t|
t.string :name
t.string :code
t.string :slug
t.timestamps
end
add_index :regions, :code, unique: true
add_index :regions, :slug, unique: true
end
end

View File

@ -0,0 +1,14 @@
class CreateCountries < ActiveRecord::Migration[8.0]
def change
create_table :countries do |t|
t.string :name
t.string :code
t.string :slug
t.references :region, null: false, foreign_key: true
t.timestamps
end
add_index :countries, :code, unique: true
add_index :countries, :slug, unique: true
end
end

Some files were not shown because too many files have changed in this diff Show More