From 4e7b6f1ac22ca93a04c078f09ac63c48d121fa14 Mon Sep 17 00:00:00 2001 From: tigattack <10629864+tigattack@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:56:54 +0000 Subject: [PATCH 01/53] fix gem paths in compose.yml & k8s instructions --- docker-compose.yml | 4 ++-- docs/How_to_install_Dawarich_in_k8s.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 034172a5..b6fd98d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,7 +38,7 @@ services: 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: @@ -96,7 +96,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: diff --git a/docs/How_to_install_Dawarich_in_k8s.md b/docs/How_to_install_Dawarich_in_k8s.md index 890a4b42..9ea55edc 100644 --- a/docs/How_to_install_Dawarich_in_k8s.md +++ b/docs/How_to_install_Dawarich_in_k8s.md @@ -140,8 +140,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 @@ -196,7 +196,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 From 9a2267abf4e65d09932dc6d98194309e5a9622c1 Mon Sep 17 00:00:00 2001 From: "duck." Date: Tue, 10 Dec 2024 16:10:21 +0000 Subject: [PATCH 02/53] Bind to both IPv6 and IPv4 interfaces by default As discussed in https://github.com/Freika/dawarich/issues/498 - not tested as there appears to be no scaffolding to test this functionality? --- Procfile.dev | 2 +- Procfile.prometheus.dev | 4 ++-- config/puma.rb | 2 +- docs/How_to_install_Dawarich_in_k8s.md | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Procfile.dev b/Procfile.dev index e6096674..a0f88c84 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1 +1 @@ -web: bin/rails server -p 3000 -b 0.0.0.0 +web: bin/rails server -p 3000 -b :: diff --git a/Procfile.prometheus.dev b/Procfile.prometheus.dev index 965e5e25..71fe0374 100644 --- a/Procfile.prometheus.dev +++ b/Procfile.prometheus.dev @@ -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 :: \ No newline at end of file diff --git a/config/puma.rb b/config/puma.rb index d3094caa..e4a6a107 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -50,7 +50,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 diff --git a/docs/How_to_install_Dawarich_in_k8s.md b/docs/How_to_install_Dawarich_in_k8s.md index 890a4b42..42839bcf 100644 --- a/docs/How_to_install_Dawarich_in_k8s.md +++ b/docs/How_to_install_Dawarich_in_k8s.md @@ -7,6 +7,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 @@ -149,7 +150,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" From 5cde596884b6d8dfba72634b640c15bd3cebd1b7 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Wed, 11 Dec 2024 17:14:26 +0100 Subject: [PATCH 03/53] Rework countries and cities service --- app/jobs/stats/calculating_job.rb | 7 ++- app/services/countries_and_cities.rb | 62 ++++++++----------- app/views/shared/_right_sidebar.html.erb | 3 - ...11170110_add_index_to_points_timestamp.rb} | 0 4 files changed, 31 insertions(+), 41 deletions(-) rename db/migrate/{[timestamp]_add_index_to_points_timestamp.rb => 20241211170110_add_index_to_points_timestamp.rb} (100%) diff --git a/app/jobs/stats/calculating_job.rb b/app/jobs/stats/calculating_job.rb index 26f4756e..f7a5bc73 100644 --- a/app/jobs/stats/calculating_job.rb +++ b/app/jobs/stats/calculating_job.rb @@ -13,11 +13,14 @@ class Stats::CalculatingJob < ApplicationJob 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: #{year}-#{month}", + content: "Stats updated for #{year}-#{month}" ).call end diff --git a/app/services/countries_and_cities.rb b/app/services/countries_and_cities.rb index 026484d1..efb36511 100644 --- a/app/services/countries_and_cities.rb +++ b/app/services/countries_and_cities.rb @@ -1,56 +1,46 @@ # frozen_string_literal: true class CountriesAndCities + CityStats = Struct.new(:points, :last_timestamp, :stayed_for, keyword_init: true) + 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 do |city_points| + timestamps = city_points.map(&:timestamp) + build_city_data(city_points.first.city, city_points.size, timestamps) + end + .values 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 build_city_data(city, points_count, timestamps) + CityData.new( + city: city, + points: points_count, + timestamp: timestamps.max, + stayed_for: calculate_duration_in_minutes(timestamps) + ) 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 - 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 diff --git a/app/views/shared/_right_sidebar.html.erb b/app/views/shared/_right_sidebar.html.erb index 797adaeb..a0f6b2d6 100644 --- a/app/views/shared/_right_sidebar.html.erb +++ b/app/views/shared/_right_sidebar.html.erb @@ -31,10 +31,7 @@ <% if REVERSE_GEOCODING_ENABLED && @countries_and_cities&.any? %>
-

Countries and cities

<% @countries_and_cities.each do |country| %> - <% next if country[:cities].empty? %> -

<%= country[:country] %> (<%= country[:cities].count %> cities)

