mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -05:00
Merge branch 'master' into import_google_formats
This commit is contained in:
commit
52a032acb0
76 changed files with 2092 additions and 773 deletions
|
|
@ -1 +1 @@
|
|||
0.19.7
|
||||
0.21.1
|
||||
|
|
|
|||
41
.devcontainer/Dockerfile
Normal file
41
.devcontainer/Dockerfile
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Basis-Image für Ruby und Node.js
|
||||
FROM ruby:3.3.4-alpine
|
||||
|
||||
ENV APP_PATH=/var/app
|
||||
ENV BUNDLE_VERSION=2.5.9
|
||||
ENV BUNDLE_PATH=/usr/local/bundle/gems
|
||||
ENV TMP_PATH=/tmp/
|
||||
ENV RAILS_LOG_TO_STDOUT=true
|
||||
ENV RAILS_PORT=3000
|
||||
|
||||
# Install dependencies for application
|
||||
RUN apk -U add --no-cache \
|
||||
build-base \
|
||||
git \
|
||||
postgresql-dev \
|
||||
postgresql-client \
|
||||
libxml2-dev \
|
||||
libxslt-dev \
|
||||
nodejs \
|
||||
yarn \
|
||||
imagemagick \
|
||||
tzdata \
|
||||
less \
|
||||
yaml-dev \
|
||||
# gcompat for nokogiri on mac m1
|
||||
gcompat \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& mkdir -p $APP_PATH
|
||||
|
||||
RUN gem update --system 3.5.7 && gem install bundler --version "$BUNDLE_VERSION" \
|
||||
&& rm -rf $GEM_HOME/cache/*
|
||||
|
||||
# FIXME It would be a good idea to use a other user than root, but this lead to permission error on export and maybe more yet.
|
||||
# RUN adduser -D -h ${APP_PATH} vscode
|
||||
USER root
|
||||
|
||||
# Navigate to app directory
|
||||
WORKDIR $APP_PATH
|
||||
|
||||
EXPOSE $RAILS_PORT
|
||||
|
||||
17
.devcontainer/devcontainer.json
Normal file
17
.devcontainer/devcontainer.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "Ruby and Node DevContainer",
|
||||
"dockerComposeFile": ["docker-compose.yml"],
|
||||
"service": "dawarich_dev",
|
||||
"settings": {
|
||||
"terminal.integrated.defaultProfile.linux": "bash"
|
||||
},
|
||||
"extensions": [
|
||||
"rebornix.ruby", // Ruby-Support
|
||||
"esbenp.prettier-vscode", // Prettier for JS-Formating
|
||||
"dbaeumer.vscode-eslint" // ESLint for JavaScript
|
||||
],
|
||||
"postCreateCommand": "yarn install && bundle config set --local path 'vendor/bundle' && bundle install --jobs 20 --retry 5",
|
||||
"forwardPorts": [3000], // Redirect to Rails-App-Server
|
||||
"remoteUser": "root",
|
||||
"workspaceFolder": "/var/app"
|
||||
}
|
||||
79
.devcontainer/docker-compose.yml
Normal file
79
.devcontainer/docker-compose.yml
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
networks:
|
||||
dawarich:
|
||||
services:
|
||||
dawarich_dev:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: dawarich_dev
|
||||
volumes:
|
||||
- "${PWD}:/var/app:cached"
|
||||
- dawarich_gem_cache_app:/usr/local/bundle/gems_app
|
||||
- dawarich_public:/var/app/public
|
||||
- dawarich_watched:/var/app/tmp/imports/watched
|
||||
networks:
|
||||
- dawarich
|
||||
ports:
|
||||
- 3000:3000
|
||||
- 9394:9394
|
||||
stdin_open: true
|
||||
tty: true
|
||||
environment:
|
||||
RAILS_ENV: development
|
||||
REDIS_URL: redis://dawarich_redis:6379/0
|
||||
DATABASE_HOST: dawarich_db
|
||||
DATABASE_USERNAME: postgres
|
||||
DATABASE_PASSWORD: password
|
||||
DATABASE_NAME: dawarich_development
|
||||
MIN_MINUTES_SPENT_IN_CITY: 60
|
||||
APPLICATION_HOST: localhost
|
||||
APPLICATION_HOSTS: localhost
|
||||
TIME_ZONE: Europe/London
|
||||
APPLICATION_PROTOCOL: http
|
||||
DISTANCE_UNIT: km
|
||||
PHOTON_API_HOST: photon.komoot.io
|
||||
PHOTON_API_USE_HTTPS: true
|
||||
PROMETHEUS_EXPORTER_ENABLED: false
|
||||
PROMETHEUS_EXPORTER_HOST: 0.0.0.0
|
||||
PROMETHEUS_EXPORTER_PORT: 9394
|
||||
ENABLE_TELEMETRY: false # More on telemetry: https://dawarich.app/docs/tutorials/telemetry
|
||||
dawarich_redis:
|
||||
image: redis:7.0-alpine
|
||||
container_name: dawarich_redis
|
||||
command: redis-server
|
||||
networks:
|
||||
- dawarich
|
||||
volumes:
|
||||
- dawarich_shared:/data
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ]
|
||||
interval: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
timeout: 10s
|
||||
dawarich_db:
|
||||
image: postgres:14.2-alpine
|
||||
container_name: dawarich_db
|
||||
volumes:
|
||||
- dawarich_db_data:/var/lib/postgresql/data
|
||||
- dawarich_shared:/var/shared
|
||||
networks:
|
||||
- dawarich
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U postgres -d dawarich_development" ]
|
||||
interval: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
timeout: 10s
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: password
|
||||
volumes:
|
||||
dawarich_db_data:
|
||||
dawarich_gem_cache_app:
|
||||
dawarich_gem_cache_sidekiq:
|
||||
dawarich_shared:
|
||||
dawarich_public:
|
||||
dawarich_watched:
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -53,3 +53,13 @@
|
|||
.env
|
||||
|
||||
.byebug_history
|
||||
|
||||
|
||||
.devcontainer/.onCreateCommandMarker
|
||||
.devcontainer/.postCreateCommandMarker
|
||||
.devcontainer/.updateContentCommandMarker
|
||||
|
||||
.vscode-server/
|
||||
.ash_history
|
||||
.cache/
|
||||
.dotnet/
|
||||
|
|
|
|||
117
CHANGELOG.md
117
CHANGELOG.md
|
|
@ -5,6 +5,123 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
# 0.21.1 - 2024-12-24
|
||||
|
||||
### Added
|
||||
|
||||
- Cache cleaning and preheating upon application start.
|
||||
- `PHOTON_API_KEY` env var to set Photon API key. It's an optional env var, but it's required if you want to use Photon API as a Patreon supporter.
|
||||
- 'X-Dawarich-Response' header to the `GET /api/v1/health` endpoint. It's set to 'Hey, I\'m alive!' to make it easier to check if the API is working.
|
||||
|
||||
### Changed
|
||||
|
||||
- Custom config for PostgreSQL is now optional in `docker-compose.yml`.
|
||||
|
||||
# 0.21.0 - 2024-12-20
|
||||
|
||||
⚠️ This release introduces a breaking change. ⚠️
|
||||
|
||||
The `dawarich_db` service now uses a custom `postgresql.conf` file.
|
||||
|
||||
As @tabacha pointed out in #549, the default `shm_size` for the `dawarich_db` service is too small and it may lead to database performance issues. This release introduces a `shm_size` parameter to the `dawarich_db` service to increase the size of the shared memory for PostgreSQL. This should help database with peforming vacuum and other operations. Also, it introduces a custom `postgresql.conf` file to the `dawarich_db` service.
|
||||
|
||||
To mount a custom `postgresql.conf` file, you need to create a `postgresql.conf` file in the `dawarich_db` service directory and add the following line to it:
|
||||
|
||||
```diff
|
||||
dawarich_db:
|
||||
image: postgres:14.2-alpine
|
||||
shm_size: 1G
|
||||
container_name: dawarich_db
|
||||
volumes:
|
||||
- dawarich_db_data:/var/lib/postgresql/data
|
||||
- dawarich_shared:/var/shared
|
||||
+ - ./postgresql.conf:/etc/postgresql/postgres.conf # Provide path to custom config
|
||||
...
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U postgres -d dawarich_development" ]
|
||||
interval: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
timeout: 10s
|
||||
+ command: postgres -c config_file=/etc/postgresql/postgres.conf # Use custom config
|
||||
```
|
||||
|
||||
To ensure your database is using custom config, you can connect to the container (`docker exec -it dawarich_db psql -U postgres`) and run `SHOW config_file;` command. It should return the following path: `/etc/postgresql/postgresql.conf`.
|
||||
|
||||
An example of a custom `postgresql.conf` file is provided in the `postgresql.conf.example` file.
|
||||
|
||||
### Added
|
||||
|
||||
- A button on a year stats card to update stats for the whole year. #466
|
||||
- A button on a month stats card to update stats for a specific month. #466
|
||||
- A confirmation alert on the Notifications page before deleting all notifications.
|
||||
- A `shm_size` parameter to the `dawarich_db` service to increase the size of the shared memory for PostgreSQL. This should help database with peforming vacuum and other operations.
|
||||
|
||||
```diff
|
||||
...
|
||||
dawarich_db:
|
||||
image: postgres:14.2-alpine
|
||||
+ shm_size: 1G
|
||||
...
|
||||
```
|
||||
|
||||
- In addition to `api_key` parameter, `Authorization` header is now being used to authenticate API requests. #543
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
Authorization: Bearer YOUR_API_KEY
|
||||
```
|
||||
|
||||
### Changed
|
||||
|
||||
- The map borders were expanded to make it easier to scroll around the map for New Zealanders.
|
||||
- The `dawarich_db` service now uses a custom `postgresql.conf` file.
|
||||
- The popup over polylines now shows dates in the user's format, based on their browser settings.
|
||||
|
||||
# 0.20.2 - 2024-12-17
|
||||
|
||||
### Added
|
||||
|
||||
- A point id is now being shown in the point popup.
|
||||
|
||||
### Fixed
|
||||
|
||||
- North Macedonia is now being shown on the scratch map. #537
|
||||
|
||||
### Changed
|
||||
|
||||
- The app process is now bound to :: instead of 0.0.0.0 to provide compatibility with IPV6.
|
||||
- The app was updated to use Rails 8.0.1.
|
||||
|
||||
# 0.20.1 - 2024-12-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- Setting `reverse_geocoded_at` for points that don't have geodata is now being performed in background job, in batches of 10,000 points to prevent memory exhaustion and long-running data migration.
|
||||
|
||||
# 0.20.0 - 2024-12-16
|
||||
|
||||
### Added
|
||||
|
||||
- `GET /api/v1/points/tracked_months` endpoint added to get list of tracked years and months.
|
||||
- `GET /api/v1/countries/visited_cities` endpoint added to get list of visited cities.
|
||||
- A link to the docs leading to a help chart for k8s. #550
|
||||
- A button to delete all notifications. #548
|
||||
- A support for `RAILS_LOG_LEVEL` env var to change log level. More on that here: https://guides.rubyonrails.org/debugging_rails_applications.html#log-levels. The available log levels are: `:debug`, `:info`, `:warn`, `:error`, `:fatal`, and `:unknown`, corresponding to the log level numbers from 0 up to 5, respectively. The default log level is `:debug`. #540
|
||||
- A devcontainer to improve developers experience. #546
|
||||
|
||||
### Fixed
|
||||
|
||||
- A point popup is no longer closes when hovering over a polyline. #536
|
||||
- When polylines layer is disabled and user deletes a point from its popup, polylines layer is no longer being enabled right away. #552
|
||||
- Paths to gems within the sidekiq and app containers. #499
|
||||
|
||||
### Changed
|
||||
|
||||
- Months and years navigation is moved to a map panel on the right side of the map.
|
||||
- List of visited cities is now being shown in a map panel on the right side of the map.
|
||||
|
||||
# 0.19.7 - 2024-12-11
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
21
DEVELOPMENT.md
Normal file
21
DEVELOPMENT.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
If you want to develop with dawarich you can use the devcontainer, with your IDE. It is tested with visual studio code.
|
||||
|
||||
Load the directory in Vs-Code and press F1. And Run the command: `Dev Containers: Rebuild Containers` after a while you should see a terminal.
|
||||
|
||||
Now you can create/prepare the Database (this need to be done once):
|
||||
```bash
|
||||
bundle exec rails db:prepare
|
||||
```
|
||||
|
||||
Afterwards you can run sidekiq:
|
||||
```bash
|
||||
bundle exec sidekiq
|
||||
|
||||
```
|
||||
|
||||
And in a second terminal the dawarich-app:
|
||||
```bash
|
||||
bundle exec bin/dev
|
||||
```
|
||||
|
||||
You can connect with a web browser to http://127.0.0.l:3000/ and login with the default credentials.
|
||||
2
Gemfile
2
Gemfile
|
|
@ -21,7 +21,7 @@ gem 'pg'
|
|||
gem 'prometheus_exporter'
|
||||
gem 'puma'
|
||||
gem 'pundit'
|
||||
gem 'rails'
|
||||
gem 'rails', '~> 8.0'
|
||||
gem 'rswag-api'
|
||||
gem 'rswag-ui'
|
||||
gem 'shrine', '~> 3.6'
|
||||
|
|
|
|||
174
Gemfile.lock
174
Gemfile.lock
|
|
@ -10,66 +10,65 @@ GIT
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
actioncable (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activejob (= 7.2.2)
|
||||
activerecord (= 7.2.2)
|
||||
activestorage (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
actionmailbox (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activejob (= 8.0.1)
|
||||
activerecord (= 8.0.1)
|
||||
activestorage (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
actionview (= 7.2.2)
|
||||
activejob (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
actionmailer (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
actionview (= 8.0.1)
|
||||
activejob (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (7.2.2)
|
||||
actionview (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
actionpack (8.0.1)
|
||||
actionview (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4, < 3.2)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activerecord (= 7.2.2)
|
||||
activestorage (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
actiontext (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activerecord (= 8.0.1)
|
||||
activestorage (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
actionview (8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
activejob (8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
activerecord (7.2.2)
|
||||
activemodel (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
activemodel (8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
activerecord (8.0.1)
|
||||
activemodel (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activejob (= 7.2.2)
|
||||
activerecord (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
activestorage (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activejob (= 8.0.1)
|
||||
activerecord (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
marcel (~> 1.0)
|
||||
activesupport (7.2.2)
|
||||
activesupport (8.0.1)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
|
|
@ -81,6 +80,7 @@ GEM
|
|||
minitest (>= 5.1)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
ast (2.4.2)
|
||||
|
|
@ -105,12 +105,12 @@ GEM
|
|||
cronex (0.15.0)
|
||||
tzinfo
|
||||
unicode (>= 0.4.4.5)
|
||||
csv (3.3.0)
|
||||
csv (3.3.2)
|
||||
data_migrate (11.2.0)
|
||||
activerecord (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
date (3.4.0)
|
||||
debug (1.9.2)
|
||||
date (3.4.1)
|
||||
debug (1.10.0)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
devise (4.9.4)
|
||||
|
|
@ -121,14 +121,14 @@ GEM
|
|||
warden (~> 1.2.3)
|
||||
diff-lcs (1.5.1)
|
||||
docile (1.4.1)
|
||||
dotenv (3.1.4)
|
||||
dotenv-rails (3.1.4)
|
||||
dotenv (= 3.1.4)
|
||||
dotenv (3.1.7)
|
||||
dotenv-rails (3.1.7)
|
||||
dotenv (= 3.1.7)
|
||||
railties (>= 6.1)
|
||||
down (5.4.2)
|
||||
addressable (~> 2.8)
|
||||
drb (2.2.1)
|
||||
erubi (1.13.0)
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
factory_bot (6.5.0)
|
||||
|
|
@ -156,12 +156,12 @@ GEM
|
|||
multi_xml (>= 0.5.2)
|
||||
i18n (1.14.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
importmap-rails (2.0.3)
|
||||
importmap-rails (2.1.0)
|
||||
actionpack (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
io-console (0.7.2)
|
||||
irb (1.14.1)
|
||||
io-console (0.8.0)
|
||||
irb (1.14.3)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
json (2.7.4)
|
||||
|
|
@ -180,7 +180,7 @@ GEM
|
|||
kaminari-core (= 1.2.2)
|
||||
kaminari-core (1.2.2)
|
||||
language_server-protocol (3.17.0.3)
|
||||
logger (1.6.1)
|
||||
logger (1.6.4)
|
||||
lograge (0.14.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
|
|
@ -197,11 +197,11 @@ GEM
|
|||
marcel (1.0.4)
|
||||
method_source (1.1.0)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.25.2)
|
||||
minitest (5.25.4)
|
||||
msgpack (1.7.3)
|
||||
multi_xml (0.7.1)
|
||||
bigdecimal (~> 3.1)
|
||||
net-imap (0.5.0)
|
||||
net-imap (0.5.2)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
|
|
@ -211,24 +211,24 @@ GEM
|
|||
net-smtp (0.5.0)
|
||||
net-protocol
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.16.7-aarch64-linux)
|
||||
nokogiri (1.17.2-aarch64-linux)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-arm-linux)
|
||||
nokogiri (1.17.2-arm-linux)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-arm64-darwin)
|
||||
nokogiri (1.17.2-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-x86-linux)
|
||||
nokogiri (1.17.2-x86-linux)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-x86_64-darwin)
|
||||
nokogiri (1.17.2-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-x86_64-linux)
|
||||
nokogiri (1.17.2-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oj (3.16.7)
|
||||
oj (3.16.8)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
optimist (3.2.0)
|
||||
orm_adapter (0.5.0)
|
||||
ostruct (0.6.0)
|
||||
ostruct (0.6.1)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.5.0)
|
||||
ast (~> 2.4.1)
|
||||
|
|
@ -246,7 +246,8 @@ GEM
|
|||
pry (>= 0.13, < 0.15)
|
||||
pry-rails (0.3.11)
|
||||
pry (>= 0.13.0)
|
||||
psych (5.2.0)
|
||||
psych (5.2.2)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
puma (6.5.0)
|
||||
|
|
@ -262,30 +263,30 @@ GEM
|
|||
rack (>= 1.3)
|
||||
rackup (2.2.1)
|
||||
rack (>= 3)
|
||||
rails (7.2.2)
|
||||
actioncable (= 7.2.2)
|
||||
actionmailbox (= 7.2.2)
|
||||
actionmailer (= 7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
actiontext (= 7.2.2)
|
||||
actionview (= 7.2.2)
|
||||
activejob (= 7.2.2)
|
||||
activemodel (= 7.2.2)
|
||||
activerecord (= 7.2.2)
|
||||
activestorage (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
rails (8.0.1)
|
||||
actioncable (= 8.0.1)
|
||||
actionmailbox (= 8.0.1)
|
||||
actionmailer (= 8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
actiontext (= 8.0.1)
|
||||
actionview (= 8.0.1)
|
||||
activejob (= 8.0.1)
|
||||
activemodel (= 8.0.1)
|
||||
activerecord (= 8.0.1)
|
||||
activestorage (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.2.2)
|
||||
railties (= 8.0.1)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.0)
|
||||
rails-html-sanitizer (1.6.2)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
railties (7.2.2)
|
||||
actionpack (= 7.2.2)
|
||||
activesupport (= 7.2.2)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
railties (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
|
|
@ -293,14 +294,14 @@ GEM
|
|||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.2.1)
|
||||
rdoc (6.8.1)
|
||||
rdoc (6.10.0)
|
||||
psych (>= 4.0.0)
|
||||
redis (5.3.0)
|
||||
redis-client (>= 0.22.0)
|
||||
redis-client (0.22.2)
|
||||
redis-client (0.23.0)
|
||||
connection_pool
|
||||
regexp_parser (2.9.2)
|
||||
reline (0.5.11)
|
||||
reline (0.6.0)
|
||||
io-console (~> 0.5)
|
||||
request_store (1.7.0)
|
||||
rack (>= 1.4)
|
||||
|
|
@ -354,13 +355,13 @@ GEM
|
|||
rubocop (>= 1.52.0, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
securerandom (0.3.2)
|
||||
securerandom (0.4.1)
|
||||
shoulda-matchers (6.4.0)
|
||||
activesupport (>= 5.2.0)
|
||||
shrine (3.6.0)
|
||||
content_disposition (~> 1.0)
|
||||
down (~> 5.1)
|
||||
sidekiq (7.3.6)
|
||||
sidekiq (7.3.7)
|
||||
connection_pool (>= 2.3.0)
|
||||
logger
|
||||
rack (>= 2.2.4)
|
||||
|
|
@ -410,7 +411,8 @@ GEM
|
|||
concurrent-ruby (~> 1.0)
|
||||
unicode (0.4.4.5)
|
||||
unicode-display_width (2.6.0)
|
||||
useragent (0.16.10)
|
||||
uri (1.0.2)
|
||||
useragent (0.16.11)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
webmock (3.24.0)
|
||||
|
|
@ -456,7 +458,7 @@ DEPENDENCIES
|
|||
pry-rails
|
||||
puma
|
||||
pundit
|
||||
rails
|
||||
rails (~> 8.0)
|
||||
redis
|
||||
rspec-rails
|
||||
rswag-api
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
web: bin/rails server -p 3000 -b 0.0.0.0
|
||||
web: bin/rails server -p 3000 -b ::
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
prometheus_exporter: bundle exec prometheus_exporter -b 0.0.0.0
|
||||
web: bin/rails server -p 3000 -b 0.0.0.0
|
||||
prometheus_exporter: bundle exec prometheus_exporter -b ANY
|
||||
web: bin/rails server -p 3000 -b ::
|
||||
|
|
@ -33,6 +33,7 @@ Donate using crypto: [0x6bAd13667692632f1bF926cA9B421bEe7EaEB8D4](https://ethers
|
|||
|
||||
## ⚠️ Disclaimer
|
||||
|
||||
- 💔 **DO NOT UPDATE AUTOMATICALLY**: Read release notes before updating. Automatic updates may break your setup.
|
||||
- 🛠️ **Under active development**: Expect frequent updates, bugs, and breaking changes.
|
||||
- ❌ **Do not delete your original data** after importing into Dawarich.
|
||||
- 📦 **Backup before updates**: Always [backup your data](https://dawarich.app/docs/tutorials/backup-and-restore) before upgrading.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Countries::VisitedCitiesController < ApiController
|
||||
before_action :validate_params
|
||||
|
||||
def index
|
||||
start_at = DateTime.parse(params[:start_at]).to_i
|
||||
end_at = DateTime.parse(params[:end_at]).to_i
|
||||
|
||||
points = current_api_user
|
||||
.tracked_points
|
||||
.where(timestamp: start_at..end_at)
|
||||
|
||||
render json: { data: CountriesAndCities.new(points).call }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def required_params
|
||||
%i[start_at end_at]
|
||||
end
|
||||
end
|
||||
|
|
@ -4,6 +4,8 @@ class Api::V1::HealthController < ApiController
|
|||
skip_before_action :authenticate_api_key
|
||||
|
||||
def index
|
||||
response.set_header('X-Dawarich-Response', 'Hey, I\'m alive!')
|
||||
render json: { status: 'ok' }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Points::TrackedMonthsController < ApiController
|
||||
def index
|
||||
render json: current_api_user.years_tracked
|
||||
end
|
||||
end
|
||||
|
|
@ -13,6 +13,26 @@ class ApiController < ApplicationController
|
|||
end
|
||||
|
||||
def current_api_user
|
||||
@current_api_user ||= User.find_by(api_key: params[:api_key])
|
||||
@current_api_user ||= User.find_by(api_key:)
|
||||
end
|
||||
|
||||
def api_key
|
||||
params[:api_key] || request.headers['Authorization']&.split(' ')&.last
|
||||
end
|
||||
|
||||
def validate_params
|
||||
missing_params = required_params.select { |param| params[param].blank? }
|
||||
|
||||
if missing_params.any?
|
||||
render json: {
|
||||
error: "Missing required parameters: #{missing_params.join(', ')}"
|
||||
}, status: :bad_request and return
|
||||
end
|
||||
|
||||
params.permit(*required_params)
|
||||
end
|
||||
|
||||
def required_params
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,10 +15,15 @@ class NotificationsController < ApplicationController
|
|||
|
||||
def mark_as_read
|
||||
current_user.notifications.unread.update_all(read_at: Time.zone.now)
|
||||
|
||||
redirect_to notifications_url, notice: 'All notifications marked as read.', status: :see_other
|
||||
end
|
||||
|
||||
|
||||
def destroy_all
|
||||
current_user.notifications.destroy_all
|
||||
redirect_to notifications_url, notice: 'All notifications where successfully destroyed.', status: :see_other
|
||||
end
|
||||
|
||||
def destroy
|
||||
@notification.destroy!
|
||||
redirect_to notifications_url, notice: 'Notification was successfully destroyed.', status: :see_other
|
||||
|
|
|
|||
|
|
@ -16,9 +16,27 @@ class StatsController < ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
current_user.years_tracked.each do |year|
|
||||
if params[:month] == 'all'
|
||||
(1..12).each do |month|
|
||||
Stats::CalculatingJob.perform_later(current_user.id, year, month)
|
||||
Stats::CalculatingJob.perform_later(current_user.id, params[:year], month)
|
||||
end
|
||||
|
||||
target = "the whole #{params[:year]}"
|
||||
else
|
||||
Stats::CalculatingJob.perform_later(current_user.id, params[:year], params[:month])
|
||||
|
||||
target = "#{Date::MONTHNAMES[params[:month].to_i]} of #{params[:year]}"
|
||||
end
|
||||
|
||||
redirect_to stats_path, notice: "Stats for #{target} are being updated", status: :see_other
|
||||
end
|
||||
|
||||
def update_all
|
||||
current_user.years_tracked.each do |year|
|
||||
year[:months].each do |month|
|
||||
Stats::CalculatingJob.perform_later(
|
||||
current_user.id, year[:year], Date::ABBR_MONTHNAMES.index(month)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ export default class extends Controller {
|
|||
|
||||
settingsButtonAdded = false;
|
||||
layerControl = null;
|
||||
visitedCitiesCache = new Map();
|
||||
trackedMonthsCache = null;
|
||||
|
||||
connect() {
|
||||
console.log("Map controller connected");
|
||||
|
|
@ -52,8 +54,8 @@ export default class extends Controller {
|
|||
this.map = L.map(this.containerTarget).setView([this.center[0], this.center[1]], 14);
|
||||
|
||||
// Set the maximum bounds to prevent infinite scroll
|
||||
var southWest = L.latLng(-90, -180);
|
||||
var northEast = L.latLng(90, 180);
|
||||
var southWest = L.latLng(-120, -210);
|
||||
var northEast = L.latLng(120, 210);
|
||||
var bounds = L.latLngBounds(southWest, northEast);
|
||||
|
||||
this.map.setMaxBounds(bounds);
|
||||
|
|
@ -171,12 +173,37 @@ export default class extends Controller {
|
|||
if (this.liveMapEnabled) {
|
||||
this.setupSubscription();
|
||||
}
|
||||
|
||||
// Add the toggle panel button
|
||||
this.addTogglePanelButton();
|
||||
|
||||
// Check if we should open the panel based on localStorage or URL params
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const isPanelOpen = localStorage.getItem('mapPanelOpen') === 'true';
|
||||
const hasDateParams = urlParams.has('start_at') && urlParams.has('end_at');
|
||||
|
||||
// Always create the panel first
|
||||
this.toggleRightPanel();
|
||||
|
||||
// Then hide it if it shouldn't be open
|
||||
if (!isPanelOpen && !hasDateParams) {
|
||||
const panel = document.querySelector('.leaflet-right-panel');
|
||||
if (panel) {
|
||||
panel.style.display = 'none';
|
||||
localStorage.setItem('mapPanelOpen', 'false');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.handleDeleteClick) {
|
||||
document.removeEventListener('click', this.handleDeleteClick);
|
||||
}
|
||||
// Store panel state before disconnecting
|
||||
if (this.rightPanel) {
|
||||
const finalState = document.querySelector('.leaflet-right-panel').style.display !== 'none' ? 'true' : 'false';
|
||||
localStorage.setItem('mapPanelOpen', finalState);
|
||||
}
|
||||
this.map.remove();
|
||||
}
|
||||
|
||||
|
|
@ -382,10 +409,14 @@ export default class extends Controller {
|
|||
.then(data => {
|
||||
// Remove the marker and update all layers
|
||||
this.removeMarker(id);
|
||||
|
||||
let wasPolyLayerVisible = false;
|
||||
// Explicitly remove old polylines layer from map
|
||||
if (this.polylinesLayer) {
|
||||
if (this.map.hasLayer(this.polylinesLayer)) {
|
||||
wasPolyLayerVisible = true;
|
||||
}
|
||||
this.map.removeLayer(this.polylinesLayer);
|
||||
|
||||
}
|
||||
|
||||
// Create new polylines layer
|
||||
|
|
@ -397,10 +428,12 @@ export default class extends Controller {
|
|||
this.userSettings,
|
||||
this.distanceUnit
|
||||
);
|
||||
|
||||
// Add new polylines layer to map and to layer control
|
||||
this.polylinesLayer.addTo(this.map);
|
||||
|
||||
if (wasPolyLayerVisible) {
|
||||
// Add new polylines layer to map and to layer control
|
||||
this.polylinesLayer.addTo(this.map);
|
||||
} else {
|
||||
this.map.removeLayer(this.polylinesLayer);
|
||||
}
|
||||
// Update the layer control
|
||||
if (this.layerControl) {
|
||||
this.map.removeControl(this.layerControl);
|
||||
|
|
@ -898,8 +931,385 @@ export default class extends Controller {
|
|||
${photo.type === 'video' ? '🎥 Video' : '📷 Photo'}
|
||||
</div>
|
||||
`;
|
||||
marker.bindPopup(popupContent);
|
||||
marker.bindPopup(popupContent, { autoClose: false });
|
||||
|
||||
this.photoMarkers.addLayer(marker);
|
||||
}
|
||||
|
||||
addTogglePanelButton() {
|
||||
const TogglePanelControl = L.Control.extend({
|
||||
onAdd: (map) => {
|
||||
const button = L.DomUtil.create('button', 'toggle-panel-button');
|
||||
button.innerHTML = '📅';
|
||||
|
||||
button.style.backgroundColor = 'white';
|
||||
button.style.width = '48px';
|
||||
button.style.height = '48px';
|
||||
button.style.border = 'none';
|
||||
button.style.cursor = 'pointer';
|
||||
button.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)';
|
||||
|
||||
// Disable map interactions when clicking the button
|
||||
L.DomEvent.disableClickPropagation(button);
|
||||
|
||||
// Toggle panel on button click
|
||||
L.DomEvent.on(button, 'click', () => {
|
||||
this.toggleRightPanel();
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
});
|
||||
|
||||
// Add the control to the map
|
||||
this.map.addControl(new TogglePanelControl({ position: 'topright' }));
|
||||
}
|
||||
|
||||
toggleRightPanel() {
|
||||
if (this.rightPanel) {
|
||||
const panel = document.querySelector('.leaflet-right-panel');
|
||||
if (panel) {
|
||||
if (panel.style.display === 'none') {
|
||||
panel.style.display = 'block';
|
||||
localStorage.setItem('mapPanelOpen', 'true');
|
||||
} else {
|
||||
panel.style.display = 'none';
|
||||
localStorage.setItem('mapPanelOpen', 'false');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.rightPanel = L.control({ position: 'topright' });
|
||||
|
||||
this.rightPanel.onAdd = () => {
|
||||
const div = L.DomUtil.create('div', 'leaflet-right-panel');
|
||||
const allMonths = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
// Get current date from URL query parameters
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const startDate = urlParams.get('start_at');
|
||||
const currentYear = startDate
|
||||
? new Date(startDate).getFullYear().toString()
|
||||
: new Date().getFullYear().toString();
|
||||
const currentMonth = startDate
|
||||
? allMonths[new Date(startDate).getMonth()]
|
||||
: allMonths[new Date().getMonth()];
|
||||
|
||||
// Initially create select with loading state and current year if available
|
||||
div.innerHTML = `
|
||||
<div class="panel-content">
|
||||
<div id='years-nav'>
|
||||
<div class="flex gap-2 mb-4">
|
||||
<select id="year-select" class="select select-bordered w-1/2 max-w-xs">
|
||||
${currentYear
|
||||
? `<option value="${currentYear}" selected>${currentYear}</option>`
|
||||
: '<option disabled selected>Loading years...</option>'}
|
||||
</select>
|
||||
<a href="${this.getWholeYearLink()}"
|
||||
id="whole-year-link"
|
||||
class="btn btn-default"
|
||||
style="color: rgb(116 128 255) !important;">
|
||||
Whole year
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class='grid grid-cols-3 gap-3' id="months-grid">
|
||||
${allMonths.map(month => `
|
||||
<a href="#"
|
||||
class="btn btn-primary disabled ${month === currentMonth ? 'btn-active' : ''}"
|
||||
data-month-name="${month}"
|
||||
style="pointer-events: none; opacity: 0.6; color: rgb(116 128 255) !important;">
|
||||
<span class="loading loading-dots loading-md"></span>
|
||||
</a>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.fetchAndDisplayTrackedMonths(div, currentYear, currentMonth, allMonths);
|
||||
|
||||
div.style.backgroundColor = 'white';
|
||||
div.style.padding = '10px';
|
||||
div.style.border = '1px solid #ccc';
|
||||
div.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)';
|
||||
div.style.marginRight = '10px';
|
||||
div.style.marginTop = '10px';
|
||||
div.style.width = '300px';
|
||||
div.style.maxHeight = '80vh';
|
||||
div.style.overflowY = 'auto';
|
||||
|
||||
L.DomEvent.disableClickPropagation(div);
|
||||
|
||||
// Add container for visited cities
|
||||
div.innerHTML += `
|
||||
<div id="visited-cities-container" class="mt-4">
|
||||
<h3 class="text-lg font-bold mb-2">Visited cities</h3>
|
||||
<div id="visited-cities-list" class="space-y-2"
|
||||
style="max-height: 300px; overflow-y: auto; overflow-x: auto; padding-right: 10px;">
|
||||
<p class="text-gray-500">Loading visited places...</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Prevent map zoom when scrolling the cities list
|
||||
const citiesList = div.querySelector('#visited-cities-list');
|
||||
L.DomEvent.disableScrollPropagation(citiesList);
|
||||
|
||||
// Fetch visited cities when panel is first created
|
||||
this.fetchAndDisplayVisitedCities();
|
||||
|
||||
// Set initial display style based on localStorage
|
||||
const isPanelOpen = localStorage.getItem('mapPanelOpen') === 'true';
|
||||
div.style.display = isPanelOpen ? 'block' : 'none';
|
||||
|
||||
return div;
|
||||
};
|
||||
|
||||
this.map.addControl(this.rightPanel);
|
||||
}
|
||||
|
||||
async fetchAndDisplayTrackedMonths(div, currentYear, currentMonth, allMonths) {
|
||||
try {
|
||||
let yearsData;
|
||||
|
||||
// Check cache first
|
||||
if (this.trackedMonthsCache) {
|
||||
yearsData = this.trackedMonthsCache;
|
||||
} else {
|
||||
const response = await fetch(`/api/v1/points/tracked_months?api_key=${this.apiKey}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
yearsData = await response.json();
|
||||
// Store in cache
|
||||
this.trackedMonthsCache = yearsData;
|
||||
}
|
||||
|
||||
const yearSelect = document.getElementById('year-select');
|
||||
|
||||
if (!Array.isArray(yearsData) || yearsData.length === 0) {
|
||||
yearSelect.innerHTML = '<option disabled selected>No data available</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the current year exists in the API response
|
||||
const currentYearData = yearsData.find(yearData => yearData.year.toString() === currentYear);
|
||||
|
||||
const options = yearsData
|
||||
.filter(yearData => yearData && yearData.year)
|
||||
.map(yearData => {
|
||||
const months = Array.isArray(yearData.months) ? yearData.months : [];
|
||||
const isCurrentYear = yearData.year.toString() === currentYear;
|
||||
return `
|
||||
<option value="${yearData.year}"
|
||||
data-months='${JSON.stringify(months)}'
|
||||
${isCurrentYear ? 'selected' : ''}>
|
||||
${yearData.year}
|
||||
</option>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
yearSelect.innerHTML = `
|
||||
<option disabled>Select year</option>
|
||||
${options}
|
||||
`;
|
||||
|
||||
const updateMonthLinks = (selectedYear, availableMonths) => {
|
||||
// Get current date from URL parameters
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const startDate = urlParams.get('start_at') ? new Date(urlParams.get('start_at')) : new Date();
|
||||
const endDate = urlParams.get('end_at') ? new Date(urlParams.get('end_at')) : new Date();
|
||||
|
||||
allMonths.forEach((month, index) => {
|
||||
const monthLink = div.querySelector(`a[data-month-name="${month}"]`);
|
||||
if (!monthLink) return;
|
||||
|
||||
// Update the content to show the month name instead of loading dots
|
||||
monthLink.innerHTML = month;
|
||||
|
||||
// Check if this month falls within the selected date range
|
||||
const isSelected = startDate && endDate &&
|
||||
selectedYear === startDate.getFullYear().toString() && // Only check months for the currently selected year
|
||||
isMonthInRange(index, startDate, endDate, parseInt(selectedYear));
|
||||
|
||||
if (availableMonths.includes(month)) {
|
||||
monthLink.classList.remove('disabled');
|
||||
monthLink.style.pointerEvents = 'auto';
|
||||
monthLink.style.opacity = '1';
|
||||
|
||||
// Update the active state based on selection
|
||||
if (isSelected) {
|
||||
monthLink.classList.add('btn-active', 'btn-primary');
|
||||
} else {
|
||||
monthLink.classList.remove('btn-active', 'btn-primary');
|
||||
}
|
||||
|
||||
const monthNum = (index + 1).toString().padStart(2, '0');
|
||||
const startDate = `${selectedYear}-${monthNum}-01T00:00`;
|
||||
const lastDay = new Date(selectedYear, index + 1, 0).getDate();
|
||||
const endDate = `${selectedYear}-${monthNum}-${lastDay}T23:59`;
|
||||
|
||||
const href = `map?end_at=${encodeURIComponent(endDate)}&start_at=${encodeURIComponent(startDate)}`;
|
||||
monthLink.setAttribute('href', href);
|
||||
} else {
|
||||
monthLink.classList.add('disabled');
|
||||
monthLink.classList.remove('btn-active', 'btn-primary');
|
||||
monthLink.style.pointerEvents = 'none';
|
||||
monthLink.style.opacity = '0.6';
|
||||
monthLink.setAttribute('href', '#');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to check if a month falls within a date range
|
||||
const isMonthInRange = (monthIndex, startDate, endDate, selectedYear) => {
|
||||
// Create date objects for the first and last day of the month in the selected year
|
||||
const monthStart = new Date(selectedYear, monthIndex, 1);
|
||||
const monthEnd = new Date(selectedYear, monthIndex + 1, 0);
|
||||
|
||||
// Check if any part of the month overlaps with the selected date range
|
||||
return monthStart <= endDate && monthEnd >= startDate;
|
||||
};
|
||||
|
||||
yearSelect.addEventListener('change', (event) => {
|
||||
const selectedOption = event.target.selectedOptions[0];
|
||||
const selectedYear = selectedOption.value;
|
||||
const availableMonths = JSON.parse(selectedOption.dataset.months || '[]');
|
||||
|
||||
// Update whole year link with selected year
|
||||
const wholeYearLink = document.getElementById('whole-year-link');
|
||||
const startDate = `${selectedYear}-01-01T00:00`;
|
||||
const endDate = `${selectedYear}-12-31T23:59`;
|
||||
const href = `map?end_at=${encodeURIComponent(endDate)}&start_at=${encodeURIComponent(startDate)}`;
|
||||
wholeYearLink.setAttribute('href', href);
|
||||
|
||||
updateMonthLinks(selectedYear, availableMonths);
|
||||
});
|
||||
|
||||
// If we have a current year, set it and update month links
|
||||
if (currentYear && currentYearData) {
|
||||
yearSelect.value = currentYear;
|
||||
updateMonthLinks(currentYear, currentYearData.months);
|
||||
}
|
||||
} catch (error) {
|
||||
const yearSelect = document.getElementById('year-select');
|
||||
yearSelect.innerHTML = '<option disabled selected>Error loading years</option>';
|
||||
console.error('Error fetching tracked months:', error);
|
||||
}
|
||||
}
|
||||
|
||||
chunk(array, size) {
|
||||
const chunked = [];
|
||||
for (let i = 0; i < array.length; i += size) {
|
||||
chunked.push(array.slice(i, i + size));
|
||||
}
|
||||
return chunked;
|
||||
}
|
||||
|
||||
getWholeYearLink() {
|
||||
// First try to get year from URL parameters
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
let year;
|
||||
|
||||
if (urlParams.has('start_at')) {
|
||||
year = new Date(urlParams.get('start_at')).getFullYear();
|
||||
} else {
|
||||
// If no URL params, try to get year from start_at input
|
||||
const startAtInput = document.querySelector('input#start_at');
|
||||
if (startAtInput && startAtInput.value) {
|
||||
year = new Date(startAtInput.value).getFullYear();
|
||||
} else {
|
||||
// If no input value, use current year
|
||||
year = new Date().getFullYear();
|
||||
}
|
||||
}
|
||||
|
||||
const startDate = `${year}-01-01T00:00`;
|
||||
const endDate = `${year}-12-31T23:59`;
|
||||
return `map?end_at=${encodeURIComponent(endDate)}&start_at=${encodeURIComponent(startDate)}`;
|
||||
}
|
||||
|
||||
async fetchAndDisplayVisitedCities() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const startAt = urlParams.get('start_at') || new Date().toISOString();
|
||||
const endAt = urlParams.get('end_at') || new Date().toISOString();
|
||||
|
||||
// Create a cache key from the date range
|
||||
const cacheKey = `${startAt}-${endAt}`;
|
||||
|
||||
// Check if we have cached data for this date range
|
||||
if (this.visitedCitiesCache.has(cacheKey)) {
|
||||
this.displayVisitedCities(this.visitedCitiesCache.get(cacheKey));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/countries/visited_cities?api_key=${this.apiKey}&start_at=${startAt}&end_at=${endAt}`, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Cache the results
|
||||
this.visitedCitiesCache.set(cacheKey, data.data);
|
||||
|
||||
this.displayVisitedCities(data.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching visited cities:', error);
|
||||
const container = document.getElementById('visited-cities-list');
|
||||
if (container) {
|
||||
container.innerHTML = '<p class="text-red-500">Error loading visited places</p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
displayVisitedCities(citiesData) {
|
||||
const container = document.getElementById('visited-cities-list');
|
||||
if (!container) return;
|
||||
|
||||
if (!citiesData || citiesData.length === 0) {
|
||||
container.innerHTML = '<p class="text-gray-500">No places visited during this period</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const html = citiesData.map(country => `
|
||||
<div class="mb-4" style="min-width: min-content;">
|
||||
<h4 class="font-bold text-md">${country.country}</h4>
|
||||
<ul class="ml-4 space-y-1">
|
||||
${country.cities.map(city => `
|
||||
<li class="text-sm whitespace-nowrap">
|
||||
${city.city}
|
||||
<span class="text-gray-500">
|
||||
(${new Date(city.timestamp * 1000).toLocaleDateString()})
|
||||
</span>
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
formatDuration(seconds) {
|
||||
const days = Math.floor(seconds / (24 * 60 * 60));
|
||||
const hours = Math.floor((seconds % (24 * 60 * 60)) / (60 * 60));
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours}h`;
|
||||
}
|
||||
return `${hours}h`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ export function countryCodesMap() {
|
|||
"Niue": "NU",
|
||||
"Norfolk Island": "NF",
|
||||
"Northern Mariana Islands": "MP",
|
||||
"North Macedonia": "MK",
|
||||
"Norway": "NO",
|
||||
"Oman": "OM",
|
||||
"Pakistan": "PK",
|
||||
|
|
|
|||
|
|
@ -55,7 +55,15 @@ export function minutesToDaysHoursMinutes(minutes) {
|
|||
|
||||
export function formatDate(timestamp, timezone) {
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleString("en-GB", { timeZone: timezone });
|
||||
let locale;
|
||||
if (navigator.languages !== undefined) {
|
||||
locale = navigator.languages[0];
|
||||
} else if (navigator.language) {
|
||||
locale = navigator.language;
|
||||
} else {
|
||||
locale = 'en-GB';
|
||||
}
|
||||
return date.toLocaleString(locale, { timeZone: timezone });
|
||||
}
|
||||
|
||||
export function haversineDistance(lat1, lon1, lat2, lon2, unit = 'km') {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export function createMarkersArray(markersData, userSettings) {
|
|||
|
||||
const popupContent = createPopupContent(marker, userSettings.timezone, userSettings.distanceUnit);
|
||||
let markerColor = marker[5] < 0 ? "orange" : "blue";
|
||||
return L.circleMarker([lat, lon], { radius: 4, color: markerColor }).bindPopup(popupContent);
|
||||
return L.circleMarker([lat, lon], { radius: 4, color: markerColor }).bindPopup(popupContent, { autoClose: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { formatDate } from "../maps/helpers";
|
||||
import { formatDistance } from "../maps/helpers";
|
||||
import { getUrlParameter } from "../maps/helpers";
|
||||
import { minutesToDaysHoursMinutes } from "../maps/helpers";
|
||||
|
|
@ -12,8 +13,8 @@ export function addHighlightOnHover(polyline, map, polylineCoordinates, userSett
|
|||
const startPoint = polylineCoordinates[0];
|
||||
const endPoint = polylineCoordinates[polylineCoordinates.length - 1];
|
||||
|
||||
const firstTimestamp = new Date(startPoint[4] * 1000).toLocaleString("en-GB", { timeZone: userSettings.timezone });
|
||||
const lastTimestamp = new Date(endPoint[4] * 1000).toLocaleString("en-GB", { timeZone: userSettings.timezone });
|
||||
const firstTimestamp = formatDate(startPoint[4], userSettings.timezone);
|
||||
const lastTimestamp = formatDate(endPoint[4], userSettings.timezone);
|
||||
|
||||
const minutes = Math.round((endPoint[4] - startPoint[4]) / 60);
|
||||
const timeOnRoute = minutesToDaysHoursMinutes(minutes);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export function createPopupContent(marker, timezone, distanceUnit) {
|
|||
<strong>Altitude:</strong> ${marker[3]}m<br>
|
||||
<strong>Velocity:</strong> ${marker[5]}km/h<br>
|
||||
<strong>Battery:</strong> ${marker[2]}%<br>
|
||||
<strong>Id:</strong> ${marker[6]}<br>
|
||||
<a href="#" data-id="${marker[6]}" class="delete-point">[Delete]</a>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
9
app/jobs/cache/cleaning_job.rb
vendored
Normal file
9
app/jobs/cache/cleaning_job.rb
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Cache::CleaningJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform
|
||||
Cache::Clean.call
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DataMigrations::SetReverseGeocodedAtForPointsJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform
|
||||
timestamp = Time.current
|
||||
|
||||
Point.where.not(geodata: {})
|
||||
.where(reverse_geocoded_at: nil)
|
||||
.in_batches(of: 10_000) do |relation|
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
relation.update_all(reverse_geocoded_at: timestamp)
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -6,18 +6,21 @@ class Stats::CalculatingJob < ApplicationJob
|
|||
def perform(user_id, year, month)
|
||||
Stats::CalculateMonth.new(user_id, year, month).call
|
||||
|
||||
create_stats_updated_notification(user_id)
|
||||
create_stats_updated_notification(user_id, year, month)
|
||||
rescue StandardError => e
|
||||
create_stats_update_failed_notification(user_id, e)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_stats_updated_notification(user_id)
|
||||
def create_stats_updated_notification(user_id, year, month)
|
||||
user = User.find(user_id)
|
||||
|
||||
Notifications::Create.new(
|
||||
user:, kind: :info, title: 'Stats updated', content: 'Stats updated'
|
||||
user:,
|
||||
kind: :info,
|
||||
title: "Stats updated for #{Date::MONTHNAMES[month.to_i]} of #{year}",
|
||||
content: "Stats updated for #{Date::MONTHNAMES[month.to_i]} of #{year}"
|
||||
).call
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -21,20 +21,6 @@ class Stat < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def self.year_cities_and_countries(year, user)
|
||||
start_at = DateTime.new(year).beginning_of_year
|
||||
end_at = DateTime.new(year).end_of_year
|
||||
|
||||
points = user.tracked_points.without_raw_data.where(timestamp: start_at..end_at)
|
||||
|
||||
data = CountriesAndCities.new(points).call
|
||||
|
||||
{
|
||||
countries: data.map { _1[:country] }.uniq.count,
|
||||
cities: data.sum { _1[:cities].count }
|
||||
}
|
||||
end
|
||||
|
||||
def points
|
||||
user.tracked_points
|
||||
.without_raw_data
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class User < ApplicationRecord
|
||||
# Include default devise modules. Others available are:
|
||||
# :confirmable, :lockable, :timeoutable, and :omniauthable
|
||||
devise :database_authenticatable, :registerable,
|
||||
:recoverable, :rememberable, :validatable, :trackable
|
||||
|
||||
|
|
@ -70,10 +68,13 @@ class User < ApplicationRecord
|
|||
Rails.cache.fetch("dawarich/user_#{id}_years_tracked", expires_in: 1.day) do
|
||||
tracked_points
|
||||
.pluck(:timestamp)
|
||||
.map { |ts| Time.zone.at(ts).year }
|
||||
.uniq
|
||||
.sort
|
||||
.reverse
|
||||
.map { |ts| Time.zone.at(ts) }
|
||||
.group_by(&:year)
|
||||
.transform_values do |dates|
|
||||
dates.map { |date| date.strftime('%b') }.uniq.sort
|
||||
end
|
||||
.map { |year, months| { year: year, months: months } }
|
||||
.sort_by { |entry| -entry[:year] } # Sort in descending order
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
24
app/services/cache/clean.rb
vendored
Normal file
24
app/services/cache/clean.rb
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Cache::Clean
|
||||
class << self
|
||||
def call
|
||||
Rails.logger.info('Cleaning cache...')
|
||||
delete_version_cache
|
||||
delete_years_tracked_cache
|
||||
Rails.logger.info('Cache cleaned')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def delete_version_cache
|
||||
Rails.cache.delete(CheckAppVersion::VERSION_CACHE_KEY)
|
||||
end
|
||||
|
||||
def delete_years_tracked_cache
|
||||
User.find_each do |user|
|
||||
Rails.cache.delete("dawarich/user_#{user.id}_years_tracked")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,56 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CountriesAndCities
|
||||
CountryData = Struct.new(:country, :cities, keyword_init: true)
|
||||
CityData = Struct.new(:city, :points, :timestamp, :stayed_for, keyword_init: true)
|
||||
|
||||
def initialize(points)
|
||||
@points = points
|
||||
end
|
||||
|
||||
def call
|
||||
grouped_records = group_points
|
||||
mapped_with_cities = map_with_cities(grouped_records)
|
||||
filtered_cities = filter_cities(mapped_with_cities)
|
||||
normalize_result(filtered_cities)
|
||||
points
|
||||
.reject { |point| point.country.nil? || point.city.nil? }
|
||||
.group_by(&:country)
|
||||
.transform_values { |country_points| process_country_points(country_points) }
|
||||
.map { |country, cities| CountryData.new(country: country, cities: cities) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :points
|
||||
|
||||
def group_points
|
||||
points.group_by(&:country)
|
||||
def process_country_points(country_points)
|
||||
country_points
|
||||
.group_by(&:city)
|
||||
.transform_values { |city_points| create_city_data_if_valid(city_points) }
|
||||
.values
|
||||
.compact
|
||||
end
|
||||
|
||||
def map_with_cities(grouped_records)
|
||||
grouped_records.transform_values do |grouped_points|
|
||||
grouped_points
|
||||
.pluck(:city, :timestamp) # Extract city and timestamp
|
||||
.delete_if { _1.first.nil? } # Remove records without city
|
||||
.group_by { |city, _| city } # Group by city
|
||||
.transform_values do |cities|
|
||||
{
|
||||
points: cities.count,
|
||||
last_timestamp: cities.map(&:last).max, # Get the maximum timestamp
|
||||
stayed_for: ((cities.map(&:last).max - cities.map(&:last).min).to_i / 60) # Calculate the time stayed in minutes
|
||||
}
|
||||
end
|
||||
end
|
||||
def create_city_data_if_valid(city_points)
|
||||
timestamps = city_points.pluck(:timestamp)
|
||||
duration = calculate_duration_in_minutes(timestamps)
|
||||
city = city_points.first.city
|
||||
points_count = city_points.size
|
||||
|
||||
build_city_data(city, points_count, timestamps, duration)
|
||||
end
|
||||
|
||||
def filter_cities(mapped_with_cities)
|
||||
# Remove cities where user stayed for less than 1 hour
|
||||
mapped_with_cities.transform_values do |cities|
|
||||
cities.reject { |_, data| data[:stayed_for] < MIN_MINUTES_SPENT_IN_CITY }
|
||||
end
|
||||
def build_city_data(city, points_count, timestamps, duration)
|
||||
return nil if duration < ::MIN_MINUTES_SPENT_IN_CITY
|
||||
|
||||
CityData.new(
|
||||
city: city,
|
||||
points: points_count,
|
||||
timestamp: timestamps.max,
|
||||
stayed_for: duration
|
||||
)
|
||||
end
|
||||
|
||||
def normalize_result(hash)
|
||||
hash.map do |country, cities|
|
||||
{
|
||||
country:,
|
||||
cities: cities.map do |city, data|
|
||||
{ city:, points: data[:points], timestamp: data[:last_timestamp], stayed_for: data[:stayed_for] }
|
||||
end
|
||||
}
|
||||
end
|
||||
def calculate_duration_in_minutes(timestamps)
|
||||
((timestamps.max - timestamps.min).to_i / 60)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ class ReverseGeocoding::Points::FetchData
|
|||
def call
|
||||
return if point.reverse_geocoded?
|
||||
|
||||
update_point_with_geocoding_data
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_point_with_geocoding_data
|
||||
response = Geocoder.search([point.latitude, point.longitude]).first
|
||||
return if response.blank? || response.data['error'].present?
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
class Stats::CalculateMonth
|
||||
def initialize(user_id, year, month)
|
||||
@user = User.find(user_id)
|
||||
@year = year
|
||||
@month = month
|
||||
@year = year.to_i
|
||||
@month = month.to_i
|
||||
end
|
||||
|
||||
def call
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<% content_for :title, 'Map' %>
|
||||
|
||||
<div class="flex flex-col lg:flex-row lg:space-x-4 my-5 w-full">
|
||||
<div class='w-full lg:w-5/6'>
|
||||
<div class='w-full'>
|
||||
<div class="flex flex-col space-y-4 mb-4 w-full">
|
||||
<%= form_with url: map_path(import_id: params[:import_id]), method: :get do |f| %>
|
||||
<div class="flex flex-col space-y-4 sm:flex-row sm:space-y-0 sm:space-x-4 sm:items-end">
|
||||
|
|
@ -58,10 +58,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='w-full lg:w-1/6 mt-8 lg:mt-0 mx-auto'>
|
||||
<%= render 'shared/right_sidebar' %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= render 'map/settings_modals' %>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@
|
|||
<h1 class="font-bold text-4xl mb-4">Notifications</h1>
|
||||
<div class="flex items-center justify-center mb-4">
|
||||
<% if @notifications.unread.any? %>
|
||||
<%= link_to "Mark all as read", mark_notifications_as_read_path, method: :post, data: { turbo_method: :post }, class: "btn btn-sm btn-primary" %>
|
||||
<%= link_to "Mark all as read", mark_notifications_as_read_path, method: :post, data: { turbo_method: :post }, class: "btn btn-sm btn-primary" %>
|
||||
<% end %>
|
||||
<% if @notifications.any? %>
|
||||
<%= link_to "Delete all", delete_all_notifications_path, method: :post, data: { turbo_method: :post, turbo_confirm: 'Are you sure you want to delete all notifications?' }, class: "btn btn-sm btn-warning" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
<%= sidebar_distance(@distance) %> <%= sidebar_points(@points) %>
|
||||
|
||||
<div id='years-nav'>
|
||||
<div class="dropdown">
|
||||
<div tabindex="0" role="button" class="btn">Select year</div>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
|
||||
<% current_user.years_tracked.each do |year| %>
|
||||
<li><%= link_to year, map_url(year_timespan(year).merge(year: year, import_id: params[:import_id])) %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<% @years.each do |year| %>
|
||||
<h3 class='text-xl'>
|
||||
<%= year %>
|
||||
</h3>
|
||||
|
||||
<div class='grid grid-cols-3 gap-3'>
|
||||
<% (1..12).to_a.each_slice(3) do |months| %>
|
||||
<% months.each do |month_number| %>
|
||||
<% if past?(year, month_number) && points_exist?(year, month_number, current_user) %>
|
||||
<%= link_to Date::ABBR_MONTHNAMES[month_number], map_url(timespan(month_number, year).merge(import_id: params[:import_id])), class: 'btn btn-default' %>
|
||||
<% else %>
|
||||
<div class='btn btn-disabled'><%= Date::ABBR_MONTHNAMES[month_number] %></div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if REVERSE_GEOCODING_ENABLED && @countries_and_cities&.any? %>
|
||||
<hr class='my-5'>
|
||||
<h2 class='text-lg font-semibold'>Countries and cities</h2>
|
||||
<% @countries_and_cities.each do |country| %>
|
||||
<% next if country[:cities].empty? %>
|
||||
|
||||
<h2 class="text-lg font-semibold mt-5">
|
||||
<%= country[:country] %> (<%= country[:cities].count %> cities)
|
||||
</h2>
|
||||
<ul class="timeline timeline-vertical">
|
||||
<% country[:cities].each do |city| %>
|
||||
<li>
|
||||
<hr />
|
||||
<div class="timeline-start"><%= link_to_date(city[:timestamp]) %></div>
|
||||
<div class="timeline-middle">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="h-5 w-5">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="timeline-end timeline-box"><%= city[:city] %></div>
|
||||
<hr />
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
@ -1,10 +1,16 @@
|
|||
<div id="<%= dom_id stat %>" class="card w-full bg-base-200 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
<%= link_to map_url(timespan(stat.month, stat.year)), class: "underline hover:no-underline text-#{header_colors.sample}" do %>
|
||||
<%= "#{Date::MONTHNAMES[stat.month]} of #{stat.year}" %>
|
||||
<% end %>
|
||||
</h2>
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="card-title">
|
||||
<%= link_to map_url(timespan(stat.month, stat.year)), class: "underline hover:no-underline text-#{header_colors.sample}" do %>
|
||||
<%= Date::MONTHNAMES[stat.month] %>
|
||||
<% end %>
|
||||
</h2>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<%= link_to '[Update]', update_year_month_stats_path(stat.year, stat.month), data: { turbo_method: :put }, class: 'text-sm text-gray-500 hover:underline' %>
|
||||
</div>
|
||||
</div>
|
||||
<p><%= stat.distance %><%= DISTANCE_UNIT %></p>
|
||||
<% if REVERSE_GEOCODING_ENABLED %>
|
||||
<div class="card-actions justify-end">
|
||||
|
|
|
|||
|
|
@ -21,15 +21,18 @@
|
|||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= link_to 'Update stats', stats_path, data: { 'turbo-method' => :post }, class: 'btn btn-primary mt-5' %>
|
||||
<%= link_to 'Update stats', update_all_stats_path, data: { turbo_method: :put }, class: 'btn btn-primary mt-5' %>
|
||||
|
||||
<div class="mt-5 grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-6 p-4">
|
||||
<div class="mt-6 grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-6">
|
||||
<% @stats.each do |year, stats| %>
|
||||
<div class="card w-full bg-base-200 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-<%= header_colors.sample %>">
|
||||
<%= link_to year, "/stats/#{year}", class: 'underline hover:no-underline' %>
|
||||
<%= link_to '[Map]', map_url(year_timespan(year)), class: 'underline hover:no-underline' %>
|
||||
<h2 class="card-title justify-between text-<%= header_colors.sample %>">
|
||||
<div>
|
||||
<%= link_to year, "/stats/#{year}", class: 'underline hover:no-underline' %>
|
||||
<%= link_to '[Map]', map_url(year_timespan(year)), class: 'underline hover:no-underline' %>
|
||||
</div>
|
||||
<%= link_to '[Update]', update_year_month_stats_path(year, :all), data: { turbo_method: :put }, class: 'text-sm text-gray-500 hover:underline' %>
|
||||
</h2>
|
||||
<p>
|
||||
<% cache [current_user, 'year_distance_stat', year], skip_digest: true do %>
|
||||
|
|
|
|||
8
bin/rubocop
Executable file
8
bin/rubocop
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env ruby
|
||||
require "rubygems"
|
||||
require "bundler/setup"
|
||||
|
||||
# explicit rubocop config increases performance slightly while avoiding config confusion.
|
||||
ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))
|
||||
|
||||
load Gem.bin_path("rubocop", "rubocop")
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
#!/usr/bin/env ruby
|
||||
require "fileutils"
|
||||
|
||||
# path to your application root.
|
||||
APP_ROOT = File.expand_path("..", __dir__)
|
||||
APP_NAME = "dawarich"
|
||||
|
||||
def system!(*args)
|
||||
system(*args, exception: true)
|
||||
|
|
@ -30,4 +30,8 @@ FileUtils.chdir APP_ROOT do
|
|||
|
||||
puts "\n== Restarting application server =="
|
||||
system! "bin/rails restart"
|
||||
|
||||
# puts "\n== Configuring puma-dev =="
|
||||
# system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}"
|
||||
# system "curl -Is https://#{APP_NAME}.test/up | head -n 1"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Bundler.require(*Rails.groups)
|
|||
module Dawarich
|
||||
class Application < Rails::Application
|
||||
# Initialize configuration defaults for originally generated Rails version.
|
||||
config.load_defaults 7.0
|
||||
config.load_defaults 8.0
|
||||
|
||||
# Please, add to the `ignore` list any other `lib` subdirectories that do
|
||||
# not contain `.rb` files, or that should not be reloaded or eager loaded.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ require_relative 'application'
|
|||
# Initialize the Rails application.
|
||||
Rails.application.initialize!
|
||||
|
||||
# Clear the cache of the application version
|
||||
# Clear the cache
|
||||
Cache::CleaningJob.perform_later
|
||||
|
||||
Rails.cache.delete(CheckAppVersion::VERSION_CACHE_KEY)
|
||||
# Preheat the cache
|
||||
Cache::PreheatingJob.perform_later
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ Rails.application.configure do
|
|||
# Enable server timing
|
||||
config.server_timing = true
|
||||
|
||||
# Info include generic and useful information about system operation, but avoids logging too much
|
||||
# information to avoid inadvertent exposure of personally identifiable information (PII). If you
|
||||
# want to log everything, leave the level on "debug".
|
||||
config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'debug')
|
||||
|
||||
# Enable/disable caching. By default caching is disabled.
|
||||
# Run rails dev:cache to toggle caching.
|
||||
if Rails.root.join('tmp/caching-dev.txt').exist?
|
||||
|
|
@ -74,7 +79,7 @@ Rails.application.configure do
|
|||
# config.i18n.raise_on_missing_translations = true
|
||||
|
||||
# Annotate rendered view with file names.
|
||||
# config.action_view.annotate_rendered_view_with_filenames = true
|
||||
config.action_view.annotate_rendered_view_with_filenames = true
|
||||
|
||||
# Uncomment if you wish to allow Action Cable access from any origin.
|
||||
# config.action_cable.disable_request_forgery_protection = true
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
require "active_support/core_ext/integer/time"
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'active_support/core_ext/integer/time'
|
||||
|
||||
Rails.application.configure do
|
||||
# Settings specified here will take precedence over those in config/application.rb.
|
||||
|
|
@ -39,6 +41,8 @@ Rails.application.configure do
|
|||
# Store uploaded files on the local file system (see config/storage.yml for options).
|
||||
config.active_storage.service = :local
|
||||
|
||||
config.silence_healthcheck_path = '/api/v1/health'
|
||||
|
||||
# Mount Action Cable outside main process or domain.
|
||||
# config.action_cable.mount_path = nil
|
||||
# config.action_cable.url = "wss://example.com/cable"
|
||||
|
|
@ -52,17 +56,17 @@ Rails.application.configure do
|
|||
config.force_ssl = true
|
||||
|
||||
# Log to STDOUT by default
|
||||
config.logger = ActiveSupport::Logger.new(STDOUT)
|
||||
.tap { |logger| logger.formatter = ::Logger::Formatter.new }
|
||||
.then { |logger| ActiveSupport::TaggedLogging.new(logger) }
|
||||
config.logger = ActiveSupport::Logger.new($stdout)
|
||||
.tap { |logger| logger.formatter = ::Logger::Formatter.new }
|
||||
.then { |logger| ActiveSupport::TaggedLogging.new(logger) }
|
||||
|
||||
# Prepend all log lines with the following tags.
|
||||
config.log_tags = [ :request_id ]
|
||||
config.log_tags = [:request_id]
|
||||
|
||||
# Info include generic and useful information about system operation, but avoids logging too much
|
||||
# information to avoid inadvertent exposure of personally identifiable information (PII). If you
|
||||
# want to log everything, set the level to "debug".
|
||||
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
|
||||
config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'info')
|
||||
|
||||
# Use a different cache store in production.
|
||||
# config.cache_store = :mem_cache_store
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'active_support/core_ext/integer/time'
|
||||
|
||||
# The test environment is used exclusively to run your application's
|
||||
|
|
@ -24,11 +26,11 @@ Rails.application.configure do
|
|||
}
|
||||
|
||||
# Show full error reports and disable caching.
|
||||
config.consider_all_requests_local = true
|
||||
config.consider_all_requests_local = true
|
||||
config.action_controller.perform_caching = false
|
||||
config.cache_store = :null_store
|
||||
|
||||
# Raise exceptions instead of rendering exception templates.
|
||||
# Render exception templates for rescuable exceptions and raise for other exceptions.
|
||||
config.action_dispatch.show_exceptions = :rescuable
|
||||
|
||||
# Disable request forgery protection in test environment.
|
||||
|
|
@ -37,6 +39,8 @@ Rails.application.configure do
|
|||
# Store uploaded files on the local file system in a temporary directory.
|
||||
config.active_storage.service = :test
|
||||
|
||||
# Disable caching for Action Mailer templates even if Action Controller
|
||||
# caching is enabled.
|
||||
config.action_mailer.perform_caching = false
|
||||
|
||||
# Tell Action Mailer not to deliver emails to the real world.
|
||||
|
|
@ -44,6 +48,10 @@ Rails.application.configure do
|
|||
# ActionMailer::Base.deliveries array.
|
||||
config.action_mailer.delivery_method = :test
|
||||
|
||||
# Unlike controllers, the mailer instance doesn't have any context about the
|
||||
# incoming request so you'll need to provide the :host parameter yourself.
|
||||
config.action_mailer.default_url_options = { host: 'www.example.com' }
|
||||
|
||||
# Print deprecation notices to the stderr.
|
||||
config.active_support.deprecation = :stderr
|
||||
|
||||
|
|
@ -59,6 +67,6 @@ Rails.application.configure do
|
|||
# Annotate rendered view with file names.
|
||||
# config.action_view.annotate_rendered_view_with_filenames = true
|
||||
|
||||
# Raise error when a before_action's only/except options reference missing actions
|
||||
# Raise error when a before_action's only/except options reference missing actions.
|
||||
config.action_controller.raise_on_missing_callback_actions = true
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
# Version of your assets, change this if you want to expire all your assets.
|
||||
Rails.application.config.assets.version = "1.0"
|
||||
Rails.application.config.assets.version = '1.0'
|
||||
|
||||
# Add additional assets to the asset load path.
|
||||
# Rails.application.config.assets.paths << Emoji.images_path
|
||||
|
||||
# Precompile additional assets.
|
||||
# application.js, application.css, and all non-JS/CSS in the app/assets
|
||||
# folder are already added.
|
||||
# Rails.application.config.assets.precompile += %w( admin.js admin.css )
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
|
||||
# Use this to limit dissemination of sensitive information.
|
||||
# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
|
||||
Rails.application.config.filter_parameters += [
|
||||
:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
|
||||
Rails.application.config.filter_parameters += %i[
|
||||
passw email secret token _key crypt salt certificate otp ssn cvv cvc latitude longitude lat lng
|
||||
]
|
||||
|
|
|
|||
|
|
@ -17,4 +17,6 @@ if defined?(PHOTON_API_HOST)
|
|||
settings[:photon] = { use_https: PHOTON_API_USE_HTTPS, host: PHOTON_API_HOST }
|
||||
end
|
||||
|
||||
settings[:http_headers] = { 'X-Api-Key' => PHOTON_API_KEY } if defined?(PHOTON_API_KEY)
|
||||
|
||||
Geocoder.configure(settings)
|
||||
|
|
|
|||
8
config/initializers/httparty.rb
Normal file
8
config/initializers/httparty.rb
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Suppress warnings about nil deprecation
|
||||
# https://github.com/jnunemaker/httparty/issues/568#issuecomment-1450473603
|
||||
|
||||
HTTParty::Response.class_eval do
|
||||
def warn_about_nil_deprecation; end
|
||||
end
|
||||
|
|
@ -1,223 +0,0 @@
|
|||
# Be sure to restart your server when you modify this file.
|
||||
#
|
||||
# This file eases your Rails 7.1 framework defaults upgrade.
|
||||
#
|
||||
# Uncomment each configuration one by one to switch to the new default.
|
||||
# Once your application is ready to run with all new defaults, you can remove
|
||||
# this file and set the `config.load_defaults` to `7.1`.
|
||||
#
|
||||
# Read the Guide for Upgrading Ruby on Rails for more info on each option.
|
||||
# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html
|
||||
|
||||
# No longer add autoloaded paths into `$LOAD_PATH`. This means that you won't be able
|
||||
# to manually require files that are managed by the autoloader, which you shouldn't do anyway.
|
||||
# This will reduce the size of the load path, making `require` faster if you don't use bootsnap, or reduce the size
|
||||
# of the bootsnap cache if you use it.
|
||||
# Rails.application.config.add_autoload_paths_to_load_path = false
|
||||
|
||||
# Remove the default X-Download-Options headers since it is used only by Internet Explorer.
|
||||
# If you need to support Internet Explorer, add back `"X-Download-Options" => "noopen"`.
|
||||
# Rails.application.config.action_dispatch.default_headers = {
|
||||
# "X-Frame-Options" => "SAMEORIGIN",
|
||||
# "X-XSS-Protection" => "0",
|
||||
# "X-Content-Type-Options" => "nosniff",
|
||||
# "X-Permitted-Cross-Domain-Policies" => "none",
|
||||
# "Referrer-Policy" => "strict-origin-when-cross-origin"
|
||||
# }
|
||||
|
||||
# Do not treat an `ActionController::Parameters` instance
|
||||
# as equal to an equivalent `Hash` by default.
|
||||
# Rails.application.config.action_controller.allow_deprecated_parameters_hash_equality = false
|
||||
|
||||
# Active Record Encryption now uses SHA-256 as its hash digest algorithm. Important: If you have
|
||||
# data encrypted with previous Rails versions, there are two scenarios to consider:
|
||||
#
|
||||
# 1. If you have +config.active_support.key_generator_hash_digest_class+ configured as SHA1 (the default
|
||||
# before Rails 7.0), you need to configure SHA-1 for Active Record Encryption too:
|
||||
# Rails.application.config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA1
|
||||
# 2. If you have +config.active_support.key_generator_hash_digest_class+ configured as SHA256 (the new default
|
||||
# in 7.0), then you need to configure SHA-256 for Active Record Encryption:
|
||||
# Rails.application.config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA256
|
||||
#
|
||||
# If you don't currently have data encrypted with Active Record encryption, you can disable this setting to
|
||||
# configure the default behavior starting 7.1+:
|
||||
# Rails.application.config.active_record.encryption.support_sha1_for_non_deterministic_encryption = false
|
||||
|
||||
# No longer run after_commit callbacks on the first of multiple Active Record
|
||||
# instances to save changes to the same database row within a transaction.
|
||||
# Instead, run these callbacks on the instance most likely to have internal
|
||||
# state which matches what was committed to the database, typically the last
|
||||
# instance to save.
|
||||
# Rails.application.config.active_record.run_commit_callbacks_on_first_saved_instances_in_transaction = false
|
||||
|
||||
# Configures SQLite with a strict strings mode, which disables double-quoted string literals.
|
||||
#
|
||||
# SQLite has some quirks around double-quoted string literals.
|
||||
# It first tries to consider double-quoted strings as identifier names, but if they don't exist
|
||||
# it then considers them as string literals. Because of this, typos can silently go unnoticed.
|
||||
# For example, it is possible to create an index for a non existing column.
|
||||
# See https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted for more details.
|
||||
# Rails.application.config.active_record.sqlite3_adapter_strict_strings_by_default = true
|
||||
|
||||
# Disable deprecated singular associations names
|
||||
# Rails.application.config.active_record.allow_deprecated_singular_associations_name = false
|
||||
|
||||
# Enable the Active Job `BigDecimal` argument serializer, which guarantees
|
||||
# roundtripping. Without this serializer, some queue adapters may serialize
|
||||
# `BigDecimal` arguments as simple (non-roundtrippable) strings.
|
||||
#
|
||||
# When deploying an application with multiple replicas, old (pre-Rails 7.1)
|
||||
# replicas will not be able to deserialize `BigDecimal` arguments from this
|
||||
# serializer. Therefore, this setting should only be enabled after all replicas
|
||||
# have been successfully upgraded to Rails 7.1.
|
||||
# Rails.application.config.active_job.use_big_decimal_serializer = true
|
||||
|
||||
# Specify if an `ArgumentError` should be raised if `Rails.cache` `fetch` or
|
||||
# `write` are given an invalid `expires_at` or `expires_in` time.
|
||||
# Options are `true`, and `false`. If `false`, the exception will be reported
|
||||
# as `handled` and logged instead.
|
||||
# Rails.application.config.active_support.raise_on_invalid_cache_expiration_time = true
|
||||
|
||||
# Specify whether Query Logs will format tags using the SQLCommenter format
|
||||
# (https://open-telemetry.github.io/opentelemetry-sqlcommenter/), or using the legacy format.
|
||||
# Options are `:legacy` and `:sqlcommenter`.
|
||||
# Rails.application.config.active_record.query_log_tags_format = :sqlcommenter
|
||||
|
||||
# Specify the default serializer used by `MessageEncryptor` and `MessageVerifier`
|
||||
# instances.
|
||||
#
|
||||
# The legacy default is `:marshal`, which is a potential vector for
|
||||
# deserialization attacks in cases where a message signing secret has been
|
||||
# leaked.
|
||||
#
|
||||
# In Rails 7.1, the new default is `:json_allow_marshal` which serializes and
|
||||
# deserializes with `ActiveSupport::JSON`, but can fall back to deserializing
|
||||
# with `Marshal` so that legacy messages can still be read.
|
||||
#
|
||||
# In Rails 7.2, the default will become `:json` which serializes and
|
||||
# deserializes with `ActiveSupport::JSON` only.
|
||||
#
|
||||
# Alternatively, you can choose `:message_pack` or `:message_pack_allow_marshal`,
|
||||
# which serialize with `ActiveSupport::MessagePack`. `ActiveSupport::MessagePack`
|
||||
# can roundtrip some Ruby types that are not supported by JSON, and may provide
|
||||
# improved performance, but it requires the `msgpack` gem.
|
||||
#
|
||||
# For more information, see
|
||||
# https://guides.rubyonrails.org/v7.1/configuring.html#config-active-support-message-serializer
|
||||
#
|
||||
# If you are performing a rolling deploy of a Rails 7.1 upgrade, wherein servers
|
||||
# that have not yet been upgraded must be able to read messages from upgraded
|
||||
# servers, first deploy without changing the serializer, then set the serializer
|
||||
# in a subsequent deploy.
|
||||
# Rails.application.config.active_support.message_serializer = :json_allow_marshal
|
||||
|
||||
# Enable a performance optimization that serializes message data and metadata
|
||||
# together. This changes the message format, so messages serialized this way
|
||||
# cannot be read by older versions of Rails. However, messages that use the old
|
||||
# format can still be read, regardless of whether this optimization is enabled.
|
||||
#
|
||||
# To perform a rolling deploy of a Rails 7.1 upgrade, wherein servers that have
|
||||
# not yet been upgraded must be able to read messages from upgraded servers,
|
||||
# leave this optimization off on the first deploy, then enable it on a
|
||||
# subsequent deploy.
|
||||
# Rails.application.config.active_support.use_message_serializer_for_metadata = true
|
||||
|
||||
# Set the maximum size for Rails log files.
|
||||
#
|
||||
# `config.load_defaults 7.1` does not set this value for environments other than
|
||||
# development and test.
|
||||
#
|
||||
# if Rails.env.local?
|
||||
# Rails.application.config.log_file_size = 100 * 1024 * 1024
|
||||
# end
|
||||
|
||||
# Enable raising on assignment to attr_readonly attributes. The previous
|
||||
# behavior would allow assignment but silently not persist changes to the
|
||||
# database.
|
||||
# Rails.application.config.active_record.raise_on_assign_to_attr_readonly = true
|
||||
|
||||
# Enable validating only parent-related columns for presence when the parent is mandatory.
|
||||
# The previous behavior was to validate the presence of the parent record, which performed an extra query
|
||||
# to get the parent every time the child record was updated, even when parent has not changed.
|
||||
# Rails.application.config.active_record.belongs_to_required_validates_foreign_key = false
|
||||
|
||||
# Enable precompilation of `config.filter_parameters`. Precompilation can
|
||||
# improve filtering performance, depending on the quantity and types of filters.
|
||||
# Rails.application.config.precompile_filter_parameters = true
|
||||
|
||||
# Enable before_committed! callbacks on all enrolled records in a transaction.
|
||||
# The previous behavior was to only run the callbacks on the first copy of a record
|
||||
# if there were multiple copies of the same record enrolled in the transaction.
|
||||
# Rails.application.config.active_record.before_committed_on_all_records = true
|
||||
|
||||
# Disable automatic column serialization into YAML.
|
||||
# To keep the historic behavior, you can set it to `YAML`, however it is
|
||||
# recommended to explicitly define the serialization method for each column
|
||||
# rather than to rely on a global default.
|
||||
# Rails.application.config.active_record.default_column_serializer = nil
|
||||
|
||||
# Enable a performance optimization that serializes Active Record models
|
||||
# in a faster and more compact way.
|
||||
#
|
||||
# To perform a rolling deploy of a Rails 7.1 upgrade, wherein servers that have
|
||||
# not yet been upgraded must be able to read caches from upgraded servers,
|
||||
# leave this optimization off on the first deploy, then enable it on a
|
||||
# subsequent deploy.
|
||||
# Rails.application.config.active_record.marshalling_format_version = 7.1
|
||||
|
||||
# Run `after_commit` and `after_*_commit` callbacks in the order they are defined in a model.
|
||||
# This matches the behaviour of all other callbacks.
|
||||
# In previous versions of Rails, they ran in the inverse order.
|
||||
# Rails.application.config.active_record.run_after_transaction_callbacks_in_order_defined = true
|
||||
|
||||
# Whether a `transaction` block is committed or rolled back when exited via `return`, `break` or `throw`.
|
||||
#
|
||||
# Rails.application.config.active_record.commit_transaction_on_non_local_return = true
|
||||
|
||||
# Controls when to generate a value for <tt>has_secure_token</tt> declarations.
|
||||
#
|
||||
# Rails.application.config.active_record.generate_secure_token_on = :initialize
|
||||
|
||||
# ** Please read carefully, this must be configured in config/application.rb **
|
||||
# Change the format of the cache entry.
|
||||
# Changing this default means that all new cache entries added to the cache
|
||||
# will have a different format that is not supported by Rails 7.0
|
||||
# applications.
|
||||
# Only change this value after your application is fully deployed to Rails 7.1
|
||||
# and you have no plans to rollback.
|
||||
# When you're ready to change format, add this to `config/application.rb` (NOT
|
||||
# this file):
|
||||
# config.active_support.cache_format_version = 7.1
|
||||
|
||||
# Configure Action View to use HTML5 standards-compliant sanitizers when they are supported on your
|
||||
# platform.
|
||||
#
|
||||
# `Rails::HTML::Sanitizer.best_supported_vendor` will cause Action View to use HTML5-compliant
|
||||
# sanitizers if they are supported, else fall back to HTML4 sanitizers.
|
||||
#
|
||||
# In previous versions of Rails, Action View always used `Rails::HTML4::Sanitizer` as its vendor.
|
||||
#
|
||||
# Rails.application.config.action_view.sanitizer_vendor = Rails::HTML::Sanitizer.best_supported_vendor
|
||||
|
||||
# Configure Action Text to use an HTML5 standards-compliant sanitizer when it is supported on your
|
||||
# platform.
|
||||
#
|
||||
# `Rails::HTML::Sanitizer.best_supported_vendor` will cause Action Text to use HTML5-compliant
|
||||
# sanitizers if they are supported, else fall back to HTML4 sanitizers.
|
||||
#
|
||||
# In previous versions of Rails, Action Text always used `Rails::HTML4::Sanitizer` as its vendor.
|
||||
#
|
||||
# Rails.application.config.action_text.sanitizer_vendor = Rails::HTML::Sanitizer.best_supported_vendor
|
||||
|
||||
# Configure the log level used by the DebugExceptions middleware when logging
|
||||
# uncaught exceptions during requests
|
||||
# Rails.application.config.action_dispatch.debug_exception_log_level = :error
|
||||
|
||||
# Configure the test helpers in Action View, Action Dispatch, and rails-dom-testing to use HTML5
|
||||
# parsers.
|
||||
#
|
||||
# Nokogiri::HTML5 isn't supported on JRuby, so JRuby applications must set this to :html4.
|
||||
#
|
||||
# In previous versions of Rails, these test helpers always used an HTML4 parser.
|
||||
#
|
||||
# Rails.application.config.dom_testing_default_html_version = :html5
|
||||
30
config/initializers/new_framework_defaults_8_0.rb
Normal file
30
config/initializers/new_framework_defaults_8_0.rb
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Be sure to restart your server when you modify this file.
|
||||
#
|
||||
# This file eases your Rails 8.0 framework defaults upgrade.
|
||||
#
|
||||
# Uncomment each configuration one by one to switch to the new default.
|
||||
# Once your application is ready to run with all new defaults, you can remove
|
||||
# this file and set the `config.load_defaults` to `8.0`.
|
||||
#
|
||||
# Read the Guide for Upgrading Ruby on Rails for more info on each option.
|
||||
# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html
|
||||
|
||||
###
|
||||
# Specifies whether `to_time` methods preserve the UTC offset of their receivers or preserves the timezone.
|
||||
# If set to `:zone`, `to_time` methods will use the timezone of their receivers.
|
||||
# If set to `:offset`, `to_time` methods will use the UTC offset.
|
||||
# If `false`, `to_time` methods will convert to the local system UTC offset instead.
|
||||
#++
|
||||
# Rails.application.config.active_support.to_time_preserves_timezone = :zone
|
||||
|
||||
###
|
||||
# When both `If-Modified-Since` and `If-None-Match` are provided by the client
|
||||
# only consider `If-None-Match` as specified by RFC 7232 Section 6.
|
||||
# If set to `false` both conditions need to be satisfied.
|
||||
#++
|
||||
# Rails.application.config.action_dispatch.strict_freshness = true
|
||||
|
||||
###
|
||||
# Set `Regexp.timeout` to `1`s by default to improve security over Regexp Denial-of-Service attacks.
|
||||
#++
|
||||
# Regexp.timeout = 1
|
||||
|
|
@ -6,9 +6,8 @@
|
|||
# the maximum value specified for Puma. Default is set to 5 threads for minimum
|
||||
# and maximum; this matches the default thread size of Active Record.
|
||||
#
|
||||
max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
|
||||
min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count }
|
||||
threads min_threads_count, max_threads_count
|
||||
threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
|
||||
threads threads_count, threads_count
|
||||
|
||||
# Specifies the `worker_timeout` threshold that Puma will use to wait before
|
||||
# terminating a worker in development environments.
|
||||
|
|
@ -50,7 +49,7 @@ if ENV['PROMETHEUS_EXPORTER_ENABLED'].to_s == 'true'
|
|||
|
||||
before_fork do
|
||||
PrometheusExporter::Client.default = PrometheusExporter::Client.new(
|
||||
host: ENV.fetch('PROMETHEUS_EXPORTER_HOST', '0.0.0.0'),
|
||||
host: ENV.fetch('PROMETHEUS_EXPORTER_HOST', 'ANY'),
|
||||
port: ENV.fetch('PROMETHEUS_EXPORTER_PORT', 9394)
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -38,12 +38,17 @@ Rails.application.routes.draw do
|
|||
end
|
||||
resources :notifications, only: %i[index show destroy]
|
||||
post 'notifications/mark_as_read', to: 'notifications#mark_as_read', as: :mark_notifications_as_read
|
||||
post 'notifications/destroy_all', to: 'notifications#destroy_all', as: :delete_all_notifications
|
||||
resources :stats, only: :index do
|
||||
collection do
|
||||
post :update
|
||||
put :update_all
|
||||
end
|
||||
end
|
||||
get 'stats/:year', to: 'stats#show', constraints: { year: /\d{4}/ }
|
||||
put 'stats/:year/:month/update',
|
||||
to: 'stats#update',
|
||||
as: :update_year_month_stats,
|
||||
constraints: { year: /\d{4}/, month: /\d{1,2}|all/ }
|
||||
|
||||
root to: 'home#index'
|
||||
devise_for :users, skip: [:registrations]
|
||||
|
|
@ -52,8 +57,6 @@ Rails.application.routes.draw do
|
|||
put 'users' => 'devise/registrations#update', :as => 'user_registration'
|
||||
end
|
||||
|
||||
# And then modify the app/views/devise/shared/_links.erb
|
||||
|
||||
get 'map', to: 'map#index'
|
||||
|
||||
namespace :api do
|
||||
|
|
@ -78,6 +81,11 @@ Rails.application.routes.draw do
|
|||
|
||||
namespace :countries do
|
||||
resources :borders, only: :index
|
||||
resources :visited_cities, only: :index
|
||||
end
|
||||
|
||||
namespace :points do
|
||||
get 'tracked_months', to: 'tracked_months#index'
|
||||
end
|
||||
|
||||
resources :photos, only: %i[index] do
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@
|
|||
|
||||
class SetReverseGeocodedAtForPoints < ActiveRecord::Migration[7.2]
|
||||
def up
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
Point.where.not(geodata: {}).update_all(reverse_geocoded_at: Time.current)
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
DataMigrations::SetReverseGeocodedAtForPointsJob.perform_later
|
||||
end
|
||||
|
||||
def down
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexToPointsTimestamp < ActiveRecord::Migration[7.2]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def change
|
||||
add_index :points, %i[user_id timestamp], algorithm: :concurrently
|
||||
end
|
||||
end
|
||||
|
|
@ -18,10 +18,12 @@ services:
|
|||
timeout: 10s
|
||||
dawarich_db:
|
||||
image: postgres:14.2-alpine
|
||||
shm_size: 1G
|
||||
container_name: dawarich_db
|
||||
volumes:
|
||||
- dawarich_db_data:/var/lib/postgresql/data
|
||||
- dawarich_shared:/var/shared
|
||||
# - ./postgresql.conf:/etc/postgresql/postgresql.conf # Optional, uncomment if you want to use a custom config
|
||||
networks:
|
||||
- dawarich
|
||||
environment:
|
||||
|
|
@ -34,11 +36,12 @@ services:
|
|||
retries: 5
|
||||
start_period: 30s
|
||||
timeout: 10s
|
||||
# command: postgres -c config_file=/etc/postgresql/postgresql.conf # Use custom config, uncomment if you want to use a custom config
|
||||
dawarich_app:
|
||||
image: freikin/dawarich:latest
|
||||
container_name: dawarich_app
|
||||
volumes:
|
||||
- dawarich_gem_cache_app:/usr/local/bundle/gems_app
|
||||
- dawarich_gem_cache_app:/usr/local/bundle/gems
|
||||
- dawarich_public:/var/app/public
|
||||
- dawarich_watched:/var/app/tmp/imports/watched
|
||||
networks:
|
||||
|
|
@ -97,7 +100,7 @@ services:
|
|||
image: freikin/dawarich:latest
|
||||
container_name: dawarich_sidekiq
|
||||
volumes:
|
||||
- dawarich_gem_cache_sidekiq:/usr/local/bundle/gems_sidekiq
|
||||
- dawarich_gem_cache_sidekiq:/usr/local/bundle/gems
|
||||
- dawarich_public:/var/app/public
|
||||
- dawarich_watched:/var/app/tmp/imports/watched
|
||||
networks:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# How to install Dawarich on Kubernetes
|
||||
|
||||
> An **unofficial Helm chart** is available [here](https://github.com/Cogitri/charts/tree/master/charts/dawarich). For a manual installation using YAML manifests, see below.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Kubernetes cluster and basic kubectl knowledge.
|
||||
|
|
@ -7,6 +9,7 @@
|
|||
- Working Postgres and Redis instances. In this example Postgres lives in 'db' namespace and Redis in 'redis' namespace.
|
||||
- Ngingx ingress controller with Letsencrypt integeation.
|
||||
- This example uses 'example.com' as a domain name, you want to change it to your own.
|
||||
- This will work on IPv4 and IPv6 Single Stack clusters, as well as Dual Stack deployments.
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -140,8 +143,8 @@ spec:
|
|||
image: freikin/dawarich:0.16.4
|
||||
imagePullPolicy: Always
|
||||
volumeMounts:
|
||||
- mountPath: /usr/local/bundle/gems_app
|
||||
name: gem-cache
|
||||
- mountPath: /usr/local/bundle/gems
|
||||
name: gem-app
|
||||
- mountPath: /var/app/public
|
||||
name: public
|
||||
- mountPath: /var/app/tmp/imports/watched
|
||||
|
|
@ -149,7 +152,7 @@ spec:
|
|||
command:
|
||||
- "dev-entrypoint.sh"
|
||||
args:
|
||||
- "bin/rails server -p 3000 -b 0.0.0.0"
|
||||
- "bin/rails server -p 3000 -b ::"
|
||||
resources:
|
||||
requests:
|
||||
memory: "1Gi"
|
||||
|
|
@ -196,7 +199,7 @@ spec:
|
|||
image: freikin/dawarich:0.16.4
|
||||
imagePullPolicy: Always
|
||||
volumeMounts:
|
||||
- mountPath: /usr/local/bundle/gems_sidekiq
|
||||
- mountPath: /usr/local/bundle/gems
|
||||
name: gem-sidekiq
|
||||
- mountPath: /var/app/public
|
||||
name: public
|
||||
|
|
|
|||
36
postgresql.conf.example
Normal file
36
postgresql.conf.example
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
listen_addresses = '*'
|
||||
max_connections = 50
|
||||
|
||||
shared_buffers = 512MB
|
||||
|
||||
work_mem = 128MB
|
||||
maintenance_work_mem = 128MB
|
||||
|
||||
|
||||
dynamic_shared_memory_type = posix
|
||||
checkpoint_timeout = 10min # range 30s-1d
|
||||
max_wal_size = 2GB
|
||||
min_wal_size = 80MB
|
||||
max_parallel_workers_per_gather = 4
|
||||
|
||||
log_min_duration_statement = 500 # -1 is disabled, 0 logs all statements
|
||||
# -1 disables, 0 logs all temp files
|
||||
log_timezone = 'UTC'
|
||||
|
||||
|
||||
autovacuum_vacuum_scale_factor = 0.05 # fraction of table size before vacuum
|
||||
autovacuum_analyze_scale_factor = 0.05 # fraction of table size before analyze
|
||||
|
||||
|
||||
datestyle = 'iso, dmy'
|
||||
|
||||
timezone = 'UTC'
|
||||
|
||||
lc_messages = 'en_US.utf8' # locale for system error message
|
||||
# strings
|
||||
lc_monetary = 'en_US.utf8' # locale for monetary formatting
|
||||
lc_numeric = 'en_US.utf8' # locale for number formatting
|
||||
lc_time = 'en_US.utf8' # locale for time formatting
|
||||
|
||||
|
||||
default_text_search_config = 'pg_catalog.english'
|
||||
114
public/400.html
Normal file
114
public/400.html
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<!doctype html>
|
||||
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
|
||||
<title>The server cannot process the request due to a client error (400 Bad Request)</title>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<style>
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #FFF;
|
||||
color: #261B23;
|
||||
display: grid;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Aptos, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: clamp(1rem, 2.5vw, 2rem);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.0025em;
|
||||
line-height: 1.4;
|
||||
min-height: 100vh;
|
||||
place-items: center;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
font-weight: 700;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.0925em;
|
||||
}
|
||||
|
||||
b, strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
i, em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
gap: 1em;
|
||||
padding: 2em;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
main header {
|
||||
width: min(100%, 12em);
|
||||
}
|
||||
|
||||
main header svg {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
main article {
|
||||
width: min(100%, 30em);
|
||||
}
|
||||
|
||||
main article p {
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
main article br {
|
||||
|
||||
display: none;
|
||||
|
||||
@media(min-width: 48em) {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- This file lives in public/400.html -->
|
||||
|
||||
<main>
|
||||
<header>
|
||||
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm140.456 133.2831c-40.823 0-64.884-35.146-64.884-85.7015 0-50.5554 24.061-85.700907 64.884-85.700907 40.822 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.062 85.7015-64.884 85.7015zm0-133.2831c-17.573 0-22.71 21.8984-22.71 47.5816 0 25.6835 5.137 47.5815 22.71 47.5815 17.302 0 22.709-21.898 22.709-47.5815 0-25.6832-5.407-47.5816-22.709-47.5816z" fill="#f0eff0"/><path d="m123.606 85.4445c3.212 1.0523 5.538 4.2089 5.538 8.0301 0 6.1472-4.209 9.5254-11.298 9.5254h-15.617v-34.0033h14.565c7.089 0 11.353 3.1566 11.353 9.2484 0 3.6551-2.049 6.3134-4.541 7.1994zm-12.904-2.9905h5.095c2.603 0 3.988-.9968 3.988-3.1013 0-2.1044-1.385-3.0459-3.988-3.0459h-5.095zm0 6.6456v6.5902h5.981c2.492 0 3.877-1.3291 3.877-3.2674 0-2.049-1.385-3.3228-3.877-3.3228zm43.786 13.9004h-8.362v-1.274c-.831.831-3.323 1.717-5.981 1.717-4.929 0-9.083-2.769-9.083-8.0301 0-4.818 4.154-7.9193 9.581-7.9193 2.049 0 4.486.6646 5.483 1.3845v-1.606c0-1.606-.942-2.9905-3.046-2.9905-1.606 0-2.548.7199-2.935 1.8275h-8.197c.72-4.8181 4.985-8.6393 11.409-8.6393 7.088 0 11.131 3.7659 11.131 10.2453zm-8.362-6.9779v-1.4399c-.554-1.0522-2.049-1.7167-3.655-1.7167-1.717 0-3.434.7199-3.434 2.3813 0 1.7168 1.717 2.4367 3.434 2.4367 1.606 0 3.101-.6645 3.655-1.6614zm27.996 6.9779v-1.994c-1.163 1.329-3.599 2.548-6.147 2.548-7.199 0-11.131-5.8151-11.131-13.0145s3.932-13.0143 11.131-13.0143c2.548 0 4.984 1.2184 6.147 2.5475v-13.0697h8.695v35.997zm0-9.1931v-6.5902c-.664-1.3291-2.159-2.326-3.821-2.326-2.99 0-4.763 2.4368-4.763 5.6488s1.773 5.5934 4.763 5.5934c1.717 0 3.157-.9415 3.821-2.326zm35.471-2.049h-3.101v11.2421h-8.806v-34.0033h15.285c7.31 0 12.35 4.1535 12.35 11.5744 0 5.1503-2.603 8.6947-6.757 10.2453l7.975 12.1836h-9.858zm-3.101-15.2849v8.1962h5.538c3.156 0 4.596-1.606 4.596-4.0981s-1.44-4.0981-4.596-4.0981zm36.957 17.8323h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm30.98 27.5234v-10.799c-1.163 1.329-3.6 2.548-6.147 2.548-7.2 0-11.132-5.9259-11.132-13.0145 0-7.144 3.932-13.0143 11.132-13.0143 2.547 0 4.984 1.2184 6.147 2.5475v-1.9937h8.695v33.726zm0-17.9981v-6.5902c-.665-1.3291-2.105-2.326-3.821-2.326-2.991 0-4.763 2.4368-4.763 5.6488s1.772 5.5934 4.763 5.5934c1.661 0 3.156-.9415 3.821-2.326zm36.789-15.7279v24.921h-8.695v-2.16c-1.329 1.551-3.821 2.714-6.646 2.714-5.482 0-8.75-3.5999-8.75-9.1379v-16.3371h8.64v14.288c0 2.1045.996 3.5997 3.212 3.5997 1.606 0 3.101-1.0522 3.544-2.769v-15.1187zm19.084 16.2263h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.963 5.095 11.963 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm13.428 11.0206h8.474c.387 1.3845 1.606 2.1598 3.156 2.1598 1.44 0 2.548-.5538 2.548-1.7168 0-.9414-.72-1.2737-1.939-1.5506l-4.873-.9969c-4.154-.886-6.867-2.8797-6.867-7.2547 0-5.3165 4.762-8.4178 10.633-8.4178 6.812 0 10.522 3.1567 11.297 8.0855h-8.03c-.277-1.0522-1.052-1.9937-3.046-1.9937-1.273 0-2.326.5538-2.326 1.6614 0 .7753.554 1.163 1.717 1.3845l4.929 1.163c4.541 1.0522 6.978 3.4335 6.978 7.4763 0 5.3168-4.818 8.2518-10.91 8.2518-6.369 0-10.965-2.88-11.741-8.2518zm27.538-.8861v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.993-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.871 0-9.193-2.769-9.193-9.0819z" fill="#d30001"/></svg>
|
||||
</header>
|
||||
<article>
|
||||
<p><strong>The server cannot process the request due to a client error.</strong> Please check the request and try again. If you’re the application owner check the logs for more information.</p>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
169
public/404.html
169
public/404.html
|
|
@ -1,67 +1,114 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>The page you were looking for doesn't exist (404)</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<style>
|
||||
.rails-default-error-page {
|
||||
background-color: #EFEFEF;
|
||||
color: #2E2F30;
|
||||
text-align: center;
|
||||
font-family: arial, sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
<!doctype html>
|
||||
|
||||
.rails-default-error-page div.dialog {
|
||||
width: 95%;
|
||||
max-width: 33em;
|
||||
margin: 4em auto 0;
|
||||
}
|
||||
<html lang="en">
|
||||
|
||||
.rails-default-error-page div.dialog > div {
|
||||
border: 1px solid #CCC;
|
||||
border-right-color: #999;
|
||||
border-left-color: #999;
|
||||
border-bottom-color: #BBB;
|
||||
border-top: #B00100 solid 4px;
|
||||
border-top-left-radius: 9px;
|
||||
border-top-right-radius: 9px;
|
||||
background-color: white;
|
||||
padding: 7px 12% 0;
|
||||
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
||||
}
|
||||
<head>
|
||||
|
||||
.rails-default-error-page h1 {
|
||||
font-size: 100%;
|
||||
color: #730E15;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
<title>The page you were looking for doesn’t exist (404 Not found)</title>
|
||||
|
||||
.rails-default-error-page div.dialog > p {
|
||||
margin: 0 0 1em;
|
||||
padding: 1em;
|
||||
background-color: #F7F7F7;
|
||||
border: 1px solid #CCC;
|
||||
border-right-color: #999;
|
||||
border-left-color: #999;
|
||||
border-bottom-color: #999;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-top-color: #DADADA;
|
||||
color: #666;
|
||||
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<style>
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #FFF;
|
||||
color: #261B23;
|
||||
display: grid;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Aptos, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: clamp(1rem, 2.5vw, 2rem);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.0025em;
|
||||
line-height: 1.4;
|
||||
min-height: 100vh;
|
||||
place-items: center;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
font-weight: 700;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.0925em;
|
||||
}
|
||||
|
||||
b, strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
i, em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
gap: 1em;
|
||||
padding: 2em;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
main header {
|
||||
width: min(100%, 12em);
|
||||
}
|
||||
|
||||
main header svg {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
main article {
|
||||
width: min(100%, 30em);
|
||||
}
|
||||
|
||||
main article p {
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
main article br {
|
||||
|
||||
display: none;
|
||||
|
||||
@media(min-width: 48em) {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- This file lives in public/404.html -->
|
||||
|
||||
<main>
|
||||
<header>
|
||||
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm165.328-35.41581-45.689 100.02991h26.224v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.184v-31.901l50.285-103.27391z" fill="#f0eff0"/><path d="m157.758 68.9967v34.0033h-7.199l-14.233-19.8814v19.8814h-8.584v-34.0033h8.307l13.125 18.7184v-18.7184zm28.454 21.5428c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.528 0c0-3.4336-1.496-5.8703-4.209-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.209-2.3813 4.209-5.8149zm13.184 3.8766v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm37.027 8.5839h-8.806v-34.0033h23.924v7.6978h-15.118v6.7564h13.9v7.5316h-13.9zm41.876-12.4605c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.529 0c0-3.4336-1.495-5.8703-4.208-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.208-2.3813 4.208-5.8149zm35.337-12.4605v24.921h-8.695v-2.16c-1.329 1.551-3.821 2.714-6.646 2.714-5.482 0-8.75-3.5999-8.75-9.1379v-16.3371h8.64v14.288c0 2.1045.997 3.5997 3.212 3.5997 1.606 0 3.101-1.0522 3.544-2.769v-15.1187zm4.076 24.921v-24.921h8.694v2.1598c1.385-1.5506 3.822-2.7136 6.701-2.7136 5.538 0 8.806 3.5997 8.806 9.1377v16.3371h-8.639v-14.2327c0-2.049-1.053-3.5443-3.268-3.5443-1.717 0-3.156.9969-3.6 2.7136v15.0634zm44.113 0v-1.994c-1.163 1.329-3.6 2.548-6.147 2.548-7.2 0-11.132-5.8151-11.132-13.0145s3.932-13.0143 11.132-13.0143c2.547 0 4.984 1.2184 6.147 2.5475v-13.0697h8.695v35.997zm0-9.1931v-6.5902c-.665-1.3291-2.16-2.326-3.821-2.326-2.991 0-4.763 2.4368-4.763 5.6488s1.772 5.5934 4.763 5.5934c1.717 0 3.156-.9415 3.821-2.326z" fill="#d30001"/></svg>
|
||||
</header>
|
||||
<article>
|
||||
<p><strong>The page you were looking for doesn’t exist.</strong> You may have mistyped the address or the page may have moved. If you’re the application owner check the logs for more information.</p>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
|
||||
<body class="rails-default-error-page">
|
||||
<!-- This file lives in public/404.html -->
|
||||
<div class="dialog">
|
||||
<div>
|
||||
<h1>The page you were looking for doesn't exist.</h1>
|
||||
<p>You may have mistyped the address or the page may have moved.</p>
|
||||
</div>
|
||||
<p>If you are the application owner check the logs for more information.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
114
public/406-unsupported-browser.html
Normal file
114
public/406-unsupported-browser.html
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<!doctype html>
|
||||
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
|
||||
<title>Your browser is not supported (406 Not Acceptable)</title>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<style>
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #FFF;
|
||||
color: #261B23;
|
||||
display: grid;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Aptos, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: clamp(1rem, 2.5vw, 2rem);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.0025em;
|
||||
line-height: 1.4;
|
||||
min-height: 100vh;
|
||||
place-items: center;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
font-weight: 700;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.0925em;
|
||||
}
|
||||
|
||||
b, strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
i, em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
gap: 1em;
|
||||
padding: 2em;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
main header {
|
||||
width: min(100%, 12em);
|
||||
}
|
||||
|
||||
main header svg {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
main article {
|
||||
width: min(100%, 30em);
|
||||
}
|
||||
|
||||
main article p {
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
main article br {
|
||||
|
||||
display: none;
|
||||
|
||||
@media(min-width: 48em) {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- This file lives in public/406-unsupported-browser.html -->
|
||||
|
||||
<main>
|
||||
<header>
|
||||
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm202.906 9.7326h-41.093c-2.433-7.2994-7.84-12.4361-17.302-12.4361-16.221 0-25.413 17.5728-25.954 34.8752v1.3517c5.137-7.0291 16.221-12.4361 30.82-12.4361 33.524 0 54.881 24.0612 54.881 53.7998 0 33.253-23.791 58.396-61.64 58.396-21.628 0-39.741-10.003-50.825-27.576-9.733-14.599-13.788-32.442-13.788-54.3406 0-51.9072 24.331-89.485807 66.236-89.485807 32.712 0 53.258 18.654107 58.665 47.851907zm-82.727 66.2355c0 13.247 9.463 22.439 22.71 22.439 12.977 0 22.439-9.192 22.439-22.439 0-13.517-9.462-22.7091-22.439-22.7091-13.247 0-22.71 9.1921-22.71 22.7091z" fill="#f0eff0"/><path d="m100.761 68.9967v34.0033h-7.1991l-14.2326-19.8814v19.8814h-8.5839v-34.0033h8.307l13.125 18.7184v-18.7184zm28.454 21.5428c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.529 0c0-3.4336-1.495-5.8703-4.208-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.208-2.3813 4.208-5.8149zm13.185 3.8766v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm39.02-25.4194h9.083l12.958 34.0033h-9.027l-2.436-6.5902h-12.35l-2.381 6.5902h-8.806zm4.431 10.5222-3.489 9.5807h6.978zm17.44 11.0206c0-7.6978 5.095-13.0143 12.572-13.0143 6.701 0 10.854 3.9874 11.574 9.8023h-8.418c-.221-1.4953-1.384-2.6029-3.156-2.6029-2.437 0-3.988 2.2706-3.988 5.8149s1.551 5.7595 3.988 5.7595c1.772 0 2.935-1.0522 3.156-2.5475h8.418c-.72 5.7596-4.873 9.8025-11.574 9.8025-7.477 0-12.572-5.3167-12.572-13.0145zm25.676 0c0-7.6978 5.095-13.0143 12.572-13.0143 6.701 0 10.854 3.9874 11.574 9.8023h-8.418c-.221-1.4953-1.384-2.6029-3.156-2.6029-2.437 0-3.988 2.2706-3.988 5.8149s1.551 5.7595 3.988 5.7595c1.772 0 2.935-1.0522 3.156-2.5475h8.418c-.72 5.7596-4.873 9.8025-11.574 9.8025-7.477 0-12.572-5.3167-12.572-13.0145zm42.013 3.7658h8.031c-.887 5.7597-5.206 9.2487-11.686 9.2487-7.642 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.317-13.0143 12.516-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.319 4.5965 1.773 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm23.4 16.7244v10.799h-8.694v-33.726h8.694v1.9937c1.163-1.3291 3.6-2.5475 6.148-2.5475 7.199 0 11.131 5.8703 11.131 13.0143 0 7.0886-3.932 13.0145-11.131 13.0145-2.548 0-4.985-1.219-6.148-2.548zm0-13.7893v6.5902c.665 1.3845 2.16 2.326 3.822 2.326 2.99 0 4.762-2.3814 4.762-5.5934s-1.772-5.6488-4.762-5.6488c-1.717 0-3.157.9969-3.822 2.326zm21.892 7.1994v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.206v6.7564h-5.206v8.307c0 1.9383.941 2.769 2.658 2.769.942 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm39.458 8.5839h-8.363v-1.274c-.83.831-3.322 1.717-5.981 1.717-4.928 0-9.082-2.769-9.082-8.0301 0-4.818 4.154-7.9193 9.581-7.9193 2.049 0 4.486.6646 5.482 1.3845v-1.606c0-1.606-.941-2.9905-3.045-2.9905-1.606 0-2.548.7199-2.936 1.8275h-8.196c.72-4.8181 4.984-8.6393 11.408-8.6393 7.089 0 11.132 3.7659 11.132 10.2453zm-8.363-6.9779v-1.4399c-.553-1.0522-2.049-1.7167-3.655-1.7167-1.716 0-3.433.7199-3.433 2.3813 0 1.7168 1.717 2.4367 3.433 2.4367 1.606 0 3.102-.6645 3.655-1.6614zm20.742 4.9839v1.994h-8.694v-35.997h8.694v13.0697c1.163-1.3291 3.6-2.5475 6.148-2.5475 7.199 0 11.131 5.8149 11.131 13.0143s-3.932 13.0145-11.131 13.0145c-2.548 0-4.985-1.219-6.148-2.548zm0-13.7893v6.5902c.665 1.3845 2.105 2.326 3.822 2.326 2.99 0 4.762-2.3814 4.762-5.5934s-1.772-5.6488-4.762-5.6488c-1.662 0-3.157.9969-3.822 2.326zm28.759-20.2137v35.997h-8.695v-35.997zm19.172 27.3023h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.516-13.0143 7.642 0 11.962 5.095 11.962 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.434 1.2737-3.988 3.5997h7.532c-.111-2.0491-1.384-3.5997-3.544-3.5997z" fill="#d30001"/></svg>
|
||||
</header>
|
||||
<article>
|
||||
<p><strong>Your browser is not supported.</strong><br> Please upgrade your browser to continue.</p>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
169
public/422.html
169
public/422.html
File diff suppressed because one or more lines are too long
168
public/500.html
168
public/500.html
File diff suppressed because one or more lines are too long
BIN
public/icon.png
Normal file
BIN
public/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
3
public/icon.svg
Normal file
3
public/icon.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="256" cy="256" r="256" fill="red"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 122 B |
|
|
@ -13,44 +13,6 @@ RSpec.describe Stat, type: :model do
|
|||
let(:year) { 2021 }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
describe '.year_cities_and_countries' do
|
||||
subject { described_class.year_cities_and_countries(year, user) }
|
||||
|
||||
let(:timestamp) { DateTime.new(year, 1, 1, 0, 0, 0) }
|
||||
|
||||
before do
|
||||
stub_const('MIN_MINUTES_SPENT_IN_CITY', 60)
|
||||
end
|
||||
|
||||
context 'when there are points' do
|
||||
let!(:points) do
|
||||
[
|
||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp:),
|
||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 10.minutes),
|
||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 20.minutes),
|
||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 30.minutes),
|
||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 40.minutes),
|
||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 50.minutes),
|
||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 60.minutes),
|
||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 70.minutes),
|
||||
create(:point, user:, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 80.minutes),
|
||||
create(:point, user:, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 90.minutes)
|
||||
]
|
||||
end
|
||||
|
||||
it 'returns countries and cities' do
|
||||
# User spent only 20 minutes in Brugges, so it should not be included
|
||||
expect(subject).to eq(countries: 2, cities: 1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are no points' do
|
||||
it 'returns countries and cities' do
|
||||
expect(subject).to eq(countries: 0, cities: 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#distance_by_day' do
|
||||
subject { stat.distance_by_day }
|
||||
|
||||
|
|
|
|||
|
|
@ -117,10 +117,8 @@ RSpec.describe User, type: :model do
|
|||
describe '#years_tracked' do
|
||||
let!(:points) { create_list(:point, 3, user:, timestamp: DateTime.new(2024, 1, 1, 5, 0, 0)) }
|
||||
|
||||
subject { user.years_tracked }
|
||||
|
||||
it 'returns years tracked' do
|
||||
expect(subject).to eq([2024])
|
||||
expect(user.years_tracked).to eq([{ year: 2024, months: ['Jan'] }])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ RSpec.describe '/api/v1/areas', type: :request do
|
|||
|
||||
describe 'GET /index' do
|
||||
it 'renders a successful response' do
|
||||
get api_v1_areas_url(api_key: user.api_key)
|
||||
get api_v1_areas_url, headers: { 'Authorization' => "Bearer #{user.api_key}" }
|
||||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
|
|
@ -20,12 +20,14 @@ RSpec.describe '/api/v1/areas', type: :request do
|
|||
|
||||
it 'creates a new Area' do
|
||||
expect do
|
||||
post api_v1_areas_url(api_key: user.api_key), params: { area: valid_attributes }
|
||||
post api_v1_areas_url, headers: { 'Authorization' => "Bearer #{user.api_key}" },
|
||||
params: { area: valid_attributes }
|
||||
end.to change(Area, :count).by(1)
|
||||
end
|
||||
|
||||
it 'redirects to the created api_v1_area' do
|
||||
post api_v1_areas_url(api_key: user.api_key), params: { area: valid_attributes }
|
||||
post api_v1_areas_url, headers: { 'Authorization' => "Bearer #{user.api_key}" },
|
||||
params: { area: valid_attributes }
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
end
|
||||
|
|
@ -38,12 +40,15 @@ RSpec.describe '/api/v1/areas', type: :request do
|
|||
|
||||
it 'does not create a new Area' do
|
||||
expect do
|
||||
post api_v1_areas_url(api_key: user.api_key), params: { area: invalid_attributes }
|
||||
post api_v1_areas_url, headers: { 'Authorization' => "Bearer #{user.api_key}" },
|
||||
params: { area: invalid_attributes }
|
||||
end.to change(Area, :count).by(0)
|
||||
end
|
||||
|
||||
it 'renders a response with 422 status' do
|
||||
post api_v1_areas_url(api_key: user.api_key), params: { area: invalid_attributes }
|
||||
post api_v1_areas_url, headers: { 'Authorization' => "Bearer #{user.api_key}" },
|
||||
params: { area: invalid_attributes }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
|
@ -56,14 +61,16 @@ RSpec.describe '/api/v1/areas', type: :request do
|
|||
let(:new_attributes) { attributes_for(:area).merge(name: 'New Name') }
|
||||
|
||||
it 'updates the requested api_v1_area' do
|
||||
patch api_v1_area_url(area, api_key: user.api_key), params: { area: new_attributes }
|
||||
patch api_v1_area_url(area), headers: { 'Authorization' => "Bearer #{user.api_key}" },
|
||||
params: { area: new_attributes }
|
||||
area.reload
|
||||
|
||||
expect(area.reload.name).to eq('New Name')
|
||||
end
|
||||
|
||||
it 'redirects to the api_v1_area' do
|
||||
patch api_v1_area_url(area, api_key: user.api_key), params: { area: new_attributes }
|
||||
patch api_v1_area_url(area), headers: { 'Authorization' => "Bearer #{user.api_key}" },
|
||||
params: { area: new_attributes }
|
||||
area.reload
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
|
@ -75,7 +82,8 @@ RSpec.describe '/api/v1/areas', type: :request do
|
|||
let(:invalid_attributes) { attributes_for(:area, name: nil) }
|
||||
|
||||
it 'renders a response with 422 status' do
|
||||
patch api_v1_area_url(area, api_key: user.api_key), params: { area: invalid_attributes }
|
||||
patch api_v1_area_url(area), headers: { 'Authorization' => "Bearer #{user.api_key}" },
|
||||
params: { area: invalid_attributes }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
|
@ -87,12 +95,12 @@ RSpec.describe '/api/v1/areas', type: :request do
|
|||
|
||||
it 'destroys the requested api_v1_area' do
|
||||
expect do
|
||||
delete api_v1_area_url(area, api_key: user.api_key)
|
||||
delete api_v1_area_url(area), headers: { 'Authorization' => "Bearer #{user.api_key}" }
|
||||
end.to change(Area, :count).by(-1)
|
||||
end
|
||||
|
||||
it 'redirects to the api_v1_areas list' do
|
||||
delete api_v1_area_url(area, api_key: user.api_key)
|
||||
delete api_v1_area_url(area), headers: { 'Authorization' => "Bearer #{user.api_key}" }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
|
|
|||
17
spec/requests/api/v1/countries/visited_cities_spec.rb
Normal file
17
spec/requests/api/v1/countries/visited_cities_spec.rb
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::Countries::VisitedCities', type: :request do
|
||||
describe 'GET /index' do
|
||||
let(:user) { create(:user) }
|
||||
let(:start_at) { '2023-01-01' }
|
||||
let(:end_at) { '2023-12-31' }
|
||||
|
||||
it 'returns visited cities' do
|
||||
get "/api/v1/countries/visited_cities?api_key=#{user.api_key}&start_at=#{start_at}&end_at=#{end_at}"
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
15
spec/requests/api/v1/points/tracked_months_spec.rb
Normal file
15
spec/requests/api/v1/points/tracked_months_spec.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::Points::TrackedMonths', type: :request do
|
||||
describe 'GET /index' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it 'returns tracked months' do
|
||||
get "/api/v1/points/tracked_months?api_key=#{user.api_key}"
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -49,7 +49,7 @@ RSpec.describe '/exports', type: :request do
|
|||
expect(response).to redirect_to(exports_url)
|
||||
end
|
||||
|
||||
it 'enqeuues a job to process the export' do
|
||||
it 'enqueues a job to process the export' do
|
||||
ActiveJob::Base.queue_adapter = :test
|
||||
|
||||
expect { post exports_url, params: }.to have_enqueued_job(ExportJob)
|
||||
|
|
|
|||
|
|
@ -27,12 +27,10 @@ RSpec.describe '/stats', type: :request do
|
|||
end
|
||||
|
||||
context 'when user is signed in' do
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before { sign_in user }
|
||||
|
||||
describe 'GET /index' do
|
||||
it 'renders a successful response' do
|
||||
get stats_url
|
||||
|
|
@ -54,15 +52,37 @@ RSpec.describe '/stats', type: :request do
|
|||
describe 'POST /update' do
|
||||
let(:stat) { create(:stat, user:, year: 2024) }
|
||||
|
||||
it 'enqueues Stats::CalculatingJob for each tracked year and month' do
|
||||
allow(user).to receive(:years_tracked).and_return([2024])
|
||||
context 'when updating a specific month' do
|
||||
it 'enqueues Stats::CalculatingJob for the given year and month' do
|
||||
put update_year_month_stats_url(year: '2024', month: '1')
|
||||
|
||||
post stats_url
|
||||
|
||||
(1..12).each do |month|
|
||||
expect(Stats::CalculatingJob).to have_been_enqueued.with(user.id, 2024, month)
|
||||
expect(Stats::CalculatingJob).to have_been_enqueued.with(user.id, '2024', '1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when updating the whole year' do
|
||||
it 'enqueues Stats::CalculatingJob for each month of the year' do
|
||||
put update_year_month_stats_url(year: '2024', month: 'all')
|
||||
|
||||
(1..12).each do |month|
|
||||
expect(Stats::CalculatingJob).to have_been_enqueued.with(user.id, '2024', month)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /update_all' do
|
||||
let(:stat) { create(:stat, user:, year: 2024) }
|
||||
|
||||
it 'enqueues Stats::CalculatingJob for each tracked year and month' do
|
||||
allow(user).to receive(:years_tracked).and_return([{ year: 2024, months: %w[Jan Feb] }])
|
||||
|
||||
put update_all_stats_url
|
||||
|
||||
expect(Stats::CalculatingJob).to have_been_enqueued.with(user.id, 2024, 1)
|
||||
expect(Stats::CalculatingJob).to have_been_enqueued.with(user.id, 2024, 2)
|
||||
expect(Stats::CalculatingJob).to_not have_been_enqueued.with(user.id, 2024, 3)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -36,13 +36,18 @@ RSpec.describe CountriesAndCities do
|
|||
it 'returns countries and cities' do
|
||||
expect(countries_and_cities).to eq(
|
||||
[
|
||||
{
|
||||
cities: [{ city: 'Berlin', points: 8, timestamp: 1609463400, stayed_for: 70 }],
|
||||
country: 'Germany'
|
||||
},
|
||||
{
|
||||
cities: [], country: 'Belgium'
|
||||
}
|
||||
CountriesAndCities::CountryData.new(
|
||||
country: 'Germany',
|
||||
cities: [
|
||||
CountriesAndCities::CityData.new(
|
||||
city: 'Berlin', points: 8, timestamp: 1_609_463_400, stayed_for: 70
|
||||
)
|
||||
]
|
||||
),
|
||||
CountriesAndCities::CountryData.new(
|
||||
country: 'Belgium',
|
||||
cities: []
|
||||
)
|
||||
]
|
||||
)
|
||||
end
|
||||
|
|
@ -62,12 +67,14 @@ RSpec.describe CountriesAndCities do
|
|||
it 'returns countries and cities' do
|
||||
expect(countries_and_cities).to eq(
|
||||
[
|
||||
{
|
||||
cities: [], country: 'Germany'
|
||||
},
|
||||
{
|
||||
cities: [], country: 'Belgium'
|
||||
}
|
||||
CountriesAndCities::CountryData.new(
|
||||
country: 'Germany',
|
||||
cities: []
|
||||
),
|
||||
CountriesAndCities::CountryData.new(
|
||||
country: 'Belgium',
|
||||
cities: []
|
||||
)
|
||||
]
|
||||
)
|
||||
end
|
||||
|
|
|
|||
96
spec/swagger/api/v1/countries/visited_cities_spec.rb
Normal file
96
spec/swagger/api/v1/countries/visited_cities_spec.rb
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'swagger_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::Countries::VisitedCities', type: :request do
|
||||
path '/api/v1/countries/visited_cities' do
|
||||
get 'Get visited cities by date range' do
|
||||
tags 'Countries'
|
||||
description 'Returns a list of visited cities and countries based on tracked points within the specified date range'
|
||||
produces 'application/json'
|
||||
|
||||
parameter name: :api_key, in: :query, type: :string, required: true
|
||||
parameter name: :start_at,
|
||||
in: :query,
|
||||
type: :string,
|
||||
format: 'date-time',
|
||||
required: true,
|
||||
description: 'Start date in YYYY-MM-DD format',
|
||||
example: '2023-01-01'
|
||||
|
||||
parameter name: :end_at,
|
||||
in: :query,
|
||||
type: :string,
|
||||
format: 'date-time',
|
||||
required: true,
|
||||
description: 'End date in YYYY-MM-DD format',
|
||||
example: '2023-12-31'
|
||||
|
||||
response '200', 'cities found' do
|
||||
schema type: :object,
|
||||
properties: {
|
||||
data: {
|
||||
type: :array,
|
||||
description: 'Array of countries and their visited cities',
|
||||
items: {
|
||||
type: :object,
|
||||
properties: {
|
||||
country: {
|
||||
type: :string,
|
||||
example: 'Germany'
|
||||
},
|
||||
cities: {
|
||||
type: :array,
|
||||
items: {
|
||||
type: :object,
|
||||
properties: {
|
||||
city: {
|
||||
type: :string,
|
||||
example: 'Berlin'
|
||||
},
|
||||
points: {
|
||||
type: :integer,
|
||||
example: 4394,
|
||||
description: 'Number of points in the city'
|
||||
},
|
||||
timestamp: {
|
||||
type: :integer,
|
||||
example: 1_724_868_369,
|
||||
description: 'Timestamp of the last point in the city in seconds since Unix epoch'
|
||||
},
|
||||
stayed_for: {
|
||||
type: :integer,
|
||||
example: 24_490,
|
||||
description: 'Number of minutes the user stayed in the city'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let(:start_at) { '2023-01-01' }
|
||||
let(:end_at) { '2023-12-31' }
|
||||
let(:api_key) { create(:user).api_key }
|
||||
run_test!
|
||||
end
|
||||
|
||||
response '400', 'bad request - missing parameters' do
|
||||
schema type: :object,
|
||||
properties: {
|
||||
error: {
|
||||
type: :string,
|
||||
example: 'Missing required parameters: start_at, end_at'
|
||||
}
|
||||
}
|
||||
|
||||
let(:start_at) { nil }
|
||||
let(:end_at) { nil }
|
||||
let(:api_key) { create(:user).api_key }
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
39
spec/swagger/api/v1/points/tracked_months_controller_spec.rb
Normal file
39
spec/swagger/api/v1/points/tracked_months_controller_spec.rb
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'swagger_helper'
|
||||
|
||||
describe 'Points Tracked Months API', type: :request do
|
||||
path '/api/v1/points/tracked_months' do
|
||||
get 'Returns list of tracked years and months' do
|
||||
tags 'Points'
|
||||
produces 'application/json'
|
||||
parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key'
|
||||
response '200', 'years and months found' do
|
||||
schema type: :array,
|
||||
items: {
|
||||
type: :object,
|
||||
properties: {
|
||||
year: { type: :integer, description: 'Year in YYYY format' },
|
||||
months: {
|
||||
type: :array,
|
||||
items: { type: :string, description: 'Three-letter month abbreviation' }
|
||||
}
|
||||
},
|
||||
required: %w[year months]
|
||||
},
|
||||
example: [{
|
||||
year: 2024,
|
||||
months: %w[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec]
|
||||
}]
|
||||
|
||||
let(:api_key) { create(:user).api_key }
|
||||
run_test!
|
||||
end
|
||||
|
||||
response '401', 'unauthorized' do
|
||||
let(:api_key) { 'invalid' }
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -106,6 +106,84 @@ paths:
|
|||
responses:
|
||||
'200':
|
||||
description: area deleted
|
||||
"/api/v1/countries/visited_cities":
|
||||
get:
|
||||
summary: Get visited cities by date range
|
||||
tags:
|
||||
- Countries
|
||||
description: Returns a list of visited cities and countries based on tracked
|
||||
points within the specified date range
|
||||
parameters:
|
||||
- name: api_key
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: start_at
|
||||
in: query
|
||||
format: date-time
|
||||
required: true
|
||||
description: Start date in YYYY-MM-DD format
|
||||
example: '2023-01-01'
|
||||
schema:
|
||||
type: string
|
||||
- name: end_at
|
||||
in: query
|
||||
format: date-time
|
||||
required: true
|
||||
description: End date in YYYY-MM-DD format
|
||||
example: '2023-12-31'
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: cities found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
description: Array of countries and their visited cities
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
country:
|
||||
type: string
|
||||
example: Germany
|
||||
cities:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
city:
|
||||
type: string
|
||||
example: Berlin
|
||||
points:
|
||||
type: integer
|
||||
example: 4394
|
||||
description: Number of points in the city
|
||||
timestamp:
|
||||
type: integer
|
||||
example: 1724868369
|
||||
description: Timestamp of the last point in the city
|
||||
in seconds since Unix epoch
|
||||
stayed_for:
|
||||
type: integer
|
||||
example: 24490
|
||||
description: Number of minutes the user stayed in
|
||||
the city
|
||||
'400':
|
||||
description: bad request - missing parameters
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
example: 'Missing required parameters: start_at, end_at'
|
||||
"/api/v1/health":
|
||||
get:
|
||||
summary: Retrieves application status
|
||||
|
|
@ -460,6 +538,56 @@ paths:
|
|||
- photoprism
|
||||
'404':
|
||||
description: photo not found
|
||||
"/api/v1/points/tracked_months":
|
||||
get:
|
||||
summary: Returns list of tracked years and months
|
||||
tags:
|
||||
- Points
|
||||
parameters:
|
||||
- name: api_key
|
||||
in: query
|
||||
required: true
|
||||
description: API Key
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: years and months found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
year:
|
||||
type: integer
|
||||
description: Year in YYYY format
|
||||
months:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Three-letter month abbreviation
|
||||
required:
|
||||
- year
|
||||
- months
|
||||
example:
|
||||
- year: 2024
|
||||
months:
|
||||
- Jan
|
||||
- Feb
|
||||
- Mar
|
||||
- Apr
|
||||
- May
|
||||
- Jun
|
||||
- Jul
|
||||
- Aug
|
||||
- Sep
|
||||
- Oct
|
||||
- Nov
|
||||
- Dec
|
||||
'401':
|
||||
description: unauthorized
|
||||
"/api/v1/points":
|
||||
get:
|
||||
summary: Retrieves all points
|
||||
|
|
|
|||
Loading…
Reference in a new issue