diff --git a/db/migrate/[timestamp]_add_index_to_points_timestamp.rb b/db/migrate/20241211170110_add_index_to_points_timestamp.rb similarity index 100% rename from db/migrate/[timestamp]_add_index_to_points_timestamp.rb rename to db/migrate/20241211170110_add_index_to_points_timestamp.rb From a4db806d2934303f75f3dd87356105c2a7c28746 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Wed, 11 Dec 2024 20:34:49 +0100 Subject: [PATCH 04/53] Add togglable panel for months and years navigation --- .../v1/points/tracked_months_controller.rb | 7 + .../api/v1/points/tracked_months_helper.rb | 2 + app/javascript/controllers/maps_controller.js | 278 ++++++++++++++++++ app/models/user.rb | 11 +- app/views/map/index.html.erb | 6 +- config/routes.rb | 4 + .../api/v1/points/tracked_months_spec.rb | 7 + 7 files changed, 306 insertions(+), 9 deletions(-) create mode 100644 app/controllers/api/v1/points/tracked_months_controller.rb create mode 100644 app/helpers/api/v1/points/tracked_months_helper.rb create mode 100644 spec/requests/api/v1/points/tracked_months_spec.rb diff --git a/app/controllers/api/v1/points/tracked_months_controller.rb b/app/controllers/api/v1/points/tracked_months_controller.rb new file mode 100644 index 00000000..cd430879 --- /dev/null +++ b/app/controllers/api/v1/points/tracked_months_controller.rb @@ -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 diff --git a/app/helpers/api/v1/points/tracked_months_helper.rb b/app/helpers/api/v1/points/tracked_months_helper.rb new file mode 100644 index 00000000..7be4d70a --- /dev/null +++ b/app/helpers/api/v1/points/tracked_months_helper.rb @@ -0,0 +1,2 @@ +module Api::V1::Points::TrackedMonthsHelper +end diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index d0bb046d..61aa970c 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -171,12 +171,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'); + + console.log('Initial state check:', { + isPanelOpen, + hasDateParams, + localStorageValue: localStorage.getItem('mapPanelOpen') + }); + + if (isPanelOpen || hasDateParams) { + console.log('Opening panel because:', { isPanelOpen, hasDateParams }); + this.toggleRightPanel(); + } } disconnect() { if (this.handleDeleteClick) { document.removeEventListener('click', this.handleDeleteClick); } + // Store panel state before disconnecting + if (this.rightPanel) { + const finalState = this.rightPanel._map ? 'true' : 'false'; + console.log('Disconnecting, saving panel state:', finalState); + localStorage.setItem('mapPanelOpen', finalState); + } this.map.remove(); } @@ -902,4 +927,257 @@ export default class extends Controller { this.photoMarkers.addLayer(marker); } + + addTogglePanelButton() { + const TogglePanelControl = L.Control.extend({ + onAdd: (map) => { + const button = L.DomUtil.create('button', 'toggle-panel-button'); + button.innerHTML = 'πŸ“Š'; // You can use any icon or text here + + // Style the button similarly to the settings button + 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() { + console.log('toggleRightPanel called, current state:', { + hasPanel: !!this.rightPanel, + isPanelOnMap: this.rightPanel?._map, + localStorageValue: localStorage.getItem('mapPanelOpen') + }); + + if (this.rightPanel) { + if (this.rightPanel._map) { + console.log('Removing panel from map'); + this.map.removeControl(this.rightPanel); + localStorage.setItem('mapPanelOpen', 'false'); + } else { + console.log('Adding existing panel to map'); + this.map.addControl(this.rightPanel); + localStorage.setItem('mapPanelOpen', 'true'); + } + console.log('After toggle:', { + isPanelOnMap: this.rightPanel._map, + localStorageValue: localStorage.getItem('mapPanelOpen') + }); + return; + } + + console.log('Creating new panel'); + 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() : null; + const currentMonth = startDate ? allMonths[new Date(startDate).getMonth()] : null; + + // Initially create select with loading state and current year if available + div.innerHTML = ` +
+
+ + +
+ ${allMonths.map(month => ` + + ${month} + + `).join('')} +
+
+
+ `; + + fetch(`/api/v1/points/tracked_months?api_key=${this.apiKey}`) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .then(yearsData => { + const yearSelect = document.getElementById('year-select'); + + if (!Array.isArray(yearsData) || yearsData.length === 0) { + yearSelect.innerHTML = ''; + 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 ` + + `; + }) + .join(''); + + yearSelect.innerHTML = ` + + ${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')) : null; + const endDate = urlParams.get('end_at') ? new Date(urlParams.get('end_at')) : null; + + allMonths.forEach((month, index) => { + const monthLink = div.querySelector(`a[data-month-name="${month}"]`); + if (!monthLink) return; + + // 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 || '[]'); + console.log('Year changed to:', selectedYear); + 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 => { + console.error('Error fetching years:', error); + const yearSelect = document.getElementById('year-select'); + yearSelect.innerHTML = ''; + }); + + 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.minWidth = '300px'; + div.style.maxHeight = '80vh'; + div.style.overflowY = 'auto'; + + L.DomEvent.disableClickPropagation(div); + + return div; + }; + + // Only add the panel if we should show it + const urlParams = new URLSearchParams(window.location.search); + const isPanelOpen = localStorage.getItem('mapPanelOpen') === 'true'; + const hasDateParams = urlParams.has('start_at') && urlParams.has('end_at'); + + console.log('Deciding whether to show new panel:', { + isPanelOpen, + hasDateParams, + localStorageValue: localStorage.getItem('mapPanelOpen') + }); + + if (isPanelOpen || hasDateParams) { + console.log('Adding new panel to map'); + this.map.addControl(this.rightPanel); + localStorage.setItem('mapPanelOpen', 'true'); + } else { + console.log('Not adding new panel to map'); + localStorage.setItem('mapPanelOpen', 'false'); + } + + console.log('Final panel state:', { + isPanelOnMap: this.rightPanel._map, + localStorageValue: localStorage.getItem('mapPanelOpen') + }); + } + + chunk(array, size) { + const chunked = []; + for (let i = 0; i < array.length; i += size) { + chunked.push(array.slice(i, i + size)); + } + return chunked; + } } + diff --git a/app/models/user.rb b/app/models/user.rb index a2d595cb..796cf738 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -70,10 +70,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 diff --git a/app/views/map/index.html.erb b/app/views/map/index.html.erb index 75788972..7e36c225 100644 --- a/app/views/map/index.html.erb +++ b/app/views/map/index.html.erb @@ -1,7 +1,7 @@ <% content_for :title, 'Map' %>
-
+
<%= form_with url: map_path(import_id: params[:import_id]), method: :get do |f| %>
@@ -58,10 +58,6 @@
- -
- <%= render 'shared/right_sidebar' %> -
<%= render 'map/settings_modals' %> diff --git a/config/routes.rb b/config/routes.rb index 2c40e93d..8f179cfa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -80,6 +80,10 @@ Rails.application.routes.draw do resources :borders, only: :index end + namespace :points do + get 'tracked_months', to: 'tracked_months#index' + end + resources :photos, only: %i[index] do member do get 'thumbnail', constraints: { id: %r{[^/]+} } diff --git a/spec/requests/api/v1/points/tracked_months_spec.rb b/spec/requests/api/v1/points/tracked_months_spec.rb new file mode 100644 index 00000000..6abcf28b --- /dev/null +++ b/spec/requests/api/v1/points/tracked_months_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +RSpec.describe "Api::V1::Points::TrackedMonths", type: :request do + describe "GET /index" do + pending "add some examples (or delete) #{__FILE__}" + end +end From a1368b2e6818e260503d9a67f4d6e983a4a39039 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Wed, 11 Dec 2024 20:41:51 +0100 Subject: [PATCH 05/53] Add link to whole year --- app/javascript/controllers/maps_controller.js | 60 ++++++++++++++++--- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 61aa970c..2d2f57a0 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -992,18 +992,29 @@ export default class extends Controller { // 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() : null; - const currentMonth = startDate ? allMonths[new Date(startDate).getMonth()] : null; + 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 = `
- +
+ + + Whole year + +
${allMonths.map(month => ` @@ -1060,8 +1071,8 @@ export default class extends Controller { 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')) : null; - const endDate = urlParams.get('end_at') ? new Date(urlParams.get('end_at')) : null; + 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}"]`); @@ -1115,6 +1126,14 @@ export default class extends Controller { 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); + console.log('Year changed to:', selectedYear); updateMonthLinks(selectedYear, availableMonths); }); @@ -1179,5 +1198,28 @@ export default class extends Controller { } 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)}`; + } } From cab70839b99746bc6d37593348019625b55f11f4 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Wed, 11 Dec 2024 21:21:24 +0100 Subject: [PATCH 06/53] Color buttons a bit --- app/javascript/controllers/maps_controller.js | 48 +++---------------- ...211170110_add_index_to_points_timestamp.rb | 9 ---- 2 files changed, 7 insertions(+), 50 deletions(-) delete mode 100644 db/migrate/20241211170110_add_index_to_points_timestamp.rb diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 2d2f57a0..d4ed8c3a 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -180,14 +180,7 @@ export default class extends Controller { const isPanelOpen = localStorage.getItem('mapPanelOpen') === 'true'; const hasDateParams = urlParams.has('start_at') && urlParams.has('end_at'); - console.log('Initial state check:', { - isPanelOpen, - hasDateParams, - localStorageValue: localStorage.getItem('mapPanelOpen') - }); - if (isPanelOpen || hasDateParams) { - console.log('Opening panel because:', { isPanelOpen, hasDateParams }); this.toggleRightPanel(); } } @@ -199,7 +192,7 @@ export default class extends Controller { // Store panel state before disconnecting if (this.rightPanel) { const finalState = this.rightPanel._map ? 'true' : 'false'; - console.log('Disconnecting, saving panel state:', finalState); + localStorage.setItem('mapPanelOpen', finalState); } this.map.remove(); @@ -932,9 +925,8 @@ export default class extends Controller { const TogglePanelControl = L.Control.extend({ onAdd: (map) => { const button = L.DomUtil.create('button', 'toggle-panel-button'); - button.innerHTML = 'πŸ“Š'; // You can use any icon or text here + button.innerHTML = 'πŸ“…'; - // Style the button similarly to the settings button button.style.backgroundColor = 'white'; button.style.width = '48px'; button.style.height = '48px'; @@ -959,30 +951,18 @@ export default class extends Controller { } toggleRightPanel() { - console.log('toggleRightPanel called, current state:', { - hasPanel: !!this.rightPanel, - isPanelOnMap: this.rightPanel?._map, - localStorageValue: localStorage.getItem('mapPanelOpen') - }); - if (this.rightPanel) { if (this.rightPanel._map) { - console.log('Removing panel from map'); this.map.removeControl(this.rightPanel); localStorage.setItem('mapPanelOpen', 'false'); } else { - console.log('Adding existing panel to map'); this.map.addControl(this.rightPanel); localStorage.setItem('mapPanelOpen', 'true'); } - console.log('After toggle:', { - isPanelOnMap: this.rightPanel._map, - localStorageValue: localStorage.getItem('mapPanelOpen') - }); + return; } - console.log('Creating new panel'); this.rightPanel = L.control({ position: 'topright' }); this.rightPanel.onAdd = () => { @@ -1011,7 +991,8 @@ export default class extends Controller { + class="btn btn-default" + style="color: rgb(116 128 255) !important;"> Whole year
@@ -1019,9 +1000,9 @@ export default class extends Controller {
${allMonths.map(month => ` + style="pointer-events: none; opacity: 0.6; color: rgb(116 128 255) !important;"> ${month} `).join('')} @@ -1134,7 +1115,6 @@ export default class extends Controller { const href = `map?end_at=${encodeURIComponent(endDate)}&start_at=${encodeURIComponent(startDate)}`; wholeYearLink.setAttribute('href', href); - console.log('Year changed to:', selectedYear); updateMonthLinks(selectedYear, availableMonths); }); @@ -1145,7 +1125,6 @@ export default class extends Controller { } }) .catch(error => { - console.error('Error fetching years:', error); const yearSelect = document.getElementById('year-select'); yearSelect.innerHTML = ''; }); @@ -1170,25 +1149,12 @@ export default class extends Controller { const isPanelOpen = localStorage.getItem('mapPanelOpen') === 'true'; const hasDateParams = urlParams.has('start_at') && urlParams.has('end_at'); - console.log('Deciding whether to show new panel:', { - isPanelOpen, - hasDateParams, - localStorageValue: localStorage.getItem('mapPanelOpen') - }); - if (isPanelOpen || hasDateParams) { - console.log('Adding new panel to map'); this.map.addControl(this.rightPanel); localStorage.setItem('mapPanelOpen', 'true'); } else { - console.log('Not adding new panel to map'); localStorage.setItem('mapPanelOpen', 'false'); } - - console.log('Final panel state:', { - isPanelOnMap: this.rightPanel._map, - localStorageValue: localStorage.getItem('mapPanelOpen') - }); } chunk(array, size) { diff --git a/db/migrate/20241211170110_add_index_to_points_timestamp.rb b/db/migrate/20241211170110_add_index_to_points_timestamp.rb deleted file mode 100644 index 8e4bc3fa..00000000 --- a/db/migrate/20241211170110_add_index_to_points_timestamp.rb +++ /dev/null @@ -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 From e7c393a7764533f3e759b53b905ac6f95dcf0ca4 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Wed, 11 Dec 2024 22:00:33 +0100 Subject: [PATCH 07/53] Show visited cities on map page --- .../v1/countries/visited_cities_controller.rb | 30 +++++++ .../api/v1/points/tracked_months_helper.rb | 2 - app/javascript/controllers/maps_controller.js | 88 ++++++++++++++++++- config/routes.rb | 1 + .../api/v1/countries/visited_cities_spec.rb | 7 ++ 5 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 app/controllers/api/v1/countries/visited_cities_controller.rb delete mode 100644 app/helpers/api/v1/points/tracked_months_helper.rb create mode 100644 spec/requests/api/v1/countries/visited_cities_spec.rb diff --git a/app/controllers/api/v1/countries/visited_cities_controller.rb b/app/controllers/api/v1/countries/visited_cities_controller.rb new file mode 100644 index 00000000..2b79ffd7 --- /dev/null +++ b/app/controllers/api/v1/countries/visited_cities_controller.rb @@ -0,0 +1,30 @@ +# 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 validate_params + missing_params = %i[start_at end_at].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(:start_at, :end_at) + end +end diff --git a/app/helpers/api/v1/points/tracked_months_helper.rb b/app/helpers/api/v1/points/tracked_months_helper.rb deleted file mode 100644 index 7be4d70a..00000000 --- a/app/helpers/api/v1/points/tracked_months_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module Api::V1::Points::TrackedMonthsHelper -end diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index d4ed8c3a..9170f534 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -958,8 +958,9 @@ export default class extends Controller { } else { this.map.addControl(this.rightPanel); localStorage.setItem('mapPanelOpen', 'true'); + // Fetch visited cities when panel is opened + this.fetchAndDisplayVisitedCities(); } - return; } @@ -1008,6 +1009,7 @@ export default class extends Controller { `).join('')}
+
`; @@ -1141,6 +1143,24 @@ export default class extends Controller { L.DomEvent.disableClickPropagation(div); + // Add container for visited cities + div.innerHTML += ` +
+

Visited cities

+
+

Loading visited places...

+
+
+ `; + + // 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(); + return div; }; @@ -1187,5 +1207,71 @@ export default class extends Controller { 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(); + + 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(); + this.displayVisitedCities(data.data); + } catch (error) { + console.error('Error fetching visited cities:', error); + const container = document.getElementById('visited-cities-list'); + if (container) { + container.innerHTML = '

Error loading visited places

'; + } + } + } + + displayVisitedCities(citiesData) { + const container = document.getElementById('visited-cities-list'); + if (!container) return; + + if (!citiesData || citiesData.length === 0) { + container.innerHTML = '

No places visited during this period

'; + return; + } + + const html = citiesData.map(country => ` +
+

${country.country}

+
    + ${country.cities.map(city => ` +
  • + ${city.city} + + (${new Date(city.timestamp * 1000).toLocaleDateString()}) + +
  • + `).join('')} +
+
+ `).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`; + } } diff --git a/config/routes.rb b/config/routes.rb index 8f179cfa..ebad44e5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -78,6 +78,7 @@ Rails.application.routes.draw do namespace :countries do resources :borders, only: :index + resources :visited_cities, only: :index end namespace :points do diff --git a/spec/requests/api/v1/countries/visited_cities_spec.rb b/spec/requests/api/v1/countries/visited_cities_spec.rb new file mode 100644 index 00000000..88441b3a --- /dev/null +++ b/spec/requests/api/v1/countries/visited_cities_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +RSpec.describe "Api::V1::Countries::VisitedCities", type: :request do + describe "GET /index" do + pending "add some examples (or delete) #{__FILE__}" + end +end From 0e384d99c144ab0c39d7a49b25af93d9233b8629 Mon Sep 17 00:00:00 2001 From: Sheya Bernstein Date: Fri, 13 Dec 2024 11:20:10 +0000 Subject: [PATCH 08/53] Add support for changing log level in development --- config/environments/development.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/environments/development.rb b/config/environments/development.rb index 43f8e399..e41cfed1 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -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? From cddbace10eb12932d49201c16a23859dafe6f12d Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Fri, 13 Dec 2024 13:21:04 +0100 Subject: [PATCH 09/53] Cache responses from api endpoints made from the map right panel --- app/javascript/controllers/maps_controller.js | 320 ++++++++++-------- 1 file changed, 176 insertions(+), 144 deletions(-) diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 9170f534..203c0d53 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -32,6 +32,8 @@ export default class extends Controller { settingsButtonAdded = false; layerControl = null; + visitedCitiesCache = new Map(); + trackedMonthsCache = null; connect() { console.log("Map controller connected"); @@ -180,8 +182,16 @@ export default class extends Controller { const isPanelOpen = localStorage.getItem('mapPanelOpen') === 'true'; const hasDateParams = urlParams.has('start_at') && urlParams.has('end_at'); - if (isPanelOpen || hasDateParams) { - this.toggleRightPanel(); + // 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'); + } } } @@ -191,8 +201,7 @@ export default class extends Controller { } // Store panel state before disconnecting if (this.rightPanel) { - const finalState = this.rightPanel._map ? 'true' : 'false'; - + const finalState = document.querySelector('.leaflet-right-panel').style.display !== 'none' ? 'true' : 'false'; localStorage.setItem('mapPanelOpen', finalState); } this.map.remove(); @@ -952,16 +961,17 @@ export default class extends Controller { toggleRightPanel() { if (this.rightPanel) { - if (this.rightPanel._map) { - this.map.removeControl(this.rightPanel); - localStorage.setItem('mapPanelOpen', 'false'); - } else { - this.map.addControl(this.rightPanel); - localStorage.setItem('mapPanelOpen', 'true'); - // Fetch visited cities when panel is opened - this.fetchAndDisplayVisitedCities(); + 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; } - return; } this.rightPanel = L.control({ position: 'topright' }); @@ -1004,7 +1014,7 @@ export default class extends Controller { 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;"> - ${month} + `).join('')}
@@ -1013,123 +1023,7 @@ export default class extends Controller { `; - fetch(`/api/v1/points/tracked_months?api_key=${this.apiKey}`) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return response.json(); - }) - .then(yearsData => { - const yearSelect = document.getElementById('year-select'); - - if (!Array.isArray(yearsData) || yearsData.length === 0) { - yearSelect.innerHTML = ''; - 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 ` - - `; - }) - .join(''); - - yearSelect.innerHTML = ` - - ${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; - - // 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 = ''; - }); + this.fetchAndDisplayTrackedMonths(div, currentYear, currentMonth, allMonths); div.style.backgroundColor = 'white'; div.style.padding = '10px'; @@ -1137,7 +1031,7 @@ export default class extends Controller { div.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)'; div.style.marginRight = '10px'; div.style.marginTop = '10px'; - div.style.minWidth = '300px'; + div.style.width = '300px'; div.style.maxHeight = '80vh'; div.style.overflowY = 'auto'; @@ -1148,7 +1042,7 @@ export default class extends Controller {

Visited cities

+ style="max-height: 300px; overflow-y: auto; overflow-x: auto; padding-right: 10px;">

Loading visited places...

@@ -1161,19 +1055,144 @@ export default class extends Controller { // 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; }; - // Only add the panel if we should show it - const urlParams = new URLSearchParams(window.location.search); - const isPanelOpen = localStorage.getItem('mapPanelOpen') === 'true'; - const hasDateParams = urlParams.has('start_at') && urlParams.has('end_at'); + this.map.addControl(this.rightPanel); + } - if (isPanelOpen || hasDateParams) { - this.map.addControl(this.rightPanel); - localStorage.setItem('mapPanelOpen', 'true'); - } else { - localStorage.setItem('mapPanelOpen', 'false'); + 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 = ''; + 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 ` + + `; + }) + .join(''); + + yearSelect.innerHTML = ` + + ${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 = ''; + console.error('Error fetching tracked months:', error); } } @@ -1213,6 +1232,15 @@ export default class extends Controller { 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: { @@ -1226,6 +1254,10 @@ export default class extends Controller { } 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); @@ -1246,11 +1278,11 @@ export default class extends Controller { } const html = citiesData.map(country => ` -
+

${country.country}

diff --git a/app/views/stats/_stat.html.erb b/app/views/stats/_stat.html.erb index 4309c3b0..f8e59e04 100644 --- a/app/views/stats/_stat.html.erb +++ b/app/views/stats/_stat.html.erb @@ -1,10 +1,16 @@
-

- <%= 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 %> -

+
+

+ <%= link_to map_url(timespan(stat.month, stat.year)), class: "underline hover:no-underline text-#{header_colors.sample}" do %> + <%= Date::MONTHNAMES[stat.month] %> + <% end %> +

+ +
+ <%= link_to '[Update]', update_year_month_stats_path(stat.year, stat.month), data: { turbo_method: :put }, class: 'text-sm text-gray-500 hover:underline' %> +
+

<%= stat.distance %><%= DISTANCE_UNIT %>

<% if REVERSE_GEOCODING_ENABLED %>
diff --git a/app/views/stats/index.html.erb b/app/views/stats/index.html.erb index ee3b33cd..d67037de 100644 --- a/app/views/stats/index.html.erb +++ b/app/views/stats/index.html.erb @@ -21,15 +21,18 @@ <% end %>
- <%= 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' %> -
+
<% @stats.each do |year, stats| %>
-

- <%= link_to year, "/stats/#{year}", class: 'underline hover:no-underline' %> - <%= link_to '[Map]', map_url(year_timespan(year)), class: 'underline hover:no-underline' %> +

+
+ <%= link_to year, "/stats/#{year}", class: 'underline hover:no-underline' %> + <%= link_to '[Map]', map_url(year_timespan(year)), class: 'underline hover:no-underline' %> +
+ <%= link_to '[Update]', update_year_month_stats_path(year, :all), data: { turbo_method: :put }, class: 'text-sm text-gray-500 hover:underline' %>

<% cache [current_user, 'year_distance_stat', year], skip_digest: true do %> diff --git a/config/routes.rb b/config/routes.rb index 4478d7db..d8926bd0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -41,10 +41,14 @@ Rails.application.routes.draw do 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] From d01e4f3b9e5cb84d89b24909b555b01676b858cc Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Fri, 20 Dec 2024 15:43:06 +0100 Subject: [PATCH 38/53] Update tests for stats requests --- app/models/user.rb | 2 -- config/routes.rb | 2 -- spec/requests/stats_spec.rb | 30 +++++++++++++++++++++++++----- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 796cf738..64e45425 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/config/routes.rb b/config/routes.rb index d8926bd0..8d28efde 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -57,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 diff --git a/spec/requests/stats_spec.rb b/spec/requests/stats_spec.rb index 224cb3b5..c7473bed 100644 --- a/spec/requests/stats_spec.rb +++ b/spec/requests/stats_spec.rb @@ -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,10 +52,32 @@ RSpec.describe '/stats', type: :request do describe 'POST /update' do let(:stat) { create(:stat, user:, year: 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') + + 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] }]) - post stats_url + 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) From 6c58a446ee9fa31e758d893e422c3b124b0c2aa5 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Fri, 20 Dec 2024 16:02:17 +0100 Subject: [PATCH 39/53] Support API key in Authorization header --- CHANGELOG.md | 12 ++++++++++++ app/controllers/api_controller.rb | 6 +++++- spec/requests/api/v1/areas_spec.rb | 28 ++++++++++++++++++---------- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdd8fea7..ec26ef69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added +- In addition to `api_key` parameter, `Authorization` header is now being used to authenticate API requests. #543 + +Example: + +``` +Authorization: Bearer YOUR_API_KEY +``` + +# 0.20.3 - 2024-12-20 + +### Added + - A button on a year stats card to update stats for the whole year. - A button on a month stats card to update stats for a specific month. - A confirmation alert on the Notifications page before deleting all notifications. diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index 934cdc6b..c193148e 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -13,7 +13,11 @@ 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 diff --git a/spec/requests/api/v1/areas_spec.rb b/spec/requests/api/v1/areas_spec.rb index 61874069..7be57513 100644 --- a/spec/requests/api/v1/areas_spec.rb +++ b/spec/requests/api/v1/areas_spec.rb @@ -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 From 6bdb1038143451b845f1194e93015542fdfddd31 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Fri, 20 Dec 2024 16:11:46 +0100 Subject: [PATCH 40/53] Expand map borders for New Zealanders --- CHANGELOG.md | 11 +++++------ app/javascript/controllers/maps_controller.js | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec26ef69..32c90c57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added +- A button on a year stats card to update stats for the whole year. +- A button on a month stats card to update stats for a specific month. +- A confirmation alert on the Notifications page before deleting all notifications. - In addition to `api_key` parameter, `Authorization` header is now being used to authenticate API requests. #543 Example: @@ -17,13 +20,9 @@ Example: Authorization: Bearer YOUR_API_KEY ``` -# 0.20.3 - 2024-12-20 +### Changed -### Added - -- A button on a year stats card to update stats for the whole year. -- A button on a month stats card to update stats for a specific month. -- A confirmation alert on the Notifications page before deleting all notifications. +- The map borders were expanded to make it easier to scroll around the map for New Zealanders. # 0.20.2 - 2024-12-17 diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 2cbf4a58..40893763 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -54,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); From 9106328224c3ca288cff87bc879a1b0ae6da5009 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Fri, 20 Dec 2024 16:27:16 +0100 Subject: [PATCH 41/53] Add a custom postgresql.conf file to the dawarich_db service --- .app_version | 2 +- CHANGELOG.md | 46 ++++++++++++++++++++++++++++++++++++++++--- docker-compose.yml | 3 +++ postgres.conf.example | 36 +++++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 postgres.conf.example diff --git a/.app_version b/.app_version index 144996ed..88541566 100644 --- a/.app_version +++ b/.app_version @@ -1 +1 @@ -0.20.3 +0.21.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 32c90c57..f15d85e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,52 @@ 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.20.3 - 2024-12-20 +# 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/postgresql.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/postgresql.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`. ### Added -- A button on a year stats card to update stats for the whole year. -- A button on a month stats card to update stats for a specific month. +- 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: @@ -23,6 +62,7 @@ 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. # 0.20.2 - 2024-12-17 diff --git a/docker-compose.yml b/docker-compose.yml index 4a41b272..68ad7846 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 # Provide path to your custom config networks: - dawarich environment: @@ -34,6 +36,7 @@ services: retries: 5 start_period: 30s timeout: 10s + command: postgres -c config_file=/etc/postgresql/postgresql.conf # Use custom config dawarich_app: image: freikin/dawarich:latest container_name: dawarich_app diff --git a/postgres.conf.example b/postgres.conf.example new file mode 100644 index 00000000..9e1687c3 --- /dev/null +++ b/postgres.conf.example @@ -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' From 0033dd49874e9013d1413ce65b9f84d506150c4b Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Fri, 20 Dec 2024 16:34:51 +0100 Subject: [PATCH 42/53] Update CHANGELOG.md --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f15d85e3..96f223fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ To mount a custom `postgresql.conf` file, you need to create a `postgresql.conf` volumes: - dawarich_db_data:/var/lib/postgresql/data - dawarich_shared:/var/shared -+ - ./postgresql.conf:/etc/postgresql/postgresql.conf # Provide path to custom config ++ - ./postgresql.conf:/etc/postgresql/postgresql.conf # Provide path to custom config ... healthcheck: test: [ "CMD-SHELL", "pg_isready -U postgres -d dawarich_development" ] @@ -31,11 +31,13 @@ To mount a custom `postgresql.conf` file, you need to create a `postgresql.conf` retries: 5 start_period: 30s timeout: 10s -+ command: postgres -c config_file=/etc/postgresql/postgresql.conf # Use custom config ++ command: postgres -c config_file=/etc/postgresql/postgresql.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 @@ -47,7 +49,7 @@ To ensure your database is using custom config, you can connect to the container ... dawarich_db: image: postgres:14.2-alpine -+ shm_size: 1G ++ shm_size: 1G ... ``` From a35e87490a185dec6c3c77d9f08a26987c39170d Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Fri, 20 Dec 2024 16:49:24 +0100 Subject: [PATCH 43/53] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96f223fe..b8c8b2c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ Authorization: Bearer YOUR_API_KEY - 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 From 88f86eff615a579e6589d1fe0004b8444e0e1939 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Fri, 20 Dec 2024 16:51:41 +0100 Subject: [PATCH 44/53] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 16878306..a087aab4 100644 --- a/README.md +++ b/README.md @@ -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. From 33521c0b89a63ecf7ddea663553b0cbc77b6a99b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 14:31:26 +0000 Subject: [PATCH 45/53] Bump sidekiq from 7.3.6 to 7.3.7 Bumps [sidekiq](https://github.com/sidekiq/sidekiq) from 7.3.6 to 7.3.7. - [Changelog](https://github.com/sidekiq/sidekiq/blob/main/Changes.md) - [Commits](https://github.com/sidekiq/sidekiq/compare/v7.3.6...v7.3.7) --- updated-dependencies: - dependency-name: sidekiq dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 346c9f8c..4e86ae11 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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.3) + logger (1.6.4) lograge (0.14.0) actionpack (>= 4) activesupport (>= 4) @@ -298,7 +298,7 @@ GEM 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.12) @@ -361,7 +361,7 @@ GEM 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) From 52ddb0528a91c9b79f3c1c78ad6e6e442a619fe0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 14:31:38 +0000 Subject: [PATCH 46/53] Bump importmap-rails from 2.0.3 to 2.1.0 Bumps [importmap-rails](https://github.com/rails/importmap-rails) from 2.0.3 to 2.1.0. - [Release notes](https://github.com/rails/importmap-rails/releases) - [Commits](https://github.com/rails/importmap-rails/compare/v2.0.3...v2.1.0) --- updated-dependencies: - dependency-name: importmap-rails dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 346c9f8c..e6a9b2b9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -109,7 +109,7 @@ GEM data_migrate (11.2.0) activerecord (>= 6.1) railties (>= 6.1) - date (3.4.0) + date (3.4.1) debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) @@ -128,7 +128,7 @@ GEM 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.8.0) - irb (1.14.2) + 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.3) + logger (1.6.4) lograge (0.14.0) actionpack (>= 4) activesupport (>= 4) @@ -246,7 +246,7 @@ GEM pry (>= 0.13, < 0.15) pry-rails (0.3.11) pry (>= 0.13.0) - psych (5.2.1) + psych (5.2.2) date stringio public_suffix (6.0.1) @@ -294,14 +294,14 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.2.1) - rdoc (6.9.1) + rdoc (6.10.0) psych (>= 4.0.0) redis (5.3.0) redis-client (>= 0.22.0) redis-client (0.22.2) connection_pool regexp_parser (2.9.2) - reline (0.5.12) + reline (0.6.0) io-console (~> 0.5) request_store (1.7.0) rack (>= 1.4) From f25c291cb00c346e4a31e1564f29595054be22fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 14:31:59 +0000 Subject: [PATCH 47/53] Bump geocoder from `04ee293` to 1.8.5 Bumps [geocoder](https://github.com/alexreisner/geocoder) from `04ee293` to 1.8.5. This release includes the previously tagged commit. - [Commits](https://github.com/alexreisner/geocoder/compare/04ee2936a30b30a23ded5231d7faf6cf6c27c099...v1.8.5) --- updated-dependencies: - dependency-name: geocoder dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 346c9f8c..d8883f63 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -105,7 +105,7 @@ GEM cronex (0.15.0) tzinfo unicode (>= 0.4.4.5) - csv (3.3.1) + csv (3.3.2) data_migrate (11.2.0) activerecord (>= 6.1) railties (>= 6.1) From c89ae76dd12316eb7734c21d4b28a2612f21c488 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 14:32:34 +0000 Subject: [PATCH 48/53] Bump dotenv-rails from 3.1.6 to 3.1.7 Bumps [dotenv-rails](https://github.com/bkeepers/dotenv) from 3.1.6 to 3.1.7. - [Release notes](https://github.com/bkeepers/dotenv/releases) - [Changelog](https://github.com/bkeepers/dotenv/blob/main/Changelog.md) - [Commits](https://github.com/bkeepers/dotenv/compare/v3.1.6...v3.1.7) --- updated-dependencies: - dependency-name: dotenv-rails dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 346c9f8c..2f46d4f8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -109,7 +109,7 @@ GEM data_migrate (11.2.0) activerecord (>= 6.1) railties (>= 6.1) - date (3.4.0) + date (3.4.1) debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) @@ -121,14 +121,14 @@ GEM warden (~> 1.2.3) diff-lcs (1.5.1) docile (1.4.1) - dotenv (3.1.6) - dotenv-rails (3.1.6) - dotenv (= 3.1.6) + 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) @@ -161,7 +161,7 @@ GEM activesupport (>= 6.0.0) railties (>= 6.0.0) io-console (0.8.0) - irb (1.14.2) + 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.3) + logger (1.6.4) lograge (0.14.0) actionpack (>= 4) activesupport (>= 4) @@ -246,7 +246,7 @@ GEM pry (>= 0.13, < 0.15) pry-rails (0.3.11) pry (>= 0.13.0) - psych (5.2.1) + psych (5.2.2) date stringio public_suffix (6.0.1) @@ -294,14 +294,14 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.2.1) - rdoc (6.9.1) + rdoc (6.10.0) psych (>= 4.0.0) redis (5.3.0) redis-client (>= 0.22.0) redis-client (0.22.2) connection_pool regexp_parser (2.9.2) - reline (0.5.12) + reline (0.6.0) io-console (~> 0.5) request_store (1.7.0) rack (>= 1.4) From a9ffb1a5ee287b9abe7aba5f3b7a6f5bb8c4e07e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 14:47:41 +0000 Subject: [PATCH 49/53] Bump debug from 1.9.2 to 1.10.0 Bumps [debug](https://github.com/ruby/debug) from 1.9.2 to 1.10.0. - [Release notes](https://github.com/ruby/debug/releases) - [Commits](https://github.com/ruby/debug/compare/v1.9.2...v1.10.0) --- updated-dependencies: - dependency-name: debug dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index e8289514..aadc1cd0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -110,7 +110,7 @@ GEM activerecord (>= 6.1) railties (>= 6.1) date (3.4.1) - debug (1.9.2) + debug (1.10.0) irb (~> 1.10) reline (>= 0.3.8) devise (4.9.4) From d640af40367403a30f855e843e10baf5fb5bed7f Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 23 Dec 2024 00:27:42 +0100 Subject: [PATCH 50/53] Add cache cleaning and preheating --- CHANGELOG.md | 6 ++++++ app/services/cache/clean.rb | 24 ++++++++++++++++++++++++ config/environment.rb | 6 ++++-- 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 app/services/cache/clean.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index b8c8b2c1..a57d6e34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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-23 + +### Added + +- Cache cleaning and preheating upon application start. + # 0.21.0 - 2024-12-20 ⚠️ This release introduces a breaking change. ⚠️ diff --git a/app/services/cache/clean.rb b/app/services/cache/clean.rb new file mode 100644 index 00000000..46a69e0b --- /dev/null +++ b/app/services/cache/clean.rb @@ -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 diff --git a/config/environment.rb b/config/environment.rb index c27e2a9f..3c870b83 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -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::Clean.call -Rails.cache.delete(CheckAppVersion::VERSION_CACHE_KEY) +# Preheat the cache +Cache::PreheatingJob.perform_later From 462df9e79617f2e1bdc499a0a0e44dd96c3944ad Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 24 Dec 2024 16:50:53 +0100 Subject: [PATCH 51/53] Make postgres config optional && add health check header && add photon api key --- .app_version | 2 +- CHANGELOG.md | 12 +++++++++--- app/controllers/api/v1/health_controller.rb | 2 ++ config/initializers/geocoder.rb | 2 ++ docker-compose.yml | 4 ++-- postgres.conf.example => postgresql.conf.example | 0 6 files changed, 16 insertions(+), 6 deletions(-) rename postgres.conf.example => postgresql.conf.example (100%) diff --git a/.app_version b/.app_version index 88541566..a67cebaf 100644 --- a/.app_version +++ b/.app_version @@ -1 +1 @@ -0.21.0 +0.21.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index a57d6e34..7d5c2fc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,17 @@ 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-23 +# 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 @@ -29,7 +35,7 @@ To mount a custom `postgresql.conf` file, you need to create a `postgresql.conf` volumes: - dawarich_db_data:/var/lib/postgresql/data - dawarich_shared:/var/shared -+ - ./postgresql.conf:/etc/postgresql/postgresql.conf # Provide path to custom config ++ - ./postgresql.conf:/etc/postgresql/postgres.conf # Provide path to custom config ... healthcheck: test: [ "CMD-SHELL", "pg_isready -U postgres -d dawarich_development" ] @@ -37,7 +43,7 @@ To mount a custom `postgresql.conf` file, you need to create a `postgresql.conf` retries: 5 start_period: 30s timeout: 10s -+ command: postgres -c config_file=/etc/postgresql/postgresql.conf # Use custom config ++ 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`. diff --git a/app/controllers/api/v1/health_controller.rb b/app/controllers/api/v1/health_controller.rb index 1e5ab2f1..53563cb0 100644 --- a/app/controllers/api/v1/health_controller.rb +++ b/app/controllers/api/v1/health_controller.rb @@ -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 + diff --git a/config/initializers/geocoder.rb b/config/initializers/geocoder.rb index d873c8ea..837fb394 100644 --- a/config/initializers/geocoder.rb +++ b/config/initializers/geocoder.rb @@ -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) diff --git a/docker-compose.yml b/docker-compose.yml index 68ad7846..fc46ae30 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,7 +23,7 @@ services: volumes: - dawarich_db_data:/var/lib/postgresql/data - dawarich_shared:/var/shared - - ./postgresql.conf:/etc/postgresql/postgresql.conf # Provide path to your custom config + # - ./postgresql.conf:/etc/postgresql/postgresql.conf # Optional, uncomment if you want to use a custom config networks: - dawarich environment: @@ -36,7 +36,7 @@ services: retries: 5 start_period: 30s timeout: 10s - command: postgres -c config_file=/etc/postgresql/postgresql.conf # Use custom config + # 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 diff --git a/postgres.conf.example b/postgresql.conf.example similarity index 100% rename from postgres.conf.example rename to postgresql.conf.example From 2eb96bcdf1e4e35ef184ea9d831caf9917dcb12d Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 24 Dec 2024 16:56:49 +0100 Subject: [PATCH 52/53] Update CircleCI config --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d0055f31..622392cf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,8 +28,8 @@ jobs: - run: name: Database Setup command: | - bundle exec rails db:create - bundle exec rails db:schema:load + bundle exec bin/rails db:create + bundle exec bin/rails db:schema:load - run: name: Run RSpec tests command: bundle exec rspec From 0dfdeac5c5951f9a4fe811c1a837098046455728 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 24 Dec 2024 17:01:26 +0100 Subject: [PATCH 53/53] Move cache cleaning to a job --- .circleci/config.yml | 4 ++-- app/jobs/cache/cleaning_job.rb | 9 +++++++++ config/environment.rb | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 app/jobs/cache/cleaning_job.rb diff --git a/.circleci/config.yml b/.circleci/config.yml index 622392cf..d0055f31 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,8 +28,8 @@ jobs: - run: name: Database Setup command: | - bundle exec bin/rails db:create - bundle exec bin/rails db:schema:load + bundle exec rails db:create + bundle exec rails db:schema:load - run: name: Run RSpec tests command: bundle exec rspec diff --git a/app/jobs/cache/cleaning_job.rb b/app/jobs/cache/cleaning_job.rb new file mode 100644 index 00000000..67c4315c --- /dev/null +++ b/app/jobs/cache/cleaning_job.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Cache::CleaningJob < ApplicationJob + queue_as :default + + def perform + Cache::Clean.call + end +end diff --git a/config/environment.rb b/config/environment.rb index 3c870b83..7e5c58f9 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -7,7 +7,7 @@ require_relative 'application' Rails.application.initialize! # Clear the cache -Cache::Clean.call +Cache::CleaningJob.perform_later # Preheat the cache Cache::PreheatingJob.perform_later