Compare commits
No commits in common. "master" and "0.32.0" have entirely different histories.
|
|
@ -1 +1 @@
|
|||
0.37.2
|
||||
0.32.0
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ orbs:
|
|||
jobs:
|
||||
test:
|
||||
docker:
|
||||
- image: cimg/ruby:3.4.6-browsers
|
||||
- image: cimg/ruby:3.4.1-browsers
|
||||
environment:
|
||||
RAILS_ENV: test
|
||||
CI: true
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Base-Image for Ruby and Node.js
|
||||
FROM ruby:3.4.6-alpine
|
||||
FROM ruby:3.4.1-alpine
|
||||
|
||||
ENV APP_PATH=/var/app
|
||||
ENV BUNDLE_VERSION=2.5.21
|
||||
|
|
|
|||
|
|
@ -4,6 +4,3 @@ DATABASE_PASSWORD=password
|
|||
DATABASE_NAME=dawarich_development
|
||||
DATABASE_PORT=5432
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
# Fix for macOS fork() issues with Sidekiq
|
||||
OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
|
||||
|
|
|
|||
16
.github/workflows/build_and_push.yml
vendored
|
|
@ -72,7 +72,19 @@ jobs:
|
|||
TAGS="freikin/dawarich:${VERSION}"
|
||||
|
||||
# Set platforms based on version type and release type
|
||||
PLATFORMS="linux/amd64,linux/arm64,linux/arm/v8,linux/arm/v7"
|
||||
PLATFORMS="linux/amd64,linux/arm64,linux/arm/v8,linux/arm/v7,linux/arm/v6"
|
||||
|
||||
# Check if this is a patch version (x.y.z where z > 0)
|
||||
if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[1-9][0-9]*$ ]]; then
|
||||
echo "Detected patch version ($VERSION) - building for AMD64 only"
|
||||
PLATFORMS="linux/amd64"
|
||||
elif [[ $VERSION =~ ^[0-9]+\.[0-9]+\.0$ ]]; then
|
||||
echo "Detected minor version ($VERSION) - building for all platforms"
|
||||
PLATFORMS="linux/amd64,linux/arm64,linux/arm/v8,linux/arm/v7,linux/arm/v6"
|
||||
else
|
||||
echo "Version format not recognized or non-semver - using AMD64 only for safety"
|
||||
PLATFORMS="linux/amd64"
|
||||
fi
|
||||
|
||||
# Add :rc tag for pre-releases
|
||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||
|
|
@ -96,7 +108,7 @@ jobs:
|
|||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
file: ./docker/Dockerfile.dev
|
||||
push: true
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
|
|
|||
2
.github/workflows/ci.yml
vendored
|
|
@ -34,7 +34,7 @@ jobs:
|
|||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.4.6'
|
||||
ruby-version: '3.4.1'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Set up Node.js
|
||||
|
|
|
|||
1
.gitignore
vendored
|
|
@ -84,4 +84,3 @@ node_modules/
|
|||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/e2e/temp/
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
3.4.6
|
||||
3.4.1
|
||||
|
|
|
|||
26
AGENTS.md
|
|
@ -1,26 +0,0 @@
|
|||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
Dawarich is a Rails 8 monolith. Controllers, models, jobs, services, policies, and Stimulus/Turbo JS live in `app/`, while shared POROs sit in `lib/`. Configuration, credentials, and cron/Sidekiq settings live in `config/`; API documentation assets are in `swagger/`. Database migrations and seeds live in `db/`, Docker tooling sits in `docker/`, and docs or media live in `docs/` and `screenshots/`. Runtime artifacts in `storage/`, `tmp/`, and `log/` stay untracked.
|
||||
|
||||
## Architecture & Key Services
|
||||
The stack pairs Rails 8 with PostgreSQL + PostGIS, Redis-backed Sidekiq, Devise/Pundit, Tailwind + DaisyUI, and Leaflet/Chartkick. Imports, exports, sharing, and trip analytics lean on PostGIS geometries plus workers, so queue anything non-trivial instead of blocking requests.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- `docker compose -f docker/docker-compose.yml up` — launches the full stack for smoke tests.
|
||||
- `bundle exec rails db:prepare` — create/migrate the PostGIS database.
|
||||
- `bundle exec bin/dev` and `bundle exec sidekiq` — start the web/Vite/Tailwind stack and workers locally.
|
||||
- `make test` — runs Playwright (`npx playwright test e2e --workers=1`) then `bundle exec rspec`.
|
||||
- `bundle exec rubocop` / `npx prettier --check app/javascript` — enforce formatting before commits.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
Use two-space indentation, snake_case filenames, and CamelCase classes. Keep Stimulus controllers under `app/javascript/controllers/*_controller.ts` so names match DOM `data-controller` hooks. Prefer service objects in `app/services/` for multi-step imports/exports, and let migrations named like `202405061210_add_indexes_to_events` manage schema changes. Follow Tailwind ordering conventions and avoid bespoke CSS unless necessary.
|
||||
|
||||
## Testing Guidelines
|
||||
RSpec mirrors the app hierarchy inside `spec/` with files suffixed `_spec.rb`; rely on FactoryBot/FFaker for data, WebMock for HTTP, and SimpleCov for coverage. Browser journeys live in `e2e/` and should use `data-testid` selectors plus seeded demo data to reset state. Run `make test` before pushing and document intentional gaps when coverage dips.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
Write short, imperative commit subjects (`Add globe_projection setting`) and include the PR/issue reference like `(#2138)` when relevant. Target `dev`, describe migrations, configs, and verification steps, and attach screenshots or curl examples for UI/API work. Link related Discussions for larger changes and request review from domain owners (imports, sharing, trips, etc.).
|
||||
|
||||
## Security & Configuration Tips
|
||||
Start from `.env.example` or `.env.template` and store secrets in encrypted Rails credentials; never commit files from `gps-env/` or real trace data. Rotate API keys, scrub sensitive coordinates in fixtures, and use the synthetic traces in `db/seeds.rb` when demonstrating imports.
|
||||
282
CHANGELOG.md
|
|
@ -4,288 +4,6 @@ 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.37.3] - Unreleased
|
||||
|
||||
## Fixed
|
||||
|
||||
- Routes are now being drawn the very same way on Map V2 as in Map V1. #2132 #2086
|
||||
- RailsPulse performance monitoring is now disabled for self-hosted instances. It fixes poor performance on Synology. #2139
|
||||
|
||||
## Changed
|
||||
|
||||
- Map V2 points loading is significantly sped up.
|
||||
- Points size on Map V2 was reduced to prevent overlapping.
|
||||
- Points sent from Owntracks and Overland are now being created synchronously to instantly reflect success or failure of point creation.
|
||||
|
||||
# [0.37.2] - 2026-01-04
|
||||
|
||||
## Fixed
|
||||
|
||||
- Months are now correctly ordered (Jan-Dec) in the year-end digest chart instead of being sorted alphabetically.
|
||||
- Time spent in a country and city is now calculated correctly for the year-end digest email. #2104
|
||||
- Updated Trix to fix a XSS vulnerability. #2102
|
||||
- Map v2 UI no longer blocks when Immich/Photoprism integration has a bad URL or is unreachable. Added 10-second timeout to photo API requests and improved error handling to prevent UI freezing during initial load. #2085
|
||||
|
||||
## Added
|
||||
- In Map v2 settings, you can now enable map to be rendered as a globe.
|
||||
|
||||
# [0.37.1] - 2025-12-30
|
||||
|
||||
## Fixed
|
||||
|
||||
- The db migration preventing the app from starting.
|
||||
- Raw data archive verifier now allows having points deleted from the db after archiving.
|
||||
|
||||
# [0.37.0] - 2025-12-30
|
||||
|
||||
## Added
|
||||
|
||||
- In the beginning of the year users will receive a year-end digest email with stats about their tracking activity during the past year. Users can opt out of receiving these emails in User Settings -> Notifications. Emails won't be sent if no email is configured in the SMTP settings or if user has no points tracked during the year.
|
||||
|
||||
## Changed
|
||||
|
||||
- Added and removed some indexes to improve the app performance based on the production usage data.
|
||||
|
||||
## Changed
|
||||
|
||||
- Deleting an import will now be processed in the background to prevent request timeouts for large imports.
|
||||
|
||||
## Fixed
|
||||
|
||||
- Deleting an import will no longer result in negative points count for the user.
|
||||
- Updating stats. #2022
|
||||
- Validate trip start date to be earlier than end date. #2057
|
||||
- Fog of war radius slider in map v2 settings is now being respected correctly. #2041
|
||||
- Applying changes in map v2 settings now works correctly. #2041
|
||||
- Invalidate stats cache on recalculation and other operations that change stats data.
|
||||
|
||||
|
||||
# [0.36.4] - 2025-12-26
|
||||
|
||||
## Fixed
|
||||
|
||||
- Fixed a bug preventing the app to start if a composite index on stats table already exists. #2034 #2051 #2046
|
||||
- New compiled assets will override old ones on app start to prevent serving stale assets.
|
||||
- Number of points in stats should no longer go negative when points are deleted. #2054
|
||||
- Disable Family::Invitations::CleanupJob no invitations are in the database. #2043
|
||||
- User can now enable family layer in Maps v2 and center on family members by clicking their emails. #2036
|
||||
|
||||
|
||||
# [0.36.3] - 2025-12-14
|
||||
|
||||
## Added
|
||||
|
||||
- Setting `ARCHIVE_RAW_DATA` env var to true will enable monthly raw data archiving for all users. It will look for points older than 2 months with `raw_data` column not empty and create a zip archive containing raw data files for each month. After successful archiving, raw data will be removed from the database to save space. Monthly archiving job is being run every day at 2:00 AM. Default env var value is false.
|
||||
- In map v2, user can now move points when Points layer is enabled. #2024
|
||||
- In map v2, routes are now being rendered using same logic as in map v1, route-length-wise. #2026
|
||||
|
||||
## Fixed
|
||||
|
||||
- Cities visited during a trip are now being calculated correctly. #547 #641 #1686 #1976
|
||||
- Points on the map are now show time in user's timezone. #580 #1035 #1682
|
||||
- Date range inputs now handle pre-epoch dates gracefully by clamping to valid PostgreSQL integer range. #685
|
||||
- Redis client now also being configured so that it could connect via unix socket. #1970
|
||||
- Importing KML files now creates points with correct timestamps. #1988
|
||||
- Importing KMZ files now works correctly.
|
||||
- Map settings are now being respected in map v2. #2012
|
||||
|
||||
|
||||
# [0.36.2] - 2025-12-06
|
||||
|
||||
## The Map v2 release
|
||||
|
||||
In this release we're introducing Map v2 based on MapLibre GL JS. It brings better performance, smoother interactions and more features in the future. User can select between Map v1 (Leaflet) and Map v2 (MapLibre GL JS) in the Settings -> Map Settings. New map features will be added to Map v2 only.
|
||||
|
||||
## Added
|
||||
|
||||
- User can select between Map v1 (Leaflet) and Map v2 (MapLibre GL JS) in the User Settings.
|
||||
|
||||
## Fixed
|
||||
|
||||
- Heatmap and Fog of War now are moving correctly during map interactions on v2 map. #1798
|
||||
- Polyline crossing international date line now are rendered correctly on v2 map. #1162
|
||||
- Place popup tags parsing (MapLibre GL JS compatibility)
|
||||
- Stats calculation should be faster now.
|
||||
|
||||
|
||||
# [0.36.1] - 2025-11-29
|
||||
|
||||
## Fixed
|
||||
|
||||
- Exporting user data now works a lot faster and consumes less memory.
|
||||
- Fix the restart loop. #1937 #1975
|
||||
|
||||
# [0.36.0] - 2025-11-24
|
||||
|
||||
## OIDC and KML support release
|
||||
|
||||
So, you want to configure your OIDC provider. If not — skip to the actual changelog. You're going to need to provide at least 4 environment variables: `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_ISSUER`, and `OIDC_REDIRECT_URI`. Then, if you want to rename the provider from "OpenID Connect" to something else (e.g. "Authentik"), set `OIDC_PROVIDER_NAME` variable as well. If you want to disable email/password registration and allow only OIDC login, set `ALLOW_EMAIL_PASSWORD_REGISTRATION` to `false`. After just 7 brand new environment variables, you'll never have to deal with passwords in Dawarich again!
|
||||
|
||||
Jokes aside, even though I'm not a fan of bloating the environment with too many variables, this is a nice addition and it will be reused in the cloud version of Dawarich as well. Thanks for waiting more than a year for this feature!
|
||||
|
||||
To configure your OIDC provider, set the following environment variables:
|
||||
|
||||
```
|
||||
OIDC_CLIENT_ID=client_id_example
|
||||
OIDC_CLIENT_SECRET=client_secret_example
|
||||
OIDC_ISSUER=https://authentik.yourdomain.com/application/o/dawarich/
|
||||
OIDC_REDIRECT_URI=https://your-dawarich-url.com/users/auth/openid_connect/callback
|
||||
OIDC_AUTO_REGISTER=true # optional, default is false
|
||||
OIDC_PROVIDER_NAME=YourProviderName # optional, default is OpenID Connect
|
||||
ALLOW_EMAIL_PASSWORD_REGISTRATION=false # optional, default is true
|
||||
```
|
||||
|
||||
## Added
|
||||
|
||||
- Support for KML file uploads. #350
|
||||
- Added a commented line in the `docker-compose.yml` file to use an alternative PostGIS image for ARM architecture.
|
||||
- User can now create a place directly from the map and add tags and notes to it. If reverse geocoding is enabled, list of nearby places will be shown as suggestions.
|
||||
- User can create and manage tags for places.
|
||||
- Visits for manually created places are being suggested automatically, just like for areas.
|
||||
- User can enable or disable places layers on the map to show/hide all or just some of their visited places based on tags.
|
||||
- User can define privacy zones around places with specific tags to hide map data within a certain radius.
|
||||
- If user has a place tagged with a tag named "Home" (case insensitive), and this place doesn't have a privacy zone defined, this place will be used as home location for days with no tracked data. #1659 #1575
|
||||
|
||||
## Fixed
|
||||
|
||||
- The map settings panel is now scrollable
|
||||
- Fixed a bug where family location sharing settings were not being updated correctly. #1940
|
||||
|
||||
## Changed
|
||||
|
||||
- Internal redis settings updated to implement support for connecting to Redis via unix socket. #1706
|
||||
- Implemented authentication via GitHub and Google for Dawarich Cloud.
|
||||
- Implemented OpenID Connect authentication for self-hosted Dawarich instances. #66
|
||||
|
||||
|
||||
# [0.35.1] - 2025-11-09
|
||||
|
||||
## Fixed
|
||||
|
||||
- StrongMigration issue #1931
|
||||
|
||||
|
||||
# [0.35.0] - 2025-11-09
|
||||
|
||||
⚠️ Important ⚠️
|
||||
|
||||
The default `docker-compose.yml` file has been updated to provide sensible defaults for self-hosted production environments. This should not break existing setups, but it's recommended to review your `docker-compose.yml` file and update it accordingly.
|
||||
|
||||
You can now set `RAILS_ENV` environment variable to `production` to run Dawarich in production mode.
|
||||
|
||||
## Added
|
||||
|
||||
- Selection tool on the map now can select points that user can delete in bulk. #433
|
||||
|
||||
## Fixed
|
||||
|
||||
- Taiwan flag is now shown on its own instead of in combination with China flag.
|
||||
- On the registration page and other user forms, if something goes wrong, error messages are now shown to the user.
|
||||
- Leaving family, deleting family and cancelling invitations now prompt confirmation dialog to prevent accidental actions.
|
||||
- Each pending family invitation now also contains a link to share with the invitee.
|
||||
|
||||
## Changed
|
||||
|
||||
- Removed useless system tests and cover map functionality with Playwright e2e tests instead.
|
||||
- S3 storage now can be used in self-hosted instances as well. Set STORAGE_BACKEND environment variable to `s3` and provide `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`, `AWS_BUCKET` and `AWS_ENDPOINT_URL` environment variables to configure it.
|
||||
- Number of family members on self-hosted instances is no longer limited. #1918
|
||||
- Export to GPX now adds speed and course to each point if they are available.
|
||||
- `docker-compose.yml` file updated to provide sensible defaults for self-hosted production environment.
|
||||
- `.env.example` file added with default environment variables.
|
||||
- Single Dockerfile introduced so Dawarich could be run in self-hosted mode in production environment.
|
||||
|
||||
# [0.34.2] - 2025-10-31
|
||||
|
||||
## Fixed
|
||||
|
||||
- Fixed a bug in UTM trackable concern. #1909
|
||||
|
||||
# [0.34.1] - 2025-10-30
|
||||
|
||||
## Fixed
|
||||
|
||||
- Broken Stats page for users with no reverse geocoding enabled. #1877
|
||||
|
||||
## Changed
|
||||
|
||||
- Date navigation on the map page is no longer shown as floating panel. It is now part of the top navigation bar to prevent overlapping with other map controls. #1894 #1881
|
||||
|
||||
## Added
|
||||
|
||||
- [Dawarich Cloud] Added support for UTM parameters during user registration. UTM parameters will be stored with the user record for marketing analytics purposes.
|
||||
|
||||
# [0.34.0] - 2025-10-10
|
||||
|
||||
## The Family release
|
||||
|
||||
In this release we're introducing family features that allow users to create family groups, invite members, and share location data. Family owners can manage members, control sharing settings, and ensure secure access to shared information. Location sharing is optional and can be enabled or disabled by each member individually. Users can join only one family at a time. Location sharing settings can be set to share location for 1, 6, 12, 24 hours or permanently. Family features are now available only for self-hosted instances and will be available in the cloud in the future. When "Family members" layer is enabled on the map, family member markers will be updated in real-time.
|
||||
|
||||
## Added
|
||||
|
||||
- Users can now create family groups and invite members to join.
|
||||
|
||||
## Fixed
|
||||
|
||||
- Sign out button works again. #1844
|
||||
- Fixed user deletion bug where user could not be deleted due to counter cache on points.
|
||||
- Users always have default distance unit set to kilometers. #1832
|
||||
- All confirmation dialogs are now showing only once.
|
||||
|
||||
## Changed
|
||||
|
||||
- Minor versions of Dawarich are being built for ARM64 architecture as well again. #1840
|
||||
- Importing process for Google Maps Timeline exports, GeoJSON and geodata from photos is now significantly faster.
|
||||
- The Map page now features a full-screen map.
|
||||
|
||||
|
||||
# [0.33.1] - 2025-10-07
|
||||
|
||||
## Changed
|
||||
|
||||
- On the Trip page, instead of list of visited countries, a number of them is being shown. Clicking on it opens a modal with a list of countries visited during the trip. #1731
|
||||
|
||||
## Fixed
|
||||
|
||||
- `GET /api/v1/stats` endpoint now returns correct 0 instead of null if no points were tracked in the requested period.
|
||||
- User import data now being streamed instead of loaded into memory all at once. This should prevent large imports from exhausting memory or hitting IO limits while reading export archives.
|
||||
- Popup for manual visit creation now looks better in both light and dark modes. #1835
|
||||
- Fixed a bug where visit circles were not interactive on the map page. #1833
|
||||
- Fixed a bug with stats sharing settings being not filled. #1826
|
||||
- Fixed a bug where user could not be deleted due to counter cache on points. #1818
|
||||
- Introduce apt-get upgrade before installing new packages in the docker image to prevent vulnerabilities. #1793
|
||||
- Fixed time shift when creating visits manually. #1679
|
||||
- Provide default map layer if user settings are not set.
|
||||
|
||||
# [0.33.0] - 2025-09-29
|
||||
|
||||
## Fixed
|
||||
|
||||
- Fix a bug where some points from Owntracks were not being processed correctly which prevented import from being created. #1745
|
||||
- Hexagons for the stats page are now being calculated a lot faster.
|
||||
- Prometheus exporter is now not being started when console is being run.
|
||||
- Stats will now properly reflect countries and cities visited after importing new points.
|
||||
- `GET /api/v1/points` will now return correct latitude and longitude values. #1502
|
||||
- Deleting an import will now trigger stats recalculation for affected months. #1789
|
||||
- Importing process should now schedule visits suggestions job a lot faster.
|
||||
- Importing GPX files that start with `<gpx` tag will now be detected correctly. #1775
|
||||
- Buttons on the map now have correct contrast in both light and dark modes.
|
||||
|
||||
## Changed
|
||||
|
||||
- Onboarding modal window now features a link to the App Store and a QR code to configure the Dawarich iOS app.
|
||||
- A permanent option was removed from stats sharing options. Now, stats can be shared for 1, 12 or 24 hours only.
|
||||
- User data archive importing now uploads the file directly to the storage service instead of uploading it to the app first.
|
||||
- Importing progress bars are now looking nice.
|
||||
- Ruby version was updated to 3.4.6.
|
||||
|
||||
## Added
|
||||
|
||||
- Based on preferred theme (light or dark), the map controls will now load with the corresponding styles.
|
||||
- [Dawarich Cloud] Added foundation for upcoming authentication from iOS app.
|
||||
- [Dawarich Cloud] Trial users can now create up to 5 imports. After that, they will be prompted to subscribe to a paid plan.
|
||||
- [Dawarich Cloud] Added Posthog analytics. Disabled by default, can be enabled with POSTHOG_ENABLED environment variable.
|
||||
|
||||
|
||||
# [0.32.0] - 2025-09-13
|
||||
|
||||
## Fixed
|
||||
|
|
|
|||
83
CLAUDE.md
|
|
@ -12,7 +12,6 @@ This file contains essential information for Claude to work effectively with the
|
|||
- Import from various sources (Google Maps Timeline, OwnTracks, Strava, GPX, GeoJSON, photos)
|
||||
- Export to GeoJSON and GPX formats
|
||||
- Statistics and analytics (countries visited, distance traveled, etc.)
|
||||
- Public sharing of monthly statistics with time-based expiration
|
||||
- Trips management with photo integration
|
||||
- Areas and visits tracking
|
||||
- Integration with photo management systems (Immich, Photoprism)
|
||||
|
|
@ -76,7 +75,7 @@ This file contains essential information for Claude to work effectively with the
|
|||
- **Trip**: User-defined travel periods with analytics
|
||||
- **Import**: Data import operations
|
||||
- **Export**: Data export operations
|
||||
- **Stat**: Calculated statistics and metrics with public sharing capabilities
|
||||
- **Stat**: Calculated statistics and metrics
|
||||
|
||||
### Geographic Features
|
||||
- Uses PostGIS for advanced geographic queries
|
||||
|
|
@ -127,41 +126,11 @@ npx playwright test # E2E tests
|
|||
- Various import jobs for different data sources
|
||||
- Statistical calculation jobs
|
||||
|
||||
## Public Sharing System
|
||||
|
||||
### Overview
|
||||
Dawarich includes a comprehensive public sharing system that allows users to share their monthly statistics with others without requiring authentication. This feature enables users to showcase their location data while maintaining privacy control through configurable expiration settings.
|
||||
|
||||
### Key Features
|
||||
- **Time-based expiration**: Share links can expire after 1 hour, 12 hours, 24 hours, or be permanent
|
||||
- **UUID-based access**: Each shared stat has a unique, unguessable UUID for security
|
||||
- **Public API endpoints**: Hexagon map data can be accessed via API without authentication when sharing is enabled
|
||||
- **Automatic cleanup**: Expired shares are automatically inaccessible
|
||||
- **Privacy controls**: Users can enable/disable sharing and regenerate sharing URLs at any time
|
||||
|
||||
### Technical Implementation
|
||||
- **Database**: `sharing_settings` (JSONB) and `sharing_uuid` (UUID) columns on `stats` table
|
||||
- **Routes**: `/shared/month/:uuid` for public viewing, `/stats/:year/:month/sharing` for management
|
||||
- **API**: `/api/v1/maps/hexagons` supports public access via `uuid` parameter
|
||||
- **Controllers**: `Shared::StatsController` handles public views, sharing management integrated into existing stats flow
|
||||
|
||||
### Security Features
|
||||
- **No authentication bypass**: Public sharing only exposes specifically designed endpoints
|
||||
- **UUID-based access**: Sharing URLs use unguessable UUIDs rather than sequential IDs
|
||||
- **Expiration enforcement**: Automatic expiration checking prevents access to expired shares
|
||||
- **Limited data exposure**: Only monthly statistics and hexagon data are publicly accessible
|
||||
|
||||
### Usage Patterns
|
||||
- **Social sharing**: Users can share interesting travel months with friends and family
|
||||
- **Portfolio/showcase**: Travel bloggers and photographers can showcase location statistics
|
||||
- **Data collaboration**: Researchers can share aggregated location data for analysis
|
||||
- **Public demonstrations**: Demo instances can provide public examples without compromising user data
|
||||
|
||||
## API Documentation
|
||||
|
||||
- **Framework**: rSwag (Swagger/OpenAPI)
|
||||
- **Location**: `/api-docs` endpoint
|
||||
- **Authentication**: API key (Bearer) for API access, UUID-based access for public shares
|
||||
- **Authentication**: API key (Bearer) for API access
|
||||
|
||||
## Database Schema
|
||||
|
||||
|
|
@ -173,7 +142,7 @@ Dawarich includes a comprehensive public sharing system that allows users to sha
|
|||
- `visits` - Detected area visits
|
||||
- `trips` - Travel periods
|
||||
- `imports`/`exports` - Data transfer operations
|
||||
- `stats` - Calculated metrics with sharing capabilities (`sharing_settings`, `sharing_uuid`)
|
||||
- `stats` - Calculated metrics
|
||||
|
||||
### PostGIS Integration
|
||||
- Extensive use of PostGIS geometry types
|
||||
|
|
@ -232,52 +201,6 @@ bundle exec bundle-audit # Dependency security
|
|||
4. **Testing**: Include both unit and integration tests for location-based features
|
||||
5. **Performance**: Consider database indexes for geographic queries
|
||||
6. **Security**: Never log or expose user location data inappropriately
|
||||
7. **Public Sharing**: When implementing features that interact with stats, consider public sharing access patterns:
|
||||
- Use `public_accessible?` method to check if a stat can be publicly accessed
|
||||
- Support UUID-based access in API endpoints when appropriate
|
||||
- Respect expiration settings and disable sharing when expired
|
||||
- Only expose minimal necessary data in public sharing contexts
|
||||
|
||||
### Route Drawing Implementation (Critical)
|
||||
|
||||
⚠️ **IMPORTANT: Unit Mismatch in Route Splitting Logic**
|
||||
|
||||
Both Map v1 (Leaflet) and Map v2 (MapLibre) contain an **intentional unit mismatch** in route drawing that must be preserved for consistency:
|
||||
|
||||
**The Issue**:
|
||||
- `haversineDistance()` function returns distance in **kilometers** (e.g., 0.5 km)
|
||||
- Route splitting threshold is stored and compared as **meters** (e.g., 500)
|
||||
- The code compares them directly: `0.5 > 500` = always **FALSE**
|
||||
|
||||
**Result**:
|
||||
- The distance threshold (`meters_between_routes` setting) is **effectively disabled**
|
||||
- Routes only split on **time gaps** (default: 60 minutes between points)
|
||||
- This creates longer, more continuous routes that users expect
|
||||
|
||||
**Code Locations**:
|
||||
- **Map v1**: `app/javascript/maps/polylines.js:390`
|
||||
- Uses `haversineDistance()` from `maps/helpers.js` (returns km)
|
||||
- Compares to `distanceThresholdMeters` variable (value in meters)
|
||||
|
||||
- **Map v2**: `app/javascript/maps_maplibre/layers/routes_layer.js:82-104`
|
||||
- Has built-in `haversineDistance()` method (returns km)
|
||||
- Intentionally skips `/1000` conversion to replicate v1 behavior
|
||||
- Comment explains this is matching v1's unit mismatch
|
||||
|
||||
**Critical Rules**:
|
||||
1. ❌ **DO NOT "fix" the unit mismatch** - this would break user expectations
|
||||
2. ✅ **Keep both versions synchronized** - they must behave identically
|
||||
3. ✅ **Document any changes** - route drawing changes affect all users
|
||||
4. ⚠️ If you ever fix this bug:
|
||||
- You MUST update both v1 and v2 simultaneously
|
||||
- You MUST migrate user settings (multiply existing values by 1000 or divide by 1000 depending on direction)
|
||||
- You MUST communicate the breaking change to users
|
||||
|
||||
**Additional Route Drawing Details**:
|
||||
- **Time threshold**: 60 minutes (default) - actually functional
|
||||
- **Distance threshold**: 500 meters (default) - currently non-functional due to unit bug
|
||||
- **Sorting**: Map v2 sorts points by timestamp client-side; v1 relies on backend ASC order
|
||||
- **API ordering**: Map v2 must request `order: 'asc'` to match v1's chronological data flow
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
|||
38
Gemfile
|
|
@ -5,62 +5,54 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
|||
|
||||
ruby File.read('.ruby-version').strip
|
||||
|
||||
gem 'activerecord-postgis-adapter', '11.0'
|
||||
gem 'activerecord-postgis-adapter'
|
||||
# https://meta.discourse.org/t/cant-rebuild-due-to-aws-sdk-gem-bump-and-new-aws-data-integrity-protections/354217/40
|
||||
gem 'aws-sdk-core', '~> 3.215.1', require: false
|
||||
gem 'aws-sdk-kms', '~> 1.96.0', require: false
|
||||
gem 'aws-sdk-s3', '~> 1.177.0', require: false
|
||||
gem 'bootsnap', require: false
|
||||
gem 'chartkick'
|
||||
gem 'connection_pool', '< 3' # Pin to 2.x - version 3.0+ has breaking API changes with Rails RedisCacheStore
|
||||
gem 'data_migrate'
|
||||
gem 'devise'
|
||||
gem 'foreman'
|
||||
gem 'geocoder', github: 'Freika/geocoder', branch: 'master'
|
||||
gem 'gpx'
|
||||
gem 'groupdate'
|
||||
gem 'h3', '~> 3.7'
|
||||
gem 'httparty'
|
||||
gem 'importmap-rails'
|
||||
gem 'jwt', '~> 2.8'
|
||||
gem 'kaminari'
|
||||
gem 'lograge'
|
||||
gem 'oj'
|
||||
gem 'omniauth-github', '~> 2.0.0'
|
||||
gem 'omniauth-google-oauth2'
|
||||
gem 'omniauth_openid_connect'
|
||||
gem 'omniauth-rails_csrf_protection'
|
||||
gem 'parallel'
|
||||
gem 'pg'
|
||||
gem 'prometheus_exporter'
|
||||
gem 'puma'
|
||||
gem 'pundit', '>= 2.5.1'
|
||||
gem 'pundit'
|
||||
gem 'rails', '~> 8.0'
|
||||
gem 'rails_icons'
|
||||
gem 'rails_pulse'
|
||||
gem 'redis'
|
||||
gem 'rexml'
|
||||
gem 'rgeo'
|
||||
gem 'rgeo-activerecord', '~> 8.0.0'
|
||||
gem 'rgeo-activerecord'
|
||||
gem 'rgeo-geojson'
|
||||
gem 'rqrcode', '~> 3.0'
|
||||
gem 'rswag-api'
|
||||
gem 'rswag-ui'
|
||||
gem 'rubyzip', '~> 3.2'
|
||||
gem 'sentry-rails', '>= 5.27.0'
|
||||
gem 'rubyzip', '~> 2.4'
|
||||
gem 'sentry-rails'
|
||||
gem 'sentry-ruby'
|
||||
gem 'sidekiq', '8.0.10' # Pin to 8.0.x - sidekiq 8.1+ requires connection_pool 3.0+ which has breaking changes with Rails
|
||||
gem 'sidekiq-cron', '>= 2.3.1'
|
||||
gem 'sidekiq'
|
||||
gem 'sidekiq-cron'
|
||||
gem 'sidekiq-limit_fetch'
|
||||
gem 'sprockets-rails'
|
||||
gem 'stackprof'
|
||||
gem 'stimulus-rails'
|
||||
gem 'tailwindcss-rails', '= 3.3.2'
|
||||
gem 'turbo-rails', '>= 2.0.17'
|
||||
gem 'strong_migrations'
|
||||
gem 'tailwindcss-rails'
|
||||
gem 'turbo-rails'
|
||||
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
|
||||
gem 'with_advisory_lock'
|
||||
|
||||
group :development, :test, :staging do
|
||||
group :development, :test do
|
||||
gem 'brakeman', require: false
|
||||
gem 'bundler-audit', require: false
|
||||
gem 'debug', platforms: %i[mri mingw x64_mingw]
|
||||
|
|
@ -69,7 +61,7 @@ group :development, :test, :staging do
|
|||
gem 'ffaker'
|
||||
gem 'pry-byebug'
|
||||
gem 'pry-rails'
|
||||
gem 'rspec-rails', '>= 8.0.1'
|
||||
gem 'rspec-rails'
|
||||
gem 'rswag-specs'
|
||||
end
|
||||
|
||||
|
|
@ -84,7 +76,7 @@ group :test do
|
|||
end
|
||||
|
||||
group :development do
|
||||
gem 'database_consistency', '>= 2.0.5', require: false
|
||||
gem 'rubocop-rails', '>= 2.33.4', require: false
|
||||
gem 'strong_migrations', '>= 2.4.0'
|
||||
gem 'database_consistency', require: false
|
||||
gem 'foreman'
|
||||
gem 'rubocop-rails', require: false
|
||||
end
|
||||
|
|
|
|||
446
Gemfile.lock
|
|
@ -10,29 +10,29 @@ GIT
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (8.0.3)
|
||||
actionpack (= 8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
actioncable (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (8.0.3)
|
||||
actionpack (= 8.0.3)
|
||||
activejob (= 8.0.3)
|
||||
activerecord (= 8.0.3)
|
||||
activestorage (= 8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
actionmailbox (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activestorage (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (8.0.3)
|
||||
actionpack (= 8.0.3)
|
||||
actionview (= 8.0.3)
|
||||
activejob (= 8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
actionmailer (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
actionview (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (8.0.3)
|
||||
actionview (= 8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
actionpack (8.0.2.1)
|
||||
actionview (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
|
|
@ -40,38 +40,38 @@ GEM
|
|||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (8.0.3)
|
||||
actionpack (= 8.0.3)
|
||||
activerecord (= 8.0.3)
|
||||
activestorage (= 8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
actiontext (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activestorage (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
actionview (8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
activejob (8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
activerecord (8.0.3)
|
||||
activemodel (= 8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
activemodel (8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
activerecord (8.0.2.1)
|
||||
activemodel (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
timeout (>= 0.4.0)
|
||||
activerecord-postgis-adapter (11.0.0)
|
||||
activerecord (~> 8.0.0)
|
||||
rgeo-activerecord (~> 8.0.0)
|
||||
activestorage (8.0.3)
|
||||
actionpack (= 8.0.3)
|
||||
activejob (= 8.0.3)
|
||||
activerecord (= 8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
activestorage (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
marcel (~> 1.0)
|
||||
activesupport (8.0.3)
|
||||
activesupport (8.0.2.1)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
|
|
@ -86,10 +86,8 @@ GEM
|
|||
uri (>= 0.13.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
aes_key_wrap (1.1.0)
|
||||
ast (2.4.3)
|
||||
attr_extras (7.1.0)
|
||||
attr_required (1.0.2)
|
||||
aws-eventstream (1.3.2)
|
||||
aws-partitions (1.1072.0)
|
||||
aws-sdk-core (3.215.1)
|
||||
|
|
@ -108,12 +106,11 @@ GEM
|
|||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
benchmark (0.5.0)
|
||||
bigdecimal (4.0.1)
|
||||
bindata (2.5.1)
|
||||
benchmark (0.4.1)
|
||||
bigdecimal (3.2.2)
|
||||
bootsnap (1.18.6)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (7.1.1)
|
||||
brakeman (7.0.2)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
bundler-audit (0.9.2)
|
||||
|
|
@ -129,26 +126,25 @@ GEM
|
|||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
chartkick (5.2.1)
|
||||
chartkick (5.2.0)
|
||||
chunky_png (1.4.0)
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.3.6)
|
||||
connection_pool (2.5.5)
|
||||
crack (1.0.1)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.4)
|
||||
crack (1.0.0)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
cronex (0.15.0)
|
||||
tzinfo
|
||||
unicode (>= 0.4.4.5)
|
||||
css-zero (1.1.15)
|
||||
csv (3.3.4)
|
||||
data_migrate (11.3.1)
|
||||
data_migrate (11.3.0)
|
||||
activerecord (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
database_consistency (2.0.6)
|
||||
database_consistency (2.0.4)
|
||||
activerecord (>= 3.2)
|
||||
date (3.5.0)
|
||||
date (3.4.1)
|
||||
debug (1.11.0)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
|
|
@ -165,39 +161,23 @@ GEM
|
|||
dotenv (= 3.1.8)
|
||||
railties (>= 6.1)
|
||||
drb (2.2.3)
|
||||
email_validator (2.2.4)
|
||||
activemodel
|
||||
erb (6.0.0)
|
||||
erb (5.0.2)
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.4.0)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
factory_bot (6.5.5)
|
||||
factory_bot (6.5.4)
|
||||
activesupport (>= 6.1.0)
|
||||
factory_bot_rails (6.5.1)
|
||||
factory_bot_rails (6.5.0)
|
||||
factory_bot (~> 6.5)
|
||||
railties (>= 6.1.0)
|
||||
fakeredis (0.1.4)
|
||||
faraday (2.14.0)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday-follow_redirects (0.4.0)
|
||||
faraday (>= 1, < 3)
|
||||
faraday-net_http (3.4.1)
|
||||
net-http (>= 0.5.0)
|
||||
ffaker (2.25.0)
|
||||
ffi (1.17.2-aarch64-linux-gnu)
|
||||
ffi (1.17.2-arm-linux-gnu)
|
||||
ffi (1.17.2-arm64-darwin)
|
||||
ffi (1.17.2-x86-linux-gnu)
|
||||
ffi (1.17.2-x86_64-darwin)
|
||||
ffi (1.17.2-x86_64-linux-gnu)
|
||||
ffaker (2.24.0)
|
||||
foreman (0.90.0)
|
||||
thor (~> 1.4)
|
||||
fugit (1.12.1)
|
||||
et-orbi (~> 1.4)
|
||||
fugit (1.11.1)
|
||||
et-orbi (~> 1, >= 1.2.11)
|
||||
raabro (~> 1.4)
|
||||
globalid (1.3.0)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
gpx (1.2.1)
|
||||
csv
|
||||
|
|
@ -205,36 +185,24 @@ GEM
|
|||
rake
|
||||
groupdate (6.7.0)
|
||||
activesupport (>= 7.1)
|
||||
h3 (3.7.4)
|
||||
ffi (~> 1.9)
|
||||
rgeo-geojson (~> 2.1)
|
||||
zeitwerk (~> 2.5)
|
||||
hashdiff (1.2.1)
|
||||
hashie (5.0.0)
|
||||
hashdiff (1.1.2)
|
||||
httparty (0.23.1)
|
||||
csv
|
||||
mini_mime (>= 1.0.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
i18n (1.14.8)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
importmap-rails (2.2.2)
|
||||
actionpack (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
io-console (0.8.1)
|
||||
irb (1.15.3)
|
||||
irb (1.15.2)
|
||||
pp (>= 0.6.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.18.0)
|
||||
json-jwt (1.17.0)
|
||||
activesupport (>= 4.2)
|
||||
aes_key_wrap
|
||||
base64
|
||||
bindata
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
json (2.12.0)
|
||||
json-schema (5.0.1)
|
||||
addressable (~> 2.8)
|
||||
jwt (2.10.1)
|
||||
|
|
@ -262,26 +230,22 @@ GEM
|
|||
loofah (2.24.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.9.0)
|
||||
logger
|
||||
mail (2.8.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.1.0)
|
||||
marcel (1.0.4)
|
||||
matrix (0.4.2)
|
||||
method_source (1.1.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (6.0.1)
|
||||
prism (~> 1.5)
|
||||
minitest (5.25.5)
|
||||
msgpack (1.7.3)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.8.0)
|
||||
bigdecimal (>= 3.1, < 5)
|
||||
net-http (0.6.0)
|
||||
uri
|
||||
net-imap (0.5.12)
|
||||
multi_xml (0.7.1)
|
||||
bigdecimal (~> 3.1)
|
||||
net-imap (0.5.9)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
|
|
@ -291,86 +255,36 @@ GEM
|
|||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.10)
|
||||
nokogiri (1.18.9)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.10-aarch64-linux-gnu)
|
||||
nokogiri (1.18.9-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.10-arm-linux-gnu)
|
||||
nokogiri (1.18.9-arm-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.10-arm64-darwin)
|
||||
nokogiri (1.18.9-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.10-x86_64-darwin)
|
||||
nokogiri (1.18.9-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.10-x86_64-linux-gnu)
|
||||
nokogiri (1.18.9-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
oauth2 (2.0.17)
|
||||
faraday (>= 0.17.3, < 4.0)
|
||||
jwt (>= 1.0, < 4.0)
|
||||
logger (~> 1.2)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 4)
|
||||
snaky_hash (~> 2.0, >= 2.0.3)
|
||||
version_gem (~> 1.1, >= 1.1.9)
|
||||
oj (3.16.11)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
omniauth (2.1.4)
|
||||
hashie (>= 3.4.6)
|
||||
logger
|
||||
rack (>= 2.2.3)
|
||||
rack-protection
|
||||
omniauth-github (2.0.1)
|
||||
omniauth (~> 2.0)
|
||||
omniauth-oauth2 (~> 1.8)
|
||||
omniauth-google-oauth2 (1.2.1)
|
||||
jwt (>= 2.9.2)
|
||||
oauth2 (~> 2.0)
|
||||
omniauth (~> 2.0)
|
||||
omniauth-oauth2 (~> 1.8)
|
||||
omniauth-oauth2 (1.8.0)
|
||||
oauth2 (>= 1.4, < 3)
|
||||
omniauth (~> 2.0)
|
||||
omniauth-rails_csrf_protection (1.0.2)
|
||||
actionpack (>= 4.2)
|
||||
omniauth (~> 2.0)
|
||||
omniauth_openid_connect (0.8.0)
|
||||
omniauth (>= 1.9, < 3)
|
||||
openid_connect (~> 2.2)
|
||||
openid_connect (2.3.1)
|
||||
activemodel
|
||||
attr_required (>= 1.0.0)
|
||||
email_validator
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
json-jwt (>= 1.16)
|
||||
mail
|
||||
rack-oauth2 (~> 2.2)
|
||||
swd (~> 2.0)
|
||||
tzinfo
|
||||
validate_url
|
||||
webfinger (~> 2.0)
|
||||
optimist (3.2.1)
|
||||
orm_adapter (0.5.0)
|
||||
ostruct (0.6.1)
|
||||
pagy (43.2.2)
|
||||
json
|
||||
yaml
|
||||
parallel (1.27.0)
|
||||
parser (3.3.10.0)
|
||||
parser (3.3.8.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
patience_diff (1.2.0)
|
||||
optimist (~> 3.0)
|
||||
pg (1.6.2)
|
||||
pg (1.6.2-aarch64-linux)
|
||||
pg (1.6.2-arm64-darwin)
|
||||
pg (1.6.2-x86_64-darwin)
|
||||
pg (1.6.2-x86_64-linux)
|
||||
pp (0.6.3)
|
||||
pg (1.5.9)
|
||||
pp (0.6.2)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.7.0)
|
||||
prism (1.4.0)
|
||||
prometheus_exporter (2.2.0)
|
||||
webrick
|
||||
pry (0.15.2)
|
||||
|
|
@ -384,25 +298,14 @@ GEM
|
|||
psych (5.2.6)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.2)
|
||||
puma (7.1.0)
|
||||
public_suffix (6.0.1)
|
||||
puma (6.6.1)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.5.2)
|
||||
pundit (2.5.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.2.4)
|
||||
rack-oauth2 (2.3.0)
|
||||
activesupport
|
||||
attr_required
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
json-jwt (>= 1.11.0)
|
||||
rack (>= 2.1.0)
|
||||
rack-protection (4.2.1)
|
||||
base64 (>= 0.1.0)
|
||||
logger (>= 1.6.0)
|
||||
rack (>= 3.0.0, < 4)
|
||||
rack (3.2.0)
|
||||
rack-session (2.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
|
|
@ -410,20 +313,20 @@ GEM
|
|||
rack (>= 1.3)
|
||||
rackup (2.2.1)
|
||||
rack (>= 3)
|
||||
rails (8.0.3)
|
||||
actioncable (= 8.0.3)
|
||||
actionmailbox (= 8.0.3)
|
||||
actionmailer (= 8.0.3)
|
||||
actionpack (= 8.0.3)
|
||||
actiontext (= 8.0.3)
|
||||
actionview (= 8.0.3)
|
||||
activejob (= 8.0.3)
|
||||
activemodel (= 8.0.3)
|
||||
activerecord (= 8.0.3)
|
||||
activestorage (= 8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
rails (8.0.2.1)
|
||||
actioncable (= 8.0.2.1)
|
||||
actionmailbox (= 8.0.2.1)
|
||||
actionmailer (= 8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
actiontext (= 8.0.2.1)
|
||||
actionview (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activemodel (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activestorage (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 8.0.3)
|
||||
railties (= 8.0.2.1)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
|
|
@ -434,46 +337,32 @@ GEM
|
|||
rails_icons (1.4.0)
|
||||
nokogiri (~> 1.16, >= 1.16.4)
|
||||
rails (> 6.1)
|
||||
rails_pulse (0.2.4)
|
||||
css-zero (~> 1.1, >= 1.1.4)
|
||||
groupdate (~> 6.0)
|
||||
pagy (>= 8, < 44)
|
||||
rails (>= 7.1.0, < 9.0.0)
|
||||
ransack (~> 4.0)
|
||||
request_store (~> 1.5)
|
||||
turbo-rails (~> 2.0.11)
|
||||
railties (8.0.3)
|
||||
actionpack (= 8.0.3)
|
||||
activesupport (= 8.0.3)
|
||||
railties (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
tsort (>= 0.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.3.1)
|
||||
ransack (4.4.1)
|
||||
activerecord (>= 7.2)
|
||||
activesupport (>= 7.2)
|
||||
i18n
|
||||
rdoc (6.16.1)
|
||||
rake (13.3.0)
|
||||
rdoc (6.14.2)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
tsort
|
||||
redis (5.4.1)
|
||||
redis (5.4.0)
|
||||
redis-client (>= 0.22.0)
|
||||
redis-client (0.26.2)
|
||||
redis-client (0.24.0)
|
||||
connection_pool
|
||||
regexp_parser (2.11.3)
|
||||
reline (0.6.3)
|
||||
regexp_parser (2.10.0)
|
||||
reline (0.6.2)
|
||||
io-console (~> 0.5)
|
||||
request_store (1.7.0)
|
||||
rack (>= 1.4)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rexml (3.4.4)
|
||||
rexml (3.4.1)
|
||||
rgeo (3.0.1)
|
||||
rgeo-activerecord (8.0.0)
|
||||
activerecord (>= 7.0)
|
||||
|
|
@ -487,13 +376,13 @@ GEM
|
|||
rqrcode_core (2.0.0)
|
||||
rspec-core (3.13.3)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.5)
|
||||
rspec-expectations (3.13.4)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.6)
|
||||
rspec-mocks (3.13.4)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (8.0.2)
|
||||
rspec-rails (8.0.0)
|
||||
actionpack (>= 7.2)
|
||||
activesupport (>= 7.2)
|
||||
railties (>= 7.2)
|
||||
|
|
@ -502,18 +391,18 @@ GEM
|
|||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-support (3.13.3)
|
||||
rswag-api (2.17.0)
|
||||
activesupport (>= 5.2, < 8.2)
|
||||
railties (>= 5.2, < 8.2)
|
||||
rswag-specs (2.17.0)
|
||||
activesupport (>= 5.2, < 8.2)
|
||||
json-schema (>= 2.2, < 7.0)
|
||||
railties (>= 5.2, < 8.2)
|
||||
rswag-api (2.16.0)
|
||||
activesupport (>= 5.2, < 8.1)
|
||||
railties (>= 5.2, < 8.1)
|
||||
rswag-specs (2.16.0)
|
||||
activesupport (>= 5.2, < 8.1)
|
||||
json-schema (>= 2.2, < 6.0)
|
||||
railties (>= 5.2, < 8.1)
|
||||
rspec-core (>= 2.14)
|
||||
rswag-ui (2.17.0)
|
||||
actionpack (>= 5.2, < 8.2)
|
||||
railties (>= 5.2, < 8.2)
|
||||
rubocop (1.82.1)
|
||||
rswag-ui (2.16.0)
|
||||
actionpack (>= 5.2, < 8.1)
|
||||
railties (>= 5.2, < 8.1)
|
||||
rubocop (1.75.6)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
|
|
@ -521,20 +410,20 @@ GEM
|
|||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.48.0, < 2.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.49.0)
|
||||
rubocop-ast (1.44.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.7)
|
||||
rubocop-rails (2.34.2)
|
||||
prism (~> 1.4)
|
||||
rubocop-rails (2.32.0)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
rubyzip (3.2.2)
|
||||
rubyzip (2.4.1)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.35.0)
|
||||
base64 (~> 0.2)
|
||||
|
|
@ -542,21 +431,21 @@ GEM
|
|||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 4.0)
|
||||
websocket (~> 1.0)
|
||||
sentry-rails (6.2.0)
|
||||
railties (>= 5.2.0)
|
||||
sentry-ruby (~> 6.2.0)
|
||||
sentry-ruby (6.2.0)
|
||||
sentry-rails (5.26.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.26.0)
|
||||
sentry-ruby (5.26.0)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
shoulda-matchers (6.5.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (8.0.10)
|
||||
sidekiq (8.0.4)
|
||||
connection_pool (>= 2.5.0)
|
||||
json (>= 2.9.0)
|
||||
logger (>= 1.6.2)
|
||||
rack (>= 3.1.0)
|
||||
redis-client (>= 0.23.2)
|
||||
sidekiq-cron (2.3.1)
|
||||
sidekiq-cron (2.3.0)
|
||||
cronex (>= 0.13.0)
|
||||
fugit (~> 1.8, >= 1.11.1)
|
||||
globalid (>= 1.0.1)
|
||||
|
|
@ -569,9 +458,6 @@ GEM
|
|||
simplecov_json_formatter (~> 0.1)
|
||||
simplecov-html (0.13.1)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
snaky_hash (2.0.3)
|
||||
hashie (>= 0.1.0, < 6)
|
||||
version_gem (>= 1.1.8, < 3)
|
||||
sprockets (4.2.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (>= 2.2.4, < 4)
|
||||
|
|
@ -582,19 +468,14 @@ GEM
|
|||
stackprof (0.2.27)
|
||||
stimulus-rails (1.3.4)
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.1.8)
|
||||
strong_migrations (2.5.1)
|
||||
activerecord (>= 7.1)
|
||||
super_diff (0.17.0)
|
||||
stringio (3.1.7)
|
||||
strong_migrations (2.3.0)
|
||||
activerecord (>= 7)
|
||||
super_diff (0.16.0)
|
||||
attr_extras (>= 6.2.4)
|
||||
diff-lcs
|
||||
patience_diff
|
||||
swd (2.0.3)
|
||||
activesupport (>= 3)
|
||||
attr_required (>= 0.0.5)
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
tailwindcss-rails (3.3.2)
|
||||
tailwindcss-rails (3.3.1)
|
||||
railties (>= 7.0.0)
|
||||
tailwindcss-ruby (~> 3.0)
|
||||
tailwindcss-ruby (3.4.17)
|
||||
|
|
@ -604,30 +485,21 @@ GEM
|
|||
tailwindcss-ruby (3.4.17-x86_64-darwin)
|
||||
tailwindcss-ruby (3.4.17-x86_64-linux)
|
||||
thor (1.4.0)
|
||||
timeout (0.4.4)
|
||||
tsort (0.2.0)
|
||||
turbo-rails (2.0.20)
|
||||
timeout (0.4.3)
|
||||
turbo-rails (2.0.16)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unicode (0.4.4.5)
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.2.0)
|
||||
uri (1.1.1)
|
||||
unicode-display_width (3.1.4)
|
||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||
unicode-emoji (4.0.4)
|
||||
uri (1.0.3)
|
||||
useragent (0.16.11)
|
||||
validate_url (1.0.15)
|
||||
activemodel (>= 3.0.0)
|
||||
public_suffix
|
||||
version_gem (1.1.9)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
webfinger (2.1.3)
|
||||
activesupport
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
webmock (3.26.1)
|
||||
webmock (3.25.1)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
|
|
@ -637,12 +509,8 @@ GEM
|
|||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
with_advisory_lock (7.0.2)
|
||||
activerecord (>= 7.2)
|
||||
zeitwerk (>= 2.7)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
yaml (0.4.0)
|
||||
zeitwerk (2.7.3)
|
||||
|
||||
PLATFORMS
|
||||
|
|
@ -654,7 +522,7 @@ PLATFORMS
|
|||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
activerecord-postgis-adapter (= 11.0)
|
||||
activerecord-postgis-adapter
|
||||
aws-sdk-core (~> 3.215.1)
|
||||
aws-sdk-kms (~> 1.96.0)
|
||||
aws-sdk-s3 (~> 1.177.0)
|
||||
|
|
@ -663,9 +531,8 @@ DEPENDENCIES
|
|||
bundler-audit
|
||||
capybara
|
||||
chartkick
|
||||
connection_pool (< 3)
|
||||
data_migrate
|
||||
database_consistency (>= 2.0.5)
|
||||
database_consistency
|
||||
debug
|
||||
devise
|
||||
dotenv-rails
|
||||
|
|
@ -676,60 +543,53 @@ DEPENDENCIES
|
|||
geocoder!
|
||||
gpx
|
||||
groupdate
|
||||
h3 (~> 3.7)
|
||||
httparty
|
||||
importmap-rails
|
||||
jwt (~> 2.8)
|
||||
kaminari
|
||||
lograge
|
||||
oj
|
||||
omniauth-github (~> 2.0.0)
|
||||
omniauth-google-oauth2
|
||||
omniauth-rails_csrf_protection
|
||||
omniauth_openid_connect
|
||||
parallel
|
||||
pg
|
||||
prometheus_exporter
|
||||
pry-byebug
|
||||
pry-rails
|
||||
puma
|
||||
pundit (>= 2.5.1)
|
||||
pundit
|
||||
rails (~> 8.0)
|
||||
rails_icons
|
||||
rails_pulse
|
||||
redis
|
||||
rexml
|
||||
rgeo
|
||||
rgeo-activerecord (~> 8.0.0)
|
||||
rgeo-activerecord
|
||||
rgeo-geojson
|
||||
rqrcode (~> 3.0)
|
||||
rspec-rails (>= 8.0.1)
|
||||
rspec-rails
|
||||
rswag-api
|
||||
rswag-specs
|
||||
rswag-ui
|
||||
rubocop-rails (>= 2.33.4)
|
||||
rubyzip (~> 3.2)
|
||||
rubocop-rails
|
||||
rubyzip (~> 2.4)
|
||||
selenium-webdriver
|
||||
sentry-rails (>= 5.27.0)
|
||||
sentry-rails
|
||||
sentry-ruby
|
||||
shoulda-matchers
|
||||
sidekiq (= 8.0.10)
|
||||
sidekiq-cron (>= 2.3.1)
|
||||
sidekiq
|
||||
sidekiq-cron
|
||||
sidekiq-limit_fetch
|
||||
simplecov
|
||||
sprockets-rails
|
||||
stackprof
|
||||
stimulus-rails
|
||||
strong_migrations (>= 2.4.0)
|
||||
strong_migrations
|
||||
super_diff
|
||||
tailwindcss-rails (= 3.3.2)
|
||||
turbo-rails (>= 2.0.17)
|
||||
tailwindcss-rails
|
||||
turbo-rails
|
||||
tzinfo-data
|
||||
webmock
|
||||
with_advisory_lock
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.4.6p54
|
||||
ruby 3.4.1p0
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.21
|
||||
|
|
|
|||
1
Procfile
|
|
@ -1,3 +1,2 @@
|
|||
release: bundle exec rails db:migrate
|
||||
web: bundle exec puma -C config/puma.rb
|
||||
worker: bundle exec sidekiq -C config/sidekiq.yml
|
||||
|
|
|
|||
26
README.md
|
|
@ -2,21 +2,20 @@
|
|||
|
||||
[](https://discord.gg/pHsBjpt5J8) | [](https://ko-fi.com/H2H3IDYDD) | [](https://www.patreon.com/freika)
|
||||
|
||||
[](https://app.circleci.com/pipelines/github/Freika/dawarich)
|
||||
|
||||
---
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||

|
||||

|
||||
*Map View*
|
||||
|
||||

|
||||
*Family Page*
|
||||
|
||||

|
||||

|
||||
*Statistics Overview*
|
||||
|
||||

|
||||
*Trips page*
|
||||

|
||||
*Imports page*
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -29,9 +28,6 @@ It enables you to:
|
|||
|
||||
- Track your location history.
|
||||
- Visualize your data on an interactive map.
|
||||
- Create trips and analyze your travel history.
|
||||
- Share your location with family members.
|
||||
- Integrate with photo management apps like Immich and Photoprism to visualize geotagged photos.
|
||||
- Import your location history from Google Maps Timeline, OwnTracks, GPX, GeoJSON and some other sources
|
||||
- Explore statistics like the number of countries and cities visited, total distance traveled, and more!
|
||||
|
||||
|
|
@ -71,14 +67,12 @@ Simply install one of the supported apps on your device and configure it to send
|
|||
1. Clone the repository.
|
||||
2. Run the following command to start the app:
|
||||
```bash
|
||||
docker compose -f docker/docker-compose.yml up
|
||||
docker-compose -f docker/docker-compose.yml up
|
||||
```
|
||||
3. Access the app at `http://localhost:3000`.
|
||||
|
||||
⏹️ **To stop the app**, press `Ctrl+C`.
|
||||
|
||||
You can use default values or create a `.env` file based on `.env.example` to customize your setup.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 How to Install Dawarich
|
||||
|
|
@ -105,11 +99,6 @@ Feel free to change them in the account settings.
|
|||
- Lines between points
|
||||
- Fog of War
|
||||
|
||||
### 👪 Family Sharing
|
||||
- Share your location with family members.
|
||||
- View locations of family members on the map (with their consent).
|
||||
- Each family member can enable or disable location sharing individually.
|
||||
|
||||
### 🔵 Areas
|
||||
- Draw areas on the map so Dawarich could suggest your visits there.
|
||||
|
||||
|
|
@ -120,6 +109,7 @@ Feel free to change them in the account settings.
|
|||
- Analyze your travel history: number of countries/cities visited, distance traveled, and time spent, broken down by year and month.
|
||||
|
||||
### ✈️ Trips
|
||||
|
||||
- Create a trip to visualize your travels between two points in time. You'll be able to see the route, distance, and time spent, and also add notes to your trip. If you have Immich or Photoprism integration, you'll also be able to see photos from your trips!
|
||||
|
||||
### 📸 Integrations
|
||||
|
|
|
|||
5
app.json
|
|
@ -5,6 +5,11 @@
|
|||
{ "url": "https://github.com/heroku/heroku-buildpack-nodejs.git" },
|
||||
{ "url": "https://github.com/heroku/heroku-buildpack-ruby.git" }
|
||||
],
|
||||
"scripts": {
|
||||
"dokku": {
|
||||
"predeploy": "bundle exec rails db:migrate"
|
||||
}
|
||||
},
|
||||
"healthchecks": {
|
||||
"web": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
|
||||
<title>Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917</title>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/>
|
||||
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
|
||||
</g>
|
||||
<g id="_Group_" data-name="<Group>">
|
||||
<g id="_Group_2" data-name="<Group>">
|
||||
<g id="_Group_3" data-name="<Group>">
|
||||
<path id="_Path_" data-name="<Path>" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/>
|
||||
<path id="_Path_2" data-name="<Path>" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M42.30227,27.13965h-4.7334l-1.13672,3.35645H34.42727l4.4834-12.418h2.083l4.4834,12.418H43.438ZM38.0591,25.59082h3.752l-1.84961-5.44727h-.05176Z" style="fill: #fff"/>
|
||||
<path d="M55.15969,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H48.4302v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.645,21.34766,55.15969,23.16406,55.15969,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30227,29.01563,53.24953,27.81934,53.24953,25.96973Z" style="fill: #fff"/>
|
||||
<path d="M65.12453,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H58.395v1.50586h.03418A3.21162,3.21162,0,0,1,61.312,21.34766C63.60988,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26711,29.01563,63.21438,27.81934,63.21438,25.96973Z" style="fill: #fff"/>
|
||||
<path d="M71.71047,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.626,3.60645,3.44238,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/>
|
||||
<path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z" style="fill: #fff"/>
|
||||
<path d="M86.065,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72609,30.6084,86.065,28.82617,86.065,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40039,1.16211-2.40039,3.10742c0,1.96191.89453,3.10645,2.40039,3.10645S92.76027,27.93164,92.76027,25.96973Z" style="fill: #fff"/>
|
||||
<path d="M96.18606,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z" style="fill: #fff"/>
|
||||
<path d="M109.3843,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10207,25.13477Z" style="fill: #fff"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="_Group_4" data-name="<Group>">
|
||||
<g>
|
||||
<path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z" style="fill: #fff"/>
|
||||
<path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z" style="fill: #fff"/>
|
||||
<path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z" style="fill: #fff"/>
|
||||
<path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M59.09377,8.437h.88867v6.26074h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z" style="fill: #fff"/>
|
||||
<path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z" style="fill: #fff"/>
|
||||
<path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z" style="fill: #fff"/>
|
||||
<path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z" style="fill: #fff"/>
|
||||
<path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z" style="fill: #fff"/>
|
||||
<path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z" style="fill: #fff"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 11 KiB |
19
app/assets/images/favicon/site.webmanifest.erb
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "Dawarich",
|
||||
"short_name": "Dawarich",
|
||||
"icons": [
|
||||
{
|
||||
"src": "<%= asset_path 'favicon/android-chrome-192x192.png' %>",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "<%= asset_path 'favicon/android-chrome-512x512.png' %>",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
|
@ -27,13 +27,9 @@
|
|||
/* Style for the settings panel */
|
||||
.leaflet-settings-panel {
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
||||
position: absolute !important;
|
||||
top: 10px !important;
|
||||
left: 60px !important;
|
||||
transform: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.leaflet-settings-panel label {
|
||||
|
|
@ -105,63 +101,3 @@
|
|||
content: '✅';
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* Flash message animations */
|
||||
@keyframes slideInFromRight {
|
||||
0% {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutToRight {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Family feature specific styles */
|
||||
.family-member-card {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.family-member-card:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.invitation-card {
|
||||
border-left: 4px solid #f59e0b;
|
||||
}
|
||||
|
||||
.family-invitation-form {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
/* Loading states */
|
||||
.btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
|
||||
*/
|
||||
@import 'actiontext.css';
|
||||
@import 'leaflet_theme.css';
|
||||
|
||||
@layer components {
|
||||
.fade-out {
|
||||
|
|
@ -24,8 +23,7 @@
|
|||
|
||||
/* Leaflet Panel Styles */
|
||||
.leaflet-right-panel {
|
||||
margin-top: 80px;
|
||||
/* Give space for controls above */
|
||||
margin-top: 80px; /* Give space for controls above */
|
||||
margin-right: 10px;
|
||||
transform: none;
|
||||
transition: right 0.3s ease-in-out;
|
||||
|
|
@ -53,12 +51,10 @@
|
|||
transform: scale(1);
|
||||
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.3);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.5);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.3);
|
||||
|
|
@ -75,55 +71,45 @@
|
|||
right: 310px;
|
||||
}
|
||||
|
||||
.leaflet-control-button {
|
||||
background-color: white !important;
|
||||
color: #374151 !important;
|
||||
}
|
||||
|
||||
.leaflet-control-button:hover {
|
||||
background-color: #f3f4f6 !important;
|
||||
}
|
||||
|
||||
/* Drawer Panel Styles */
|
||||
.leaflet-drawer {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 70px;
|
||||
/* Position to the left of the control buttons with margin */
|
||||
width: 24rem;
|
||||
max-height: calc(100% - 20px);
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 338px;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 8px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: scale(0.95);
|
||||
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out, visibility 0.2s;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
z-index: 450;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
height: auto;
|
||||
/* Make height fit content */
|
||||
cursor: default;
|
||||
/* Override map cursor */
|
||||
}
|
||||
|
||||
.leaflet-drawer * {
|
||||
cursor: default;
|
||||
/* Ensure all children have default cursor */
|
||||
}
|
||||
|
||||
.leaflet-drawer a,
|
||||
.leaflet-drawer button,
|
||||
.leaflet-drawer .btn,
|
||||
.leaflet-drawer input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
/* Interactive elements get pointer cursor */
|
||||
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.leaflet-drawer.open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: scale(1);
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* Controls remain in place - no transition needed */
|
||||
/* Controls transition */
|
||||
.leaflet-control-layers,
|
||||
.leaflet-control-button,
|
||||
.toggle-panel-button {
|
||||
transition: right 0.3s ease-in-out;
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
.controls-shifted {
|
||||
right: 338px !important;
|
||||
}
|
||||
|
||||
/* Selection Tool Styles */
|
||||
.leaflet-control-custom {
|
||||
background-color: white;
|
||||
|
|
@ -148,61 +134,6 @@
|
|||
|
||||
/* Cancel Selection Button */
|
||||
#cancel-selection-button {
|
||||
margin-bottom: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Emoji Picker Styles */
|
||||
em-emoji-picker {
|
||||
--color-border-over: rgba(0, 0, 0, 0.1);
|
||||
--color-border: rgba(0, 0, 0, 0.05);
|
||||
--font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--rgb-accent: 96, 165, 250;
|
||||
/* Blue accent to match application */
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
max-width: 400px;
|
||||
min-width: 318px;
|
||||
resize: horizontal;
|
||||
overflow: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Dark mode support for emoji picker */
|
||||
[data-theme="dark"] em-emoji-picker,
|
||||
html.dark em-emoji-picker {
|
||||
--color-border-over: rgba(255, 255, 255, 0.1);
|
||||
--color-border: rgba(255, 255, 255, 0.05);
|
||||
--rgb-accent: 96, 165, 250;
|
||||
}
|
||||
|
||||
/* Responsive emoji picker on mobile */
|
||||
@media (max-width: 768px) {
|
||||
em-emoji-picker {
|
||||
max-width: 90vw;
|
||||
min-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Color Picker Styles */
|
||||
.color-input {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.color-input::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.color-input::-webkit-color-swatch {
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.color-input::-moz-color-swatch {
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
.leaflet-control-layers-toggle.leaflet-layerstree-named-toggle {
|
||||
margin: 2px 5px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.leaflet-layerstree-header input {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.leaflet-layerstree-header label {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.leaflet-layerstree-header-pointer,
|
||||
.leaflet-layerstree-expand-collapse {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.leaflet-layerstree-children {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.leaflet-layerstree-children-nopad {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.leaflet-layerstree-hide,
|
||||
.leaflet-layerstree-nevershow {
|
||||
display: none;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
line-height: 1.5rem!important;
|
||||
}
|
||||
|
|
@ -1,347 +0,0 @@
|
|||
/* Leaflet Theme Styles - Light and Dark mode support */
|
||||
|
||||
/* CSS Custom Properties for Light Theme */
|
||||
[data-theme="light"] {
|
||||
--leaflet-bg-color: #ffffff;
|
||||
--leaflet-text-color: #000000;
|
||||
--leaflet-border-color: #e5e7eb;
|
||||
--leaflet-shadow-color: rgba(0, 0, 0, 0.1);
|
||||
--leaflet-hover-color: #f3f4f6;
|
||||
--leaflet-link-color: #0066cc;
|
||||
--leaflet-scale-bg: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
/* CSS Custom Properties for Dark Theme */
|
||||
[data-theme="dark"] {
|
||||
--leaflet-bg-color: #374151;
|
||||
--leaflet-text-color: #ffffff;
|
||||
--leaflet-border-color: #4b5563;
|
||||
--leaflet-shadow-color: rgba(0, 0, 0, 0.3);
|
||||
--leaflet-hover-color: #4b5563;
|
||||
--leaflet-link-color: #66b3ff;
|
||||
--leaflet-scale-bg: rgba(55, 65, 81, 0.9);
|
||||
}
|
||||
|
||||
/* Leaflet default controls theme override */
|
||||
.leaflet-control-layers,
|
||||
.leaflet-control-zoom,
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-bar a,
|
||||
.leaflet-control-layers-toggle,
|
||||
.leaflet-control-layers-list,
|
||||
.leaflet-control-draw {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
border-color: var(--leaflet-border-color) !important;
|
||||
box-shadow: 0 1px 4px var(--leaflet-shadow-color) !important;
|
||||
|
||||
}
|
||||
|
||||
/* Leaflet zoom buttons */
|
||||
.leaflet-control-zoom a {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
border-bottom: 1px solid var(--leaflet-border-color) !important;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom a:hover {
|
||||
background-color: var(--leaflet-hover-color) !important;
|
||||
}
|
||||
|
||||
/* Leaflet layer control */
|
||||
.leaflet-control-layers {
|
||||
border: none !important;
|
||||
border-radius: 0.5rem !important;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.leaflet-control-layers-expanded {
|
||||
padding: 1rem !important;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
/* Hide the toggle icon when expanded */
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.leaflet-control-layers-toggle {
|
||||
width: 44px !important;
|
||||
height: 44px !important;
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
border-radius: 0.5rem !important;
|
||||
/* Replace default icon with custom SVG */
|
||||
background-image: none !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.leaflet-control-layers-toggle:hover {
|
||||
background-color: var(--leaflet-hover-color) !important;
|
||||
}
|
||||
|
||||
.leaflet-control-layers-toggle::before {
|
||||
content: '' !important;
|
||||
display: block !important;
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"/><path d="M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12"/><path d="M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17"/></svg>') !important;
|
||||
background-size: contain !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position: center !important;
|
||||
}
|
||||
|
||||
/* Dark theme - use white stroke for the icon */
|
||||
[data-theme="dark"] .leaflet-control-layers-toggle::before {
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"/><path d="M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12"/><path d="M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17"/></svg>') !important;
|
||||
}
|
||||
|
||||
/* Light theme - use black stroke for the icon */
|
||||
[data-theme="light"] .leaflet-control-layers-toggle::before {
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"/><path d="M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12"/><path d="M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17"/></svg>') !important;
|
||||
}
|
||||
|
||||
/* Layer list styling */
|
||||
.leaflet-control-layers-list {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.leaflet-control-layers-base,
|
||||
.leaflet-control-layers-overlays {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.leaflet-control-layers-separator {
|
||||
height: 1px;
|
||||
margin: 0.75rem 0;
|
||||
background-color: var(--leaflet-border-color);
|
||||
}
|
||||
|
||||
/* Label styling */
|
||||
.leaflet-control-layers label {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
margin-bottom: 0 !important;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
}
|
||||
|
||||
.leaflet-control-layers label:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.leaflet-control-layers label span {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
/* Custom Checkbox/Radio styling using DaisyUI/Tailwind logic */
|
||||
.leaflet-control-layers input[type="checkbox"],
|
||||
.leaflet-control-layers input[type="radio"] {
|
||||
appearance: none;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border: 1px solid var(--leaflet-border-color);
|
||||
border-radius: 0.25rem;
|
||||
/* Rounded for checkbox */
|
||||
background-color: var(--leaflet-bg-color);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
margin: 0 !important;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.leaflet-control-layers input[type="radio"] {
|
||||
border-radius: 9999px;
|
||||
/* Circle for radio */
|
||||
}
|
||||
|
||||
.leaflet-control-layers input[type="checkbox"]:checked,
|
||||
.leaflet-control-layers input[type="radio"]:checked {
|
||||
background-color: var(--leaflet-link-color);
|
||||
border-color: var(--leaflet-link-color);
|
||||
}
|
||||
|
||||
/* Checkbox checkmark */
|
||||
.leaflet-control-layers input[type="checkbox"]:checked::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0.65rem;
|
||||
height: 0.65rem;
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
/* Radio dot */
|
||||
.leaflet-control-layers input[type="radio"]:checked::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
/* Leaflet Draw controls */
|
||||
.leaflet-draw-toolbar a {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
border-bottom: 1px solid var(--leaflet-border-color) !important;
|
||||
}
|
||||
|
||||
.leaflet-draw-toolbar a:hover {
|
||||
background-color: var(--leaflet-hover-color) !important;
|
||||
}
|
||||
|
||||
.leaflet-draw-actions a {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
}
|
||||
|
||||
/* Leaflet popups */
|
||||
.leaflet-popup-content-wrapper {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
}
|
||||
|
||||
/* Attribution control */
|
||||
.leaflet-control-attribution a {
|
||||
color: var(--leaflet-link-color) !important;
|
||||
}
|
||||
|
||||
/* Custom control buttons */
|
||||
.leaflet-control-button,
|
||||
.add-visit-button,
|
||||
.leaflet-bar button {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
border: 1px solid var(--leaflet-border-color) !important;
|
||||
box-shadow: 0 1px 4px var(--leaflet-shadow-color) !important;
|
||||
}
|
||||
|
||||
.leaflet-control-button:hover,
|
||||
.add-visit-button:hover,
|
||||
.leaflet-bar button:hover {
|
||||
background-color: var(--leaflet-hover-color) !important;
|
||||
}
|
||||
|
||||
/* Any other custom controls */
|
||||
.leaflet-top .leaflet-control button,
|
||||
.leaflet-bottom .leaflet-control button,
|
||||
.leaflet-left .leaflet-control button,
|
||||
.leaflet-right .leaflet-control button {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
border: 1px solid var(--leaflet-border-color) !important;
|
||||
}
|
||||
|
||||
/* Location search button */
|
||||
.location-search-toggle,
|
||||
#location-search-toggle {
|
||||
background-color: var(--leaflet-bg-color) !important;
|
||||
color: var(--leaflet-text-color) !important;
|
||||
border: 1px solid var(--leaflet-border-color) !important;
|
||||
box-shadow: 0 1px 4px var(--leaflet-shadow-color) !important;
|
||||
}
|
||||
|
||||
.location-search-toggle:hover,
|
||||
#location-search-toggle:hover {
|
||||
background-color: var(--leaflet-hover-color) !important;
|
||||
}
|
||||
|
||||
/* Distance scale control */
|
||||
.leaflet-control-scale {
|
||||
background: var(--leaflet-scale-bg) !important;
|
||||
border-radius: 3px !important;
|
||||
padding: 2px !important;
|
||||
}
|
||||
|
||||
/* Family member tooltip - dark styled like the visit popup */
|
||||
.leaflet-tooltip.family-member-tooltip {
|
||||
background-color: #374151 !important;
|
||||
color: #ffffff !important;
|
||||
border: 1px solid #4b5563 !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 4px 8px !important;
|
||||
font-size: 11px !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
.leaflet-tooltip.family-member-tooltip::before {
|
||||
border-top-color: #374151 !important;
|
||||
}
|
||||
|
||||
/* Family member popup - just override colors, keep default layout */
|
||||
.leaflet-popup-content-wrapper:has(.family-member-popup) {
|
||||
background-color: #1f2937 !important;
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper:has(.family-member-popup)+.leaflet-popup-tip {
|
||||
background-color: #1f2937 !important;
|
||||
}
|
||||
|
||||
/* Family member marker pulse animation for recent updates */
|
||||
@keyframes family-marker-pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 0 0 10px rgba(16, 185, 129, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.family-member-marker-recent {
|
||||
animation: family-marker-pulse 2s infinite;
|
||||
border-radius: 50% !important;
|
||||
}
|
||||
|
||||
.family-member-marker-recent .leaflet-marker-icon>div {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2), 0 0 0 0 rgba(16, 185, 129, 0.7);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Fix bottom controls being cut off */
|
||||
.leaflet-bottom {
|
||||
padding-bottom: 10px !important;
|
||||
transition: padding-bottom 0.3s ease;
|
||||
}
|
||||
|
||||
.leaflet-bottom.leaflet-left {
|
||||
padding-left: 10px !important;
|
||||
}
|
||||
|
||||
.leaflet-bottom.leaflet-right {
|
||||
padding-right: 10px !important;
|
||||
}
|
||||
|
||||
/* DaisyUI tooltips on map buttons - ensure they appear above date navigation (z-index: 9999) */
|
||||
.tooltip:before,
|
||||
.tooltip:after {
|
||||
z-index: 10000 !important;
|
||||
}
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
/* Maps V2 Styles */
|
||||
|
||||
/* Loading Overlay */
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.loading-overlay.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #e5e7eb;
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Popup Styles */
|
||||
.point-popup {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.popup-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.popup-row .label {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.popup-row .value {
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
/* MapLibre Popup Theme Support */
|
||||
.maplibregl-popup-content {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Larger close button */
|
||||
.maplibregl-popup-close-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
right: 4px;
|
||||
top: 4px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.maplibregl-popup-close-button:hover {
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* Light theme (default) */
|
||||
.maplibregl-popup-content {
|
||||
background-color: #ffffff;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.maplibregl-popup-close-button {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.maplibregl-popup-close-button:hover {
|
||||
background-color: #f3f4f6;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.maplibregl-popup-tip {
|
||||
border-top-color: #ffffff;
|
||||
}
|
||||
|
||||
/* Dark theme */
|
||||
html[data-theme="dark"] .maplibregl-popup-content,
|
||||
html.dark .maplibregl-popup-content {
|
||||
background-color: #1f2937;
|
||||
color: #f9fafb;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .maplibregl-popup-close-button,
|
||||
html.dark .maplibregl-popup-close-button {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .maplibregl-popup-close-button:hover,
|
||||
html.dark .maplibregl-popup-close-button:hover {
|
||||
background-color: #374151;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .maplibregl-popup-tip,
|
||||
html.dark .maplibregl-popup-tip {
|
||||
border-top-color: #1f2937;
|
||||
}
|
||||
|
||||
/* Connection Indicator */
|
||||
.connection-indicator {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 8px 16px;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: none; /* Hidden by default, shown when family sharing is active */
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
z-index: 20;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
/* Show connection indicator when family sharing is active */
|
||||
.connection-indicator.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.indicator-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #ef4444;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.connection-indicator.connected .indicator-dot {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.connection-indicator.connected .indicator-text::before {
|
||||
content: 'Connected';
|
||||
}
|
||||
|
||||
.connection-indicator.disconnected .indicator-text::before {
|
||||
content: 'Connecting...';
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,286 +0,0 @@
|
|||
/* Maps V2 Control Panel Styles */
|
||||
|
||||
.map-control-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -480px; /* Hidden by default */
|
||||
width: 480px;
|
||||
height: 100%;
|
||||
background: oklch(var(--b1));
|
||||
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
|
||||
z-index: 9999;
|
||||
transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.map-control-panel.open {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* Vertical Tab Bar */
|
||||
.panel-tabs {
|
||||
width: 64px;
|
||||
background: oklch(var(--b2));
|
||||
border-right: 1px solid oklch(var(--bc) / 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
color: oklch(var(--bc) / 0.6);
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: oklch(var(--b3));
|
||||
color: oklch(var(--bc));
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: oklch(var(--p));
|
||||
color: oklch(var(--pc));
|
||||
}
|
||||
|
||||
.tab-btn.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 24px;
|
||||
background: oklch(var(--p));
|
||||
border-radius: 2px 0 0 2px;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Panel Content */
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid oklch(var(--bc) / 0.1);
|
||||
background: oklch(var(--b1));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: oklch(var(--bc));
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* Tab Content */
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
.panel-body::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.panel-body::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.panel-body::-webkit-scrollbar-thumb {
|
||||
background: oklch(var(--bc) / 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.panel-body::-webkit-scrollbar-thumb:hover {
|
||||
background: oklch(var(--bc) / 0.3);
|
||||
}
|
||||
|
||||
/* Toggle Focus State - Remove all focus indicators */
|
||||
.toggle:focus,
|
||||
.toggle:focus-visible,
|
||||
.toggle:focus-within {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
border-color: inherit !important;
|
||||
}
|
||||
|
||||
/* Override DaisyUI toggle focus styles */
|
||||
.toggle:focus-visible:checked,
|
||||
.toggle:checked:focus,
|
||||
.toggle:checked:focus-visible {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Ensure no outline on the toggle container */
|
||||
.form-control .toggle:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/* Prevent indeterminate visual state on toggles */
|
||||
.toggle:indeterminate {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Ensure smooth toggle transitions without intermediate states */
|
||||
.toggle {
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle:checked {
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Remove any active/pressed state that might cause intermediate appearance */
|
||||
.toggle:active,
|
||||
.toggle:active:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Responsive Breakpoints */
|
||||
|
||||
/* Large tablets and smaller desktops (1024px - 1280px) */
|
||||
@media (max-width: 1280px) {
|
||||
.map-control-panel {
|
||||
width: 420px;
|
||||
right: -420px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablets (768px - 1024px) */
|
||||
@media (max-width: 1024px) {
|
||||
.map-control-panel {
|
||||
width: 380px;
|
||||
right: -380px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Small tablets and large phones (640px - 768px) */
|
||||
@media (max-width: 768px) {
|
||||
.map-control-panel {
|
||||
width: 95%;
|
||||
right: -95%;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile phones (< 640px) */
|
||||
@media (max-width: 640px) {
|
||||
.map-control-panel {
|
||||
width: 100%;
|
||||
right: -100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.panel-tabs {
|
||||
width: 56px;
|
||||
padding: 12px 0;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* Reduce spacing on mobile */
|
||||
.space-y-4 > * + * {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.space-y-6 > * + * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Very small phones (< 375px) */
|
||||
@media (max-width: 375px) {
|
||||
.panel-tabs {
|
||||
width: 52px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-big-down-icon lucide-arrow-big-down"><path d="M15 11a1 1 0 0 0 1 1h2.939a1 1 0 0 1 .75 1.811l-6.835 6.836a1.207 1.207 0 0 1-1.707 0L4.31 13.81a1 1 0 0 1 .75-1.811H8a1 1 0 0 0 1-1V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1z"/></svg>
|
||||
|
Before Width: | Height: | Size: 429 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar-plus2-icon lucide-calendar-plus-2"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/><path d="M10 16h4"/><path d="M12 14v4"/></svg>
|
||||
|
Before Width: | Height: | Size: 399 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chart-column-icon lucide-chart-column"><path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg>
|
||||
|
Before Width: | Height: | Size: 344 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down-icon lucide-chevron-down"><path d="m6 9 6 6 6-6"/></svg>
|
||||
|
Before Width: | Height: | Size: 272 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-left-icon lucide-chevron-left"><path d="m15 18-6-6 6-6"/></svg>
|
||||
|
Before Width: | Height: | Size: 274 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right-icon lucide-chevron-right"><path d="m9 18 6-6-6-6"/></svg>
|
||||
|
Before Width: | Height: | Size: 275 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-up-icon lucide-chevron-up"><path d="m18 15-6-6-6 6"/></svg>
|
||||
|
Before Width: | Height: | Size: 270 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-alert-icon lucide-circle-alert"><circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/></svg>
|
||||
|
Before Width: | Height: | Size: 360 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check-icon lucide-circle-check"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>
|
||||
|
Before Width: | Height: | Size: 305 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-plus-icon lucide-circle-plus"><circle cx="12" cy="12" r="10"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>
|
||||
|
Before Width: | Height: | Size: 316 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x-icon lucide-circle-x"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>
|
||||
|
Before Width: | Height: | Size: 313 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-goal-icon lucide-goal"><path d="M12 13V2l8 4-8 4"/><path d="M20.561 10.222a9 9 0 1 1-12.55-5.29"/><path d="M8.002 9.997a5 5 0 1 0 8.9 2.02"/></svg>
|
||||
|
Before Width: | Height: | Size: 350 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-grid2x2-icon lucide-grid-2x2"><path d="M12 3v18"/><path d="M3 12h18"/><rect x="3" y="3" width="18" height="18" rx="2"/></svg>
|
||||
|
Before Width: | Height: | Size: 328 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-heart-icon lucide-heart"><path d="M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5"/></svg>
|
||||
|
Before Width: | Height: | Size: 395 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-layers-icon lucide-layers"><path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"/><path d="M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12"/><path d="M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17"/></svg>
|
||||
|
Before Width: | Height: | Size: 526 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-lock-open-icon lucide-lock-open"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></svg>
|
||||
|
Before Width: | Height: | Size: 334 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail-icon lucide-mail"><path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7"/><rect x="2" y="4" width="20" height="16" rx="2"/></svg>
|
||||
|
Before Width: | Height: | Size: 332 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin-check-icon lucide-map-pin-check"><path d="M19.43 12.935c.357-.967.57-1.955.57-2.935a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 32.197 32.197 0 0 0 .813-.728"/><circle cx="12" cy="10" r="3"/><path d="m16 18 2 2 4-4"/></svg>
|
||||
|
Before Width: | Height: | Size: 457 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle-question-mark-icon lucide-message-circle-question-mark"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
|
||||
|
Before Width: | Height: | Size: 485 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pocket-knife-icon lucide-pocket-knife"><path d="M3 2v1c0 1 2 1 2 2S3 6 3 7s2 1 2 2-2 1-2 2 2 1 2 2"/><path d="M18 6h.01"/><path d="M6 18h.01"/><path d="M20.83 8.83a4 4 0 0 0-5.66-5.66l-12 12a4 4 0 1 0 5.66 5.66Z"/><path d="M18 11.66V22a4 4 0 0 0 4-4V6"/></svg>
|
||||
|
Before Width: | Height: | Size: 463 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-ccw-icon lucide-rotate-ccw"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
|
||||
|
Before Width: | Height: | Size: 325 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-route-icon lucide-route"><circle cx="6" cy="19" r="3"/><path d="M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15"/><circle cx="18" cy="5" r="3"/></svg>
|
||||
|
Before Width: | Height: | Size: 358 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-save-icon lucide-save"><path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/><path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/><path d="M7 3v4a1 1 0 0 0 1 1h7"/></svg>
|
||||
|
Before Width: | Height: | Size: 429 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
|
||||
|
Before Width: | Height: | Size: 295 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-settings-icon lucide-settings"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
|
Before Width: | Height: | Size: 610 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shield-check-icon lucide-shield-check"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></svg>
|
||||
|
Before Width: | Height: | Size: 447 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-dashed-mouse-pointer-icon lucide-square-dashed-mouse-pointer"><path d="M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"/><path d="M5 3a2 2 0 0 0-2 2"/><path d="M19 3a2 2 0 0 1 2 2"/><path d="M5 21a2 2 0 0 1-2-2"/><path d="M9 3h1"/><path d="M9 21h2"/><path d="M14 3h1"/><path d="M3 9v1"/><path d="M21 9v2"/><path d="M3 14v1"/></svg>
|
||||
|
Before Width: | Height: | Size: 623 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pen-icon lucide-square-pen"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>
|
||||
|
Before Width: | Height: | Size: 445 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2-icon lucide-trash-2"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
||||
|
Before Width: | Height: | Size: 398 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert-icon lucide-triangle-alert"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
|
||||
|
Before Width: | Height: | Size: 377 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-icon lucide-user"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
|
Before Width: | Height: | Size: 315 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-users-icon lucide-users"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><path d="M16 3.128a4 4 0 0 1 0 7.744"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><circle cx="9" cy="7" r="4"/></svg>
|
||||
|
Before Width: | Height: | Size: 393 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
|
Before Width: | Height: | Size: 270 B |
|
|
@ -1,14 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FamilyLocationsChannel < ApplicationCable::Channel
|
||||
def subscribed
|
||||
return reject unless DawarichSettings.family_feature_enabled?
|
||||
return reject unless current_user.in_family?
|
||||
|
||||
stream_for current_user.family
|
||||
end
|
||||
|
||||
def unsubscribed
|
||||
# Any cleanup needed when channel is unsubscribed
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::AreasController < ApiController
|
||||
before_action :set_area, only: %i[show update destroy]
|
||||
before_action :set_area, only: %i[update destroy]
|
||||
|
||||
def index
|
||||
@areas = current_api_user.areas
|
||||
|
|
@ -9,10 +9,6 @@ class Api::V1::AreasController < ApiController
|
|||
render json: @areas, status: :ok
|
||||
end
|
||||
|
||||
def show
|
||||
render json: @area, status: :ok
|
||||
end
|
||||
|
||||
def create
|
||||
@area = current_api_user.areas.build(area_params)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Countries::VisitedCitiesController < ApiController
|
||||
include SafeTimestampParser
|
||||
|
||||
before_action :validate_params
|
||||
|
||||
def index
|
||||
start_at = safe_timestamp(params[:start_at])
|
||||
end_at = safe_timestamp(params[:end_at])
|
||||
start_at = DateTime.parse(params[:start_at]).to_i
|
||||
end_at = DateTime.parse(params[:end_at]).to_i
|
||||
|
||||
points = current_api_user
|
||||
.points
|
||||
.without_raw_data
|
||||
.where(timestamp: start_at..end_at)
|
||||
|
||||
render json: { data: CountriesAndCities.new(points).call }
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Families::LocationsController < ApiController
|
||||
before_action :ensure_family_feature_enabled!
|
||||
before_action :ensure_user_in_family!
|
||||
|
||||
def index
|
||||
family_locations = Families::Locations.new(current_api_user).call
|
||||
|
||||
render json: {
|
||||
locations: family_locations,
|
||||
updated_at: Time.current.iso8601,
|
||||
sharing_enabled: current_api_user.family_sharing_enabled?
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_user_in_family!
|
||||
return if current_api_user&.in_family?
|
||||
|
||||
render json: { error: 'User is not part of a family' }, status: :forbidden
|
||||
end
|
||||
end
|
||||
|
|
@ -2,85 +2,124 @@
|
|||
|
||||
class Api::V1::Maps::HexagonsController < ApiController
|
||||
skip_before_action :authenticate_api_key, if: :public_sharing_request?
|
||||
before_action :validate_bbox_params, except: [:bounds]
|
||||
before_action :set_user_and_dates
|
||||
|
||||
def index
|
||||
context = resolve_hexagon_context
|
||||
|
||||
result = Maps::HexagonRequestHandler.new(
|
||||
params: params,
|
||||
user: context[:user] || current_api_user,
|
||||
stat: context[:stat],
|
||||
start_date: context[:start_date],
|
||||
end_date: context[:end_date]
|
||||
).call
|
||||
service = Maps::HexagonGrid.new(hexagon_params)
|
||||
result = service.call
|
||||
|
||||
Rails.logger.debug "Hexagon service result: #{result['features']&.count || 0} features"
|
||||
render json: result
|
||||
rescue ActionController::ParameterMissing => e
|
||||
render json: { error: "Missing required parameter: #{e.param}" }, status: :bad_request
|
||||
rescue ActionController::BadRequest => e
|
||||
render json: { error: e.message }, status: :bad_request
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
render json: { error: 'Shared stats not found or no longer available' }, status: :not_found
|
||||
rescue Stats::CalculateMonth::PostGISError => e
|
||||
rescue Maps::HexagonGrid::BoundingBoxTooLargeError,
|
||||
Maps::HexagonGrid::InvalidCoordinatesError => e
|
||||
render json: { error: e.message }, status: :bad_request
|
||||
rescue Maps::HexagonGrid::PostGISError => e
|
||||
render json: { error: e.message }, status: :internal_server_error
|
||||
rescue StandardError => _e
|
||||
handle_service_error
|
||||
end
|
||||
|
||||
def bounds
|
||||
context = resolve_hexagon_context
|
||||
# Get the bounding box of user's points for the date range
|
||||
return render json: { error: 'No user found' }, status: :not_found unless @target_user
|
||||
return render json: { error: 'No date range specified' }, status: :bad_request unless @start_date && @end_date
|
||||
|
||||
result = Maps::BoundsCalculator.new(
|
||||
user: context[:user] || context[:target_user],
|
||||
start_date: context[:start_date],
|
||||
end_date: context[:end_date]
|
||||
).call
|
||||
# Convert dates to timestamps (handle both string and timestamp formats)
|
||||
start_timestamp = case @start_date
|
||||
when String
|
||||
# Check if it's a numeric string (timestamp) or date string
|
||||
if @start_date.match?(/^\d+$/)
|
||||
@start_date.to_i
|
||||
else
|
||||
Time.parse(@start_date).to_i
|
||||
end
|
||||
when Integer
|
||||
@start_date
|
||||
else
|
||||
@start_date.to_i
|
||||
end
|
||||
end_timestamp = case @end_date
|
||||
when String
|
||||
# Check if it's a numeric string (timestamp) or date string
|
||||
if @end_date.match?(/^\d+$/)
|
||||
@end_date.to_i
|
||||
else
|
||||
Time.parse(@end_date).to_i
|
||||
end
|
||||
when Integer
|
||||
@end_date
|
||||
else
|
||||
@end_date.to_i
|
||||
end
|
||||
|
||||
if result[:success]
|
||||
render json: result[:data]
|
||||
points_relation = @target_user.points.where(timestamp: start_timestamp..end_timestamp)
|
||||
point_count = points_relation.count
|
||||
|
||||
if point_count.positive?
|
||||
bounds_result = ActiveRecord::Base.connection.exec_query(
|
||||
"SELECT MIN(latitude) as min_lat, MAX(latitude) as max_lat,
|
||||
MIN(longitude) as min_lng, MAX(longitude) as max_lng
|
||||
FROM points
|
||||
WHERE user_id = $1
|
||||
AND timestamp BETWEEN $2 AND $3",
|
||||
'bounds_query',
|
||||
[@target_user.id, start_timestamp, end_timestamp]
|
||||
).first
|
||||
|
||||
render json: {
|
||||
min_lat: bounds_result['min_lat'].to_f,
|
||||
max_lat: bounds_result['max_lat'].to_f,
|
||||
min_lng: bounds_result['min_lng'].to_f,
|
||||
max_lng: bounds_result['max_lng'].to_f,
|
||||
point_count: point_count
|
||||
}
|
||||
else
|
||||
render json: {
|
||||
error: result[:error],
|
||||
point_count: result[:point_count]
|
||||
error: 'No data found for the specified date range',
|
||||
point_count: 0
|
||||
}, status: :not_found
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
render json: { error: 'Shared stats not found or no longer available' }, status: :not_found
|
||||
rescue ArgumentError => e
|
||||
render json: { error: e.message }, status: :bad_request
|
||||
rescue Maps::BoundsCalculator::NoUserFoundError => e
|
||||
render json: { error: e.message }, status: :not_found
|
||||
rescue Maps::BoundsCalculator::NoDateRangeError => e
|
||||
render json: { error: e.message }, status: :bad_request
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resolve_hexagon_context
|
||||
return resolve_public_sharing_context if public_sharing_request?
|
||||
|
||||
resolve_authenticated_context
|
||||
def bbox_params
|
||||
params.permit(:min_lon, :min_lat, :max_lon, :max_lat, :hex_size, :viewport_width, :viewport_height)
|
||||
end
|
||||
|
||||
def resolve_public_sharing_context
|
||||
stat = Stat.find_by(sharing_uuid: params[:uuid])
|
||||
raise ActiveRecord::RecordNotFound unless stat&.public_accessible?
|
||||
|
||||
{
|
||||
user: stat.user,
|
||||
start_date: Date.new(stat.year, stat.month, 1).beginning_of_day.iso8601,
|
||||
end_date: Date.new(stat.year, stat.month, 1).end_of_month.end_of_day.iso8601,
|
||||
stat: stat
|
||||
}
|
||||
def hexagon_params
|
||||
bbox_params.merge(
|
||||
user_id: @target_user&.id,
|
||||
start_date: @start_date,
|
||||
end_date: @end_date
|
||||
)
|
||||
end
|
||||
|
||||
def resolve_authenticated_context
|
||||
{
|
||||
user: current_api_user,
|
||||
start_date: params[:start_date],
|
||||
end_date: params[:end_date],
|
||||
stat: nil
|
||||
}
|
||||
def set_user_and_dates
|
||||
return set_public_sharing_context if params[:uuid].present?
|
||||
|
||||
set_authenticated_context
|
||||
end
|
||||
|
||||
def set_public_sharing_context
|
||||
@stat = Stat.find_by(sharing_uuid: params[:uuid])
|
||||
|
||||
unless @stat&.public_accessible?
|
||||
render json: {
|
||||
error: 'Shared stats not found or no longer available'
|
||||
}, status: :not_found and return
|
||||
end
|
||||
|
||||
@target_user = @stat.user
|
||||
@start_date = Date.new(@stat.year, @stat.month, 1).beginning_of_day.iso8601
|
||||
@end_date = Date.new(@stat.year, @stat.month, 1).end_of_month.end_of_day.iso8601
|
||||
end
|
||||
|
||||
def set_authenticated_context
|
||||
@target_user = current_api_user
|
||||
@start_date = params[:start_date]
|
||||
@end_date = params[:end_date]
|
||||
end
|
||||
|
||||
def handle_service_error
|
||||
|
|
@ -90,4 +129,15 @@ class Api::V1::Maps::HexagonsController < ApiController
|
|||
def public_sharing_request?
|
||||
params[:uuid].present?
|
||||
end
|
||||
|
||||
def validate_bbox_params
|
||||
required_params = %w[min_lon min_lat max_lon max_lat]
|
||||
missing_params = required_params.select { |param| params[param].blank? }
|
||||
|
||||
return unless missing_params.any?
|
||||
|
||||
render json: {
|
||||
error: "Missing required parameters: #{missing_params.join(', ')}"
|
||||
}, status: :bad_request
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,13 +5,9 @@ class Api::V1::Overland::BatchesController < ApiController
|
|||
before_action :validate_points_limit, only: %i[create]
|
||||
|
||||
def create
|
||||
Overland::PointsCreator.new(batch_params, current_api_user.id).call
|
||||
Overland::BatchCreatingJob.perform_later(batch_params, current_api_user.id)
|
||||
|
||||
render json: { result: 'ok' }, status: :created
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e) if defined?(Sentry)
|
||||
|
||||
render json: { error: 'Batch creation failed' }, status: :internal_server_error
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -5,13 +5,9 @@ class Api::V1::Owntracks::PointsController < ApiController
|
|||
before_action :validate_points_limit, only: %i[create]
|
||||
|
||||
def create
|
||||
OwnTracks::PointCreator.new(point_params, current_api_user.id).call
|
||||
Owntracks::PointCreatingJob.perform_later(point_params, current_api_user.id)
|
||||
|
||||
render json: [], status: :ok
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e) if defined?(Sentry)
|
||||
|
||||
render json: { error: 'Point creation failed' }, status: :internal_server_error
|
||||
render json: {}, status: :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class PlacesController < ApiController
|
||||
before_action :set_place, only: [:show, :update, :destroy]
|
||||
|
||||
def index
|
||||
@places = current_api_user.places.includes(:tags, :visits)
|
||||
|
||||
if params[:tag_ids].present?
|
||||
tag_ids = Array(params[:tag_ids])
|
||||
|
||||
# Separate numeric tag IDs from "untagged"
|
||||
numeric_tag_ids = tag_ids.reject { |id| id == 'untagged' }.map(&:to_i)
|
||||
include_untagged = tag_ids.include?('untagged')
|
||||
|
||||
if numeric_tag_ids.any? && include_untagged
|
||||
# Both tagged and untagged: use OR logic to preserve eager loading
|
||||
tagged_ids = current_api_user.places.with_tags(numeric_tag_ids).pluck(:id)
|
||||
untagged_ids = current_api_user.places.without_tags.pluck(:id)
|
||||
combined_ids = (tagged_ids + untagged_ids).uniq
|
||||
@places = current_api_user.places.includes(:tags, :visits).where(id: combined_ids)
|
||||
elsif numeric_tag_ids.any?
|
||||
# Only tagged places with ANY of the selected tags (OR logic)
|
||||
@places = @places.with_tags(numeric_tag_ids)
|
||||
elsif include_untagged
|
||||
# Only untagged places
|
||||
@places = @places.without_tags
|
||||
end
|
||||
end
|
||||
|
||||
# Support pagination (defaults to page 1 with all results if no page param)
|
||||
page = params[:page].presence || 1
|
||||
per_page = [params[:per_page]&.to_i || 100, 500].min
|
||||
|
||||
# Apply pagination only if page param is explicitly provided
|
||||
if params[:page].present?
|
||||
@places = @places.page(page).per(per_page)
|
||||
end
|
||||
|
||||
# Always set pagination headers for consistency
|
||||
if @places.respond_to?(:current_page)
|
||||
# Paginated collection
|
||||
response.set_header('X-Current-Page', @places.current_page.to_s)
|
||||
response.set_header('X-Total-Pages', @places.total_pages.to_s)
|
||||
response.set_header('X-Total-Count', @places.total_count.to_s)
|
||||
else
|
||||
# Non-paginated collection - treat as single page with all results
|
||||
total = @places.count
|
||||
response.set_header('X-Current-Page', '1')
|
||||
response.set_header('X-Total-Pages', '1')
|
||||
response.set_header('X-Total-Count', total.to_s)
|
||||
end
|
||||
|
||||
render json: @places.map { |place| serialize_place(place) }
|
||||
end
|
||||
|
||||
def show
|
||||
render json: serialize_place(@place)
|
||||
end
|
||||
|
||||
def create
|
||||
@place = current_api_user.places.build(place_params.except(:tag_ids))
|
||||
|
||||
if @place.save
|
||||
add_tags if tag_ids.present?
|
||||
@place = current_api_user.places.includes(:tags, :visits).find(@place.id)
|
||||
|
||||
render json: serialize_place(@place), status: :created
|
||||
else
|
||||
render json: { errors: @place.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @place.update(place_params)
|
||||
set_tags if params[:place][:tag_ids]
|
||||
@place = current_api_user.places.includes(:tags, :visits).find(@place.id)
|
||||
|
||||
render json: serialize_place(@place)
|
||||
else
|
||||
render json: { errors: @place.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@place.destroy!
|
||||
|
||||
head :no_content
|
||||
end
|
||||
|
||||
def nearby
|
||||
unless params[:latitude].present? && params[:longitude].present?
|
||||
return render json: { error: 'latitude and longitude are required' }, status: :bad_request
|
||||
end
|
||||
|
||||
results = Places::NearbySearch.new(
|
||||
latitude: params[:latitude].to_f,
|
||||
longitude: params[:longitude].to_f,
|
||||
radius: params[:radius]&.to_f || 0.5,
|
||||
limit: params[:limit]&.to_i || 10
|
||||
).call
|
||||
|
||||
render json: { places: results }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_place
|
||||
@place = current_api_user.places.includes(:tags, :visits).find(params[:id])
|
||||
end
|
||||
|
||||
def place_params
|
||||
params.require(:place).permit(:name, :latitude, :longitude, :source, :note, tag_ids: [])
|
||||
end
|
||||
|
||||
def tag_ids
|
||||
ids = params.dig(:place, :tag_ids)
|
||||
Array(ids).compact
|
||||
end
|
||||
|
||||
def add_tags
|
||||
return if tag_ids.empty?
|
||||
|
||||
tags = current_api_user.tags.where(id: tag_ids)
|
||||
@place.tags << tags
|
||||
end
|
||||
|
||||
def set_tags
|
||||
tag_ids_param = Array(params.dig(:place, :tag_ids)).compact
|
||||
tags = current_api_user.tags.where(id: tag_ids_param)
|
||||
@place.tags = tags
|
||||
end
|
||||
|
||||
def serialize_place(place)
|
||||
{
|
||||
id: place.id,
|
||||
name: place.name,
|
||||
latitude: place.lat,
|
||||
longitude: place.lon,
|
||||
source: place.source,
|
||||
note: place.note,
|
||||
icon: place.tags.first&.icon,
|
||||
color: place.tags.first&.color,
|
||||
visits_count: place.visits.size,
|
||||
created_at: place.created_at,
|
||||
tags: place.tags.map do |tag|
|
||||
{
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
icon: tag.icon,
|
||||
color: tag.color,
|
||||
privacy_radius_meters: tag.privacy_radius_meters
|
||||
}
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,37 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::PointsController < ApiController
|
||||
include SafeTimestampParser
|
||||
|
||||
before_action :authenticate_active_api_user!, only: %i[create update destroy bulk_destroy]
|
||||
before_action :authenticate_active_api_user!, only: %i[create update destroy]
|
||||
before_action :validate_points_limit, only: %i[create]
|
||||
|
||||
def index
|
||||
start_at = params[:start_at].present? ? safe_timestamp(params[:start_at]) : nil
|
||||
end_at = params[:end_at].present? ? safe_timestamp(params[:end_at]) : Time.zone.now.to_i
|
||||
start_at = params[:start_at]&.to_datetime&.to_i
|
||||
end_at = params[:end_at]&.to_datetime&.to_i || Time.zone.now.to_i
|
||||
order = params[:order] || 'desc'
|
||||
|
||||
points = current_api_user
|
||||
.points
|
||||
.without_raw_data
|
||||
.where(timestamp: start_at..end_at)
|
||||
|
||||
# Filter by geographic bounds if provided
|
||||
if params[:min_longitude].present? && params[:max_longitude].present? &&
|
||||
params[:min_latitude].present? && params[:max_latitude].present?
|
||||
min_lng = params[:min_longitude].to_f
|
||||
max_lng = params[:max_longitude].to_f
|
||||
min_lat = params[:min_latitude].to_f
|
||||
max_lat = params[:max_latitude].to_f
|
||||
|
||||
# Use PostGIS to filter points within bounding box
|
||||
points = points.where(
|
||||
'ST_X(lonlat::geometry) BETWEEN ? AND ? AND ST_Y(lonlat::geometry) BETWEEN ? AND ?',
|
||||
min_lng, max_lng, min_lat, max_lat
|
||||
)
|
||||
end
|
||||
|
||||
points = points
|
||||
.order(timestamp: order)
|
||||
.page(params[:page])
|
||||
.per(params[:per_page] || 100)
|
||||
|
|
@ -53,11 +33,9 @@ class Api::V1::PointsController < ApiController
|
|||
def update
|
||||
point = current_api_user.points.find(params[:id])
|
||||
|
||||
if point.update(lonlat: "POINT(#{point_params[:longitude]} #{point_params[:latitude]})")
|
||||
render json: point_serializer.new(point.reload).call
|
||||
else
|
||||
render json: { error: point.errors.full_messages.join(', ') }, status: :unprocessable_entity
|
||||
end
|
||||
point.update(lonlat: "POINT(#{point_params[:longitude]} #{point_params[:latitude]})")
|
||||
|
||||
render json: point_serializer.new(point).call
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
|
@ -67,16 +45,6 @@ class Api::V1::PointsController < ApiController
|
|||
render json: { message: 'Point deleted successfully' }
|
||||
end
|
||||
|
||||
def bulk_destroy
|
||||
point_ids = bulk_destroy_params[:point_ids]
|
||||
|
||||
render json: { error: 'No points selected' }, status: :unprocessable_entity and return if point_ids.blank?
|
||||
|
||||
deleted_count = current_api_user.points.where(id: point_ids).destroy_all.count
|
||||
|
||||
render json: { message: 'Points were successfully destroyed', count: deleted_count }, status: :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def point_params
|
||||
|
|
@ -87,10 +55,6 @@ class Api::V1::PointsController < ApiController
|
|||
params.permit(locations: [:type, { geometry: {}, properties: {} }], batch: {})
|
||||
end
|
||||
|
||||
def bulk_destroy_params
|
||||
params.permit(point_ids: [])
|
||||
end
|
||||
|
||||
def point_serializer
|
||||
params[:slim] == 'true' ? Api::SlimPointSerializer : Api::PointSerializer
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ class Api::V1::SettingsController < ApiController
|
|||
|
||||
def index
|
||||
render json: {
|
||||
settings: current_api_user.safe_settings.config,
|
||||
settings: current_api_user.safe_settings,
|
||||
status: 'success'
|
||||
}, status: :ok
|
||||
end
|
||||
|
|
@ -14,7 +14,7 @@ class Api::V1::SettingsController < ApiController
|
|||
settings_params.each { |key, value| current_api_user.settings[key] = value }
|
||||
|
||||
if current_api_user.save
|
||||
render json: { message: 'Settings updated', settings: current_api_user.safe_settings.config, status: 'success' },
|
||||
render json: { message: 'Settings updated', settings: current_api_user.settings, status: 'success' },
|
||||
status: :ok
|
||||
else
|
||||
render json: { message: 'Something went wrong', errors: current_api_user.errors.full_messages },
|
||||
|
|
@ -30,9 +30,7 @@ class Api::V1::SettingsController < ApiController
|
|||
:time_threshold_minutes, :merge_threshold_minutes, :route_opacity,
|
||||
:preferred_map_layer, :points_rendering_mode, :live_map_enabled,
|
||||
:immich_url, :immich_api_key, :photoprism_url, :photoprism_api_key,
|
||||
:speed_colored_routes, :speed_color_scale, :fog_of_war_threshold,
|
||||
:maps_v2_style, :maps_maplibre_style, :globe_projection,
|
||||
enabled_map_layers: []
|
||||
:speed_colored_routes, :speed_color_scale, :fog_of_war_threshold
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class TagsController < ApiController
|
||||
def privacy_zones
|
||||
zones = current_api_user.tags.privacy_zones.includes(:places)
|
||||
|
||||
render json: zones.map { |tag| TagSerializer.new(tag).call }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::TracksController < ApiController
|
||||
def index
|
||||
tracks_query = Tracks::IndexQuery.new(user: current_api_user, params: params)
|
||||
paginated_tracks = tracks_query.call
|
||||
|
||||
geojson = Tracks::GeojsonSerializer.new(paginated_tracks).call
|
||||
|
||||
tracks_query.pagination_headers(paginated_tracks).each do |header, value|
|
||||
response.set_header(header, value)
|
||||
end
|
||||
|
||||
render json: geojson
|
||||
end
|
||||
end
|
||||
|
|
@ -3,17 +3,6 @@
|
|||
class Api::V1::VisitsController < ApiController
|
||||
def index
|
||||
visits = Visits::Finder.new(current_api_user, params).call
|
||||
|
||||
# Support optional pagination (backward compatible - returns all if no page param)
|
||||
if params[:page].present?
|
||||
per_page = [params[:per_page]&.to_i || 100, 500].min
|
||||
visits = visits.page(params[:page]).per(per_page)
|
||||
|
||||
response.set_header('X-Current-Page', visits.current_page.to_s)
|
||||
response.set_header('X-Total-Pages', visits.total_pages.to_s)
|
||||
response.set_header('X-Total-Count', visits.total_count.to_s)
|
||||
end
|
||||
|
||||
serialized_visits = visits.map do |visit|
|
||||
Api::VisitSerializer.new(visit).call
|
||||
end
|
||||
|
|
@ -21,11 +10,6 @@ class Api::V1::VisitsController < ApiController
|
|||
render json: serialized_visits
|
||||
end
|
||||
|
||||
def show
|
||||
visit = current_api_user.visits.find(params[:id])
|
||||
render json: Api::VisitSerializer.new(visit).call
|
||||
end
|
||||
|
||||
def create
|
||||
service = Visits::Create.new(current_api_user, visit_params)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,8 @@ class ApiController < ApplicationController
|
|||
before_action :set_version_header
|
||||
before_action :authenticate_api_key
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
|
||||
|
||||
private
|
||||
|
||||
def record_not_found
|
||||
render json: { error: 'Record not found' }, status: :not_found
|
||||
end
|
||||
|
||||
def set_version_header
|
||||
message = "Hey, I\'m alive#{current_api_user ? ' and authenticated' : ''}!"
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ class ApplicationController < ActionController::Base
|
|||
|
||||
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
||||
|
||||
before_action :unread_notifications, :set_self_hosted_status, :store_client_header
|
||||
before_action :unread_notifications, :set_self_hosted_status
|
||||
|
||||
protected
|
||||
|
||||
|
|
@ -39,49 +39,12 @@ class ApplicationController < ActionController::Base
|
|||
user_not_authorized
|
||||
end
|
||||
|
||||
def after_sign_in_path_for(resource)
|
||||
# Check for family invitation first
|
||||
invitation_token = params[:invitation_token] || session[:invitation_token]
|
||||
if invitation_token.present?
|
||||
invitation = Family::Invitation.find_by(token: invitation_token)
|
||||
return family_invitation_path(invitation.token) if invitation&.can_be_accepted?
|
||||
end
|
||||
|
||||
# Handle iOS client flow
|
||||
client_type = request.headers['X-Dawarich-Client'] || session[:dawarich_client]
|
||||
|
||||
case client_type
|
||||
when 'ios'
|
||||
payload = { api_key: resource.api_key, exp: 5.minutes.from_now.to_i }
|
||||
|
||||
token = Subscription::EncodeJwtToken.new(
|
||||
payload, ENV['AUTH_JWT_SECRET_KEY']
|
||||
).call
|
||||
|
||||
ios_success_path(token:)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_family_feature_enabled!
|
||||
return if DawarichSettings.family_feature_enabled?
|
||||
|
||||
render json: { error: 'Family feature is not enabled' }, status: :forbidden
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_self_hosted_status
|
||||
@self_hosted = DawarichSettings.self_hosted?
|
||||
end
|
||||
|
||||
def store_client_header
|
||||
return unless request.headers['X-Dawarich-Client']
|
||||
|
||||
session[:dawarich_client] = request.headers['X-Dawarich-Client']
|
||||
end
|
||||
|
||||
def user_not_authorized
|
||||
redirect_back fallback_location: root_path,
|
||||
alert: 'You are not authorized to perform this action.',
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Auth
|
||||
class IosController < ApplicationController
|
||||
def success
|
||||
# If token is provided, this is the final callback for ASWebAuthenticationSession
|
||||
if params[:token].present?
|
||||
# ASWebAuthenticationSession will capture this URL and extract the token
|
||||
render plain: "Authentication successful! You can close this window.", status: :ok
|
||||
else
|
||||
# This should not happen with our current flow, but keeping for safety
|
||||
render json: {
|
||||
success: true,
|
||||
message: 'iOS authentication successful',
|
||||
redirect_url: root_url
|
||||
}, status: :ok
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SafeTimestampParser
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
private
|
||||
|
||||
def safe_timestamp(date_string)
|
||||
return Time.zone.now.to_i if date_string.blank?
|
||||
|
||||
parsed_time = Time.zone.parse(date_string)
|
||||
|
||||
# Time.zone.parse returns epoch time (2000-01-01) for unparseable strings
|
||||
# Check if it's a valid parse by seeing if year is suspiciously at epoch
|
||||
return Time.zone.now.to_i if parsed_time.nil? || (parsed_time.year == 2000 && !date_string.include?('2000'))
|
||||
|
||||
min_timestamp = Time.zone.parse('1970-01-01').to_i
|
||||
max_timestamp = Time.zone.parse('2100-01-01').to_i
|
||||
|
||||
parsed_time.to_i.clamp(min_timestamp, max_timestamp)
|
||||
rescue ArgumentError, TypeError
|
||||
Time.zone.now.to_i
|
||||
end
|
||||
end
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module UtmTrackable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
UTM_PARAMS = %w[utm_source utm_medium utm_campaign utm_term utm_content].freeze
|
||||
|
||||
def store_utm_params
|
||||
UTM_PARAMS.each do |param|
|
||||
session[param] = params[param] if params[param].present?
|
||||
end
|
||||
end
|
||||
|
||||
def assign_utm_params(record)
|
||||
utm_data = extract_utm_data_from_session
|
||||
|
||||
return unless utm_data.any?
|
||||
|
||||
record.update_columns(utm_data)
|
||||
clear_utm_session
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def extract_utm_data_from_session
|
||||
UTM_PARAMS.each_with_object({}) do |param, hash|
|
||||
hash[param] = session[param] if session[param].present?
|
||||
end
|
||||
end
|
||||
|
||||
def clear_utm_session
|
||||
UTM_PARAMS.each { |param| session.delete(param) }
|
||||
end
|
||||
end
|
||||
|
|
@ -7,7 +7,7 @@ class ExportsController < ApplicationController
|
|||
before_action :set_export, only: %i[destroy]
|
||||
|
||||
def index
|
||||
@exports = current_user.exports.with_attached_file.order(created_at: :desc).page(params[:page])
|
||||
@exports = current_user.exports.order(created_at: :desc).page(params[:page])
|
||||
end
|
||||
|
||||
def create
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FamiliesController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :ensure_family_feature_enabled!
|
||||
before_action :set_family, only: %i[show edit update destroy]
|
||||
|
||||
def show
|
||||
authorize @family
|
||||
|
||||
@members = @family.members.includes(:family_membership).order(:email)
|
||||
@pending_invitations = @family.active_invitations.order(:created_at)
|
||||
|
||||
@member_count = @family.member_count
|
||||
@can_invite = @family.can_add_members?
|
||||
end
|
||||
|
||||
def new
|
||||
redirect_to family_path and return if current_user.in_family?
|
||||
|
||||
@family = Family.new
|
||||
authorize @family
|
||||
end
|
||||
|
||||
def create
|
||||
@family = Family.new(family_params)
|
||||
authorize @family
|
||||
|
||||
service = Families::Create.new(
|
||||
user: current_user,
|
||||
name: family_params[:name]
|
||||
)
|
||||
|
||||
if service.call
|
||||
redirect_to family_path, notice: 'Family created successfully!'
|
||||
else
|
||||
@family = Family.new(family_params)
|
||||
|
||||
if service.errors.any?
|
||||
service.errors.each do |error|
|
||||
@family.errors.add(error.attribute, error.message)
|
||||
end
|
||||
end
|
||||
|
||||
if service.error_message.present?
|
||||
@family.errors.add(:base, service.error_message)
|
||||
end
|
||||
|
||||
flash.now[:alert] = service.error_message || 'Failed to create family'
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize @family
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @family
|
||||
|
||||
if @family.update(family_params)
|
||||
redirect_to family_path, notice: 'Family updated successfully!'
|
||||
else
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @family
|
||||
|
||||
if @family.members.count > 1
|
||||
redirect_to family_path, alert: 'Cannot delete family with members. Remove all members first.'
|
||||
else
|
||||
@family.destroy
|
||||
redirect_to new_family_path, notice: 'Family deleted successfully!'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_family
|
||||
@family = current_user.family
|
||||
redirect_to new_family_path, alert: 'You are not in a family' unless @family
|
||||
end
|
||||
|
||||
def family_params
|
||||
params.require(:family).permit(:name)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Family::InvitationsController < ApplicationController
|
||||
before_action :authenticate_user!, except: %i[show]
|
||||
before_action :ensure_family_feature_enabled!, except: %i[show]
|
||||
before_action :set_family, except: %i[show]
|
||||
before_action :set_invitation_by_id_and_family, only: %i[destroy]
|
||||
|
||||
def index
|
||||
authorize @family, :show?
|
||||
|
||||
@pending_invitations = @family.family_invitations.active
|
||||
end
|
||||
|
||||
def show
|
||||
token = params[:token] || params[:id]
|
||||
@invitation = Family::Invitation.find_by!(token: token)
|
||||
|
||||
if @invitation.expired?
|
||||
redirect_to root_path, alert: 'This invitation has expired.' and return
|
||||
end
|
||||
|
||||
unless @invitation.pending?
|
||||
redirect_to root_path, alert: 'This invitation is no longer valid.' and return
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
authorize @family, :invite?
|
||||
|
||||
service = Families::Invite.new(
|
||||
family: @family,
|
||||
email: invitation_params[:email],
|
||||
invited_by: current_user
|
||||
)
|
||||
|
||||
if service.call
|
||||
redirect_to family_path, notice: 'Invitation sent successfully!'
|
||||
else
|
||||
redirect_to family_path, alert: service.error_message || 'Failed to send invitation'
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @family, :manage_invitations?
|
||||
|
||||
begin
|
||||
if @invitation.update(status: :cancelled)
|
||||
redirect_to family_path, notice: 'Invitation cancelled'
|
||||
else
|
||||
redirect_to family_path, alert: 'Failed to cancel invitation. Please try again'
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error cancelling family invitation: #{e.message}"
|
||||
redirect_to family_path, alert: 'An unexpected error occurred while cancelling the invitation'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_family
|
||||
@family = current_user.family
|
||||
|
||||
redirect_to new_family_path, alert: 'You are not in a family' and return unless @family
|
||||
end
|
||||
|
||||
def set_invitation_by_id_and_family
|
||||
# For authenticated nested routes: /families/:family_id/invitations/:id
|
||||
# The :id param contains the token value
|
||||
@family = current_user.family
|
||||
@invitation = @family.family_invitations.find_by!(token: params[:id])
|
||||
end
|
||||
|
||||
def invitation_params
|
||||
params.require(:family_invitation).permit(:email)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Family::LocationSharingController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :ensure_family_feature_enabled!
|
||||
before_action :ensure_user_in_family!
|
||||
|
||||
def update
|
||||
result = Families::UpdateLocationSharing.new(
|
||||
user: current_user,
|
||||
enabled: params[:enabled],
|
||||
duration: params[:duration]
|
||||
).call
|
||||
|
||||
render json: result.payload, status: result.status
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_user_in_family!
|
||||
return if current_user.in_family?
|
||||
|
||||
render json: { error: 'User is not part of a family' }, status: :forbidden
|
||||
end
|
||||
end
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Family::MembershipsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :ensure_family_feature_enabled!
|
||||
before_action :set_family, except: %i[create]
|
||||
before_action :set_membership, only: %i[destroy]
|
||||
before_action :set_invitation, only: %i[create]
|
||||
|
||||
def create
|
||||
authorize @invitation, policy_class: Family::MembershipPolicy
|
||||
|
||||
service = Families::AcceptInvitation.new(
|
||||
invitation: @invitation,
|
||||
user: current_user
|
||||
)
|
||||
|
||||
if service.call
|
||||
redirect_to family_path, notice: 'Welcome to the family!'
|
||||
else
|
||||
redirect_to root_path, alert: service.error_message || 'Unable to accept invitation'
|
||||
end
|
||||
rescue Pundit::NotAuthorizedError
|
||||
alert = case
|
||||
when @invitation.expired? then 'This invitation is no longer valid or has expired'
|
||||
when !@invitation.pending? then 'This invitation has already been processed'
|
||||
when @invitation.email != current_user.email then 'This invitation is not for your email address'
|
||||
else 'You are not authorized to accept this invitation'
|
||||
end
|
||||
|
||||
redirect_to root_path, alert: alert
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error accepting family invitation: #{e.message}"
|
||||
|
||||
redirect_to root_path, alert: 'An unexpected error occurred. Please try again later'
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @membership
|
||||
|
||||
member_user = @membership.user
|
||||
service = Families::Memberships::Destroy.new(user: current_user, member_to_remove: member_user)
|
||||
|
||||
if service.call
|
||||
if member_user == current_user
|
||||
redirect_to new_family_path, notice: 'You have left the family'
|
||||
else
|
||||
redirect_to family_path, notice: "#{member_user.email} has been removed from the family"
|
||||
end
|
||||
else
|
||||
redirect_to family_path, alert: service.error_message || 'Failed to remove member'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_family
|
||||
@family = current_user.family
|
||||
|
||||
redirect_to new_family_path, alert: 'You are not in a family' and return unless @family
|
||||
end
|
||||
|
||||
def set_membership
|
||||
@membership = @family.family_memberships.find(params[:id])
|
||||
end
|
||||
|
||||
def set_invitation
|
||||
@invitation = Family::Invitation.find_by!(token: params[:token])
|
||||
end
|
||||
end
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class HomeController < ApplicationController
|
||||
include ApplicationHelper
|
||||
|
||||
def index
|
||||
# redirect_to 'https://dawarich.app', allow_other_host: true and return unless SELF_HOSTED
|
||||
|
||||
redirect_to preferred_map_path if current_user
|
||||
redirect_to map_url if current_user
|
||||
|
||||
@points = current_user.points.without_raw_data if current_user
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ class ImportsController < ApplicationController
|
|||
def index
|
||||
@imports = policy_scope(Import)
|
||||
.select(:id, :name, :source, :created_at, :processed, :status)
|
||||
.with_attached_file
|
||||
.order(created_at: :desc)
|
||||
.page(params[:page])
|
||||
end
|
||||
|
|
@ -79,13 +78,9 @@ class ImportsController < ApplicationController
|
|||
end
|
||||
|
||||
def destroy
|
||||
@import.deleting!
|
||||
Imports::DestroyJob.perform_later(@import.id)
|
||||
Imports::Destroy.new(current_user, @import).call
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to imports_url, notice: 'Import is being deleted.', status: :see_other }
|
||||
format.turbo_stream
|
||||
end
|
||||
redirect_to imports_url, notice: 'Import was successfully destroyed.', status: :see_other
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
module Map
|
||||
class MaplibreController < ApplicationController
|
||||
include SafeTimestampParser
|
||||
|
||||
before_action :authenticate_user!
|
||||
layout 'map'
|
||||
|
||||
def index
|
||||
@start_at = parsed_start_at
|
||||
@end_at = parsed_end_at
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def start_at
|
||||
return safe_timestamp(params[:start_at]) if params[:start_at].present?
|
||||
|
||||
Time.zone.today.beginning_of_day.to_i
|
||||
end
|
||||
|
||||
def end_at
|
||||
return safe_timestamp(params[:end_at]) if params[:end_at].present?
|
||||
|
||||
Time.zone.today.end_of_day.to_i
|
||||
end
|
||||
|
||||
def parsed_start_at
|
||||
Time.zone.at(start_at)
|
||||
end
|
||||
|
||||
def parsed_end_at
|
||||
Time.zone.at(end_at)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Map::LeafletController < ApplicationController
|
||||
include SafeTimestampParser
|
||||
|
||||
class MapController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
layout 'map', only: :index
|
||||
|
||||
def index
|
||||
@points = filtered_points
|
||||
|
|
@ -16,7 +13,6 @@ class Map::LeafletController < ApplicationController
|
|||
@years = years_range
|
||||
@points_number = points_count
|
||||
@features = DawarichSettings.features
|
||||
@home_coordinates = current_user.home_place_coordinates
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -41,34 +37,19 @@ class Map::LeafletController < ApplicationController
|
|||
end
|
||||
|
||||
def calculate_distance
|
||||
return 0 if @points.count(:id) < 2
|
||||
return 0 if @coordinates.size < 2
|
||||
|
||||
# Use PostGIS window function for efficient distance calculation
|
||||
# This is O(1) database operation vs O(n) Ruby iteration
|
||||
import_filter = params[:import_id].present? ? 'AND import_id = :import_id' : ''
|
||||
total_distance = 0
|
||||
|
||||
sql = <<~SQL.squish
|
||||
SELECT COALESCE(SUM(distance_m) / 1000.0, 0) as total_km FROM (
|
||||
SELECT ST_Distance(
|
||||
lonlat::geography,
|
||||
LAG(lonlat::geography) OVER (ORDER BY timestamp)
|
||||
) as distance_m
|
||||
FROM points
|
||||
WHERE user_id = :user_id
|
||||
AND timestamp >= :start_at
|
||||
AND timestamp <= :end_at
|
||||
#{import_filter}
|
||||
) distances
|
||||
SQL
|
||||
@coordinates.each_cons(2) do
|
||||
distance_km = Geocoder::Calculations.distance_between(
|
||||
[_1[0], _1[1]], [_2[0], _2[1]], units: :km
|
||||
)
|
||||
|
||||
query_params = { user_id: current_user.id, start_at: start_at, end_at: end_at }
|
||||
query_params[:import_id] = params[:import_id] if params[:import_id].present?
|
||||
total_distance += distance_km
|
||||
end
|
||||
|
||||
result = Point.connection.select_value(
|
||||
ActiveRecord::Base.sanitize_sql_array([sql, query_params])
|
||||
)
|
||||
|
||||
result&.to_f&.round || 0
|
||||
total_distance.round
|
||||
end
|
||||
|
||||
def parsed_start_at
|
||||
|
|
@ -88,14 +69,14 @@ class Map::LeafletController < ApplicationController
|
|||
end
|
||||
|
||||
def start_at
|
||||
return safe_timestamp(params[:start_at]) if params[:start_at].present?
|
||||
return Time.zone.parse(params[:start_at]).to_i if params[:start_at].present?
|
||||
return Time.zone.at(points.last.timestamp).beginning_of_day.to_i if points.any?
|
||||
|
||||
Time.zone.today.beginning_of_day.to_i
|
||||
end
|
||||
|
||||
def end_at
|
||||
return safe_timestamp(params[:end_at]) if params[:end_at].present?
|
||||
return Time.zone.parse(params[:end_at]).to_i if params[:end_at].present?
|
||||
return Time.zone.at(points.last.timestamp).end_of_day.to_i if points.any?
|
||||
|
||||
Time.zone.today.end_of_day.to_i
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PointsController < ApplicationController
|
||||
include SafeTimestampParser
|
||||
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
|
|
@ -42,13 +40,13 @@ class PointsController < ApplicationController
|
|||
def start_at
|
||||
return 1.month.ago.beginning_of_day.to_i if params[:start_at].nil?
|
||||
|
||||
safe_timestamp(params[:start_at])
|
||||
Time.zone.parse(params[:start_at]).to_i
|
||||
end
|
||||
|
||||
def end_at
|
||||
return Time.zone.today.end_of_day.to_i if params[:end_at].nil?
|
||||
|
||||
safe_timestamp(params[:end_at])
|
||||
Time.zone.parse(params[:end_at]).to_i
|
||||
end
|
||||
|
||||
def points
|
||||
|
|
|
|||
|
|
@ -24,6 +24,6 @@ class Settings::MapsController < ApplicationController
|
|||
private
|
||||
|
||||
def settings_params
|
||||
params.require(:maps).permit(:name, :url, :distance_unit, :preferred_version)
|
||||
params.require(:maps).permit(:name, :url, :distance_unit)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -54,13 +54,21 @@ class Settings::UsersController < ApplicationController
|
|||
end
|
||||
|
||||
def import
|
||||
if params[:archive].blank?
|
||||
unless params[:archive].present?
|
||||
redirect_to edit_user_registration_path, alert: 'Please select a ZIP archive to import.'
|
||||
return
|
||||
end
|
||||
|
||||
import =
|
||||
create_import_from_signed_archive_id(params[:archive])
|
||||
archive_file = params[:archive]
|
||||
|
||||
validate_archive_file(archive_file)
|
||||
|
||||
import = current_user.imports.build(
|
||||
name: archive_file.original_filename,
|
||||
source: :user_data_archive
|
||||
)
|
||||
|
||||
import.file.attach(archive_file)
|
||||
|
||||
if import.save
|
||||
redirect_to edit_user_registration_path,
|
||||
|
|
@ -81,36 +89,6 @@ class Settings::UsersController < ApplicationController
|
|||
params.require(:user).permit(:email, :password)
|
||||
end
|
||||
|
||||
def create_import_from_signed_archive_id(signed_id)
|
||||
Rails.logger.debug "Creating archive import from signed ID: #{signed_id[0..20]}..."
|
||||
|
||||
blob = ActiveStorage::Blob.find_signed(signed_id)
|
||||
|
||||
# Validate that it's a ZIP file
|
||||
validate_blob_file_type(blob)
|
||||
|
||||
import_name = generate_unique_import_name(blob.filename.to_s)
|
||||
import = current_user.imports.build(
|
||||
name: import_name,
|
||||
source: :user_data_archive
|
||||
)
|
||||
import.file.attach(blob)
|
||||
|
||||
import
|
||||
end
|
||||
|
||||
def generate_unique_import_name(original_name)
|
||||
return original_name unless current_user.imports.exists?(name: original_name)
|
||||
|
||||
# Extract filename and extension
|
||||
basename = File.basename(original_name, File.extname(original_name))
|
||||
extension = File.extname(original_name)
|
||||
|
||||
# Add current datetime
|
||||
timestamp = Time.current.strftime('%Y%m%d_%H%M%S')
|
||||
"#{basename}_#{timestamp}#{extension}"
|
||||
end
|
||||
|
||||
def validate_archive_file(archive_file)
|
||||
unless ['application/zip', 'application/x-zip-compressed'].include?(archive_file.content_type) ||
|
||||
File.extname(archive_file.original_filename).downcase == '.zip'
|
||||
|
|
@ -118,12 +96,4 @@ class Settings::UsersController < ApplicationController
|
|||
redirect_to edit_user_registration_path, alert: 'Please upload a valid ZIP file.' and return
|
||||
end
|
||||
end
|
||||
|
||||
def validate_blob_file_type(blob)
|
||||
unless ['application/zip', 'application/x-zip-compressed'].include?(blob.content_type) ||
|
||||
File.extname(blob.filename.to_s).downcase == '.zip'
|
||||
|
||||
raise StandardError, 'Please upload a valid ZIP file.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class SettingsController < ApplicationController
|
|||
:meters_between_routes, :minutes_between_routes, :fog_of_war_meters,
|
||||
:time_threshold_minutes, :merge_threshold_minutes, :route_opacity,
|
||||
:immich_url, :immich_api_key, :photoprism_url, :photoprism_api_key,
|
||||
:visits_suggestions_enabled, :digest_emails_enabled
|
||||
:visits_suggestions_enabled
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Shared::DigestsController < ApplicationController
|
||||
helper Users::DigestsHelper
|
||||
helper CountryFlagHelper
|
||||
|
||||
before_action :authenticate_user!, except: [:show]
|
||||
before_action :authenticate_active_user!, only: [:update]
|
||||
|
||||
def show
|
||||
@digest = Users::Digest.find_by(sharing_uuid: params[:uuid])
|
||||
|
||||
unless @digest&.public_accessible?
|
||||
return redirect_to root_path,
|
||||
alert: 'Shared digest not found or no longer available'
|
||||
end
|
||||
|
||||
@year = @digest.year
|
||||
@user = @digest.user
|
||||
@distance_unit = @user.safe_settings.distance_unit || 'km'
|
||||
@is_public_view = true
|
||||
|
||||
render 'users/digests/public_year'
|
||||
end
|
||||
|
||||
def update
|
||||
@year = params[:year].to_i
|
||||
@digest = current_user.digests.yearly.find_by(year: @year)
|
||||
|
||||
return head :not_found unless @digest
|
||||
|
||||
if params[:enabled] == '1'
|
||||
@digest.enable_sharing!(expiration: params[:expiration] || '24h')
|
||||
sharing_url = shared_users_digest_url(@digest.sharing_uuid)
|
||||
|
||||
render json: {
|
||||
success: true,
|
||||
sharing_url: sharing_url,
|
||||
message: 'Sharing enabled successfully'
|
||||
}
|
||||
else
|
||||
@digest.disable_sharing!
|
||||
|
||||
render json: {
|
||||
success: true,
|
||||
message: 'Sharing disabled successfully'
|
||||
}
|
||||
end
|
||||
rescue StandardError
|
||||
render json: {
|
||||
success: false,
|
||||
message: 'Failed to update sharing settings'
|
||||
}, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
|
@ -17,7 +17,6 @@ class Shared::StatsController < ApplicationController
|
|||
@user = @stat.user
|
||||
@is_public_view = true
|
||||
@data_bounds = @stat.calculate_data_bounds
|
||||
@hexagons_available = @stat.hexagons_available?
|
||||
|
||||
render 'stats/public_month'
|
||||
end
|
||||
|
|
@ -30,7 +29,7 @@ class Shared::StatsController < ApplicationController
|
|||
return head :not_found unless @stat
|
||||
|
||||
if params[:enabled] == '1'
|
||||
@stat.enable_sharing!(expiration: params[:expiration] || '24h')
|
||||
@stat.enable_sharing!(expiration: params[:expiration] || 'permanent')
|
||||
sharing_url = shared_stat_url(@stat.sharing_uuid)
|
||||
|
||||
render json: {
|
||||
|
|
|
|||
|
|
@ -80,12 +80,8 @@ class StatsController < ApplicationController
|
|||
end
|
||||
|
||||
def build_stats
|
||||
columns = %i[id year month distance updated_at user_id]
|
||||
columns << :toponyms if DawarichSettings.reverse_geocoding_enabled?
|
||||
|
||||
current_user.stats
|
||||
.select(columns)
|
||||
.order(year: :desc, updated_at: :desc)
|
||||
.group_by(&:year)
|
||||
current_user.stats.group_by(&:year).transform_values do |stats|
|
||||
stats.sort_by(&:updated_at).reverse
|
||||
end.sort.reverse
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class TagsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_tag, only: [:edit, :update, :destroy]
|
||||
|
||||
def index
|
||||
@tags = policy_scope(Tag).ordered
|
||||
|
||||
authorize Tag
|
||||
end
|
||||
|
||||
def new
|
||||
@tag = current_user.tags.build
|
||||
|
||||
authorize @tag
|
||||
end
|
||||
|
||||
def create
|
||||
@tag = current_user.tags.build(tag_params)
|
||||
|
||||
authorize @tag
|
||||
|
||||
if @tag.save
|
||||
redirect_to tags_path, notice: 'Tag was successfully created.'
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
authorize @tag
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @tag
|
||||
|
||||
if @tag.update(tag_params)
|
||||
redirect_to tags_path, notice: 'Tag was successfully updated.'
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @tag
|
||||
|
||||
@tag.destroy!
|
||||
|
||||
redirect_to tags_path, notice: 'Tag was successfully deleted.', status: :see_other
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tag
|
||||
@tag = current_user.tags.find(params[:id])
|
||||
end
|
||||
|
||||
def tag_params
|
||||
params.require(:tag).permit(:name, :icon, :color, :privacy_radius_meters)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Users::DigestsController < ApplicationController
|
||||
helper Users::DigestsHelper
|
||||
helper CountryFlagHelper
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :authenticate_active_user!, only: [:create]
|
||||
before_action :set_digest, only: %i[show destroy]
|
||||
|
||||
def index
|
||||
@digests = current_user.digests.yearly.order(year: :desc)
|
||||
@available_years = available_years_for_generation
|
||||
end
|
||||
|
||||
def show
|
||||
@distance_unit = current_user.safe_settings.distance_unit || 'km'
|
||||
end
|
||||
|
||||
def create
|
||||
year = params[:year].to_i
|
||||
|
||||
if valid_year?(year)
|
||||
Users::Digests::CalculatingJob.perform_later(current_user.id, year)
|
||||
redirect_to users_digests_path,
|
||||
notice: "Year-end digest for #{year} is being generated. Check back soon!",
|
||||
status: :see_other
|
||||
else
|
||||
redirect_to users_digests_path, alert: 'Invalid year selected', status: :see_other
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
year = @digest.year
|
||||
@digest.destroy!
|
||||
redirect_to users_digests_path, notice: "Year-end digest for #{year} has been deleted", status: :see_other
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_digest
|
||||
@digest = current_user.digests.yearly.find_by!(year: params[:year])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to users_digests_path, alert: 'Digest not found'
|
||||
end
|
||||
|
||||
def available_years_for_generation
|
||||
tracked_years = current_user.stats.select(:year).distinct.pluck(:year)
|
||||
existing_digests = current_user.digests.yearly.pluck(:year)
|
||||
|
||||
(tracked_years - existing_digests - [Time.current.year]).sort.reverse
|
||||
end
|
||||
|
||||
def valid_year?(year)
|
||||
return false if year < 2000 || year > Time.current.year
|
||||
|
||||
current_user.stats.exists?(year: year)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||
def github
|
||||
handle_auth('GitHub')
|
||||
end
|
||||
|
||||
def google_oauth2
|
||||
handle_auth('Google')
|
||||
end
|
||||
|
||||
def openid_connect
|
||||
handle_auth('OpenID Connect')
|
||||
end
|
||||
|
||||
def failure
|
||||
error_type = request.env['omniauth.error.type']
|
||||
error = request.env['omniauth.error']
|
||||
|
||||
# Provide user-friendly error messages
|
||||
error_message =
|
||||
case error_type
|
||||
when :invalid_credentials
|
||||
'Invalid credentials. Please check your username and password.'
|
||||
when :timeout
|
||||
'Connection timeout. Please try again.'
|
||||
when :csrf_detected
|
||||
'Security error detected. Please try again.'
|
||||
else
|
||||
if error&.message&.include?('Discovery')
|
||||
'Unable to connect to authentication provider. Please contact your administrator.'
|
||||
elsif error&.message&.include?('Issuer mismatch')
|
||||
'Authentication provider configuration error. Please contact your administrator.'
|
||||
else
|
||||
"Authentication failed: #{params[:message] || error&.message || 'Unknown error'}"
|
||||
end
|
||||
end
|
||||
|
||||
redirect_to root_path, alert: error_message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_auth(provider)
|
||||
@user = User.from_omniauth(request.env['omniauth.auth'])
|
||||
|
||||
if @user&.persisted?
|
||||
flash[:notice] = I18n.t 'devise.omniauth_callbacks.success', kind: provider
|
||||
sign_in_and_redirect @user, event: :authentication
|
||||
elsif @user.nil?
|
||||
# User creation was rejected (e.g., OIDC auto-register disabled)
|
||||
error_message = if provider == 'OpenID Connect' && !oidc_auto_register_enabled?
|
||||
'Your account must be created by an administrator before you can sign in with OIDC. ' \
|
||||
'Please contact your administrator.'
|
||||
else
|
||||
'Unable to create your account. Please try again or contact support.'
|
||||
end
|
||||
redirect_to root_path, alert: error_message
|
||||
else
|
||||
redirect_to new_user_registration_url, alert: @user.errors.full_messages.join("\n")
|
||||
end
|
||||
end
|
||||
|
||||
def oidc_auto_register_enabled?
|
||||
OIDC_AUTO_REGISTER
|
||||
end
|
||||
end
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Users::RegistrationsController < Devise::RegistrationsController
|
||||
include UtmTrackable
|
||||
|
||||
before_action :set_invitation, only: %i[new create]
|
||||
before_action :check_registration_allowed, only: %i[new create]
|
||||
before_action :store_utm_params, only: %i[new], unless: -> { DawarichSettings.self_hosted? }
|
||||
|
||||
def new
|
||||
build_resource({})
|
||||
|
||||
resource.email = @invitation.email if @invitation
|
||||
|
||||
yield resource if block_given?
|
||||
|
||||
respond_with resource
|
||||
end
|
||||
|
||||
def create
|
||||
super do |resource|
|
||||
if resource.persisted?
|
||||
assign_utm_params(resource)
|
||||
accept_invitation_for_user(resource) if @invitation
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def after_sign_up_path_for(resource)
|
||||
return family_path if @invitation&.family
|
||||
|
||||
super(resource)
|
||||
end
|
||||
|
||||
def after_inactive_sign_up_path_for(resource)
|
||||
return family_path if @invitation&.family
|
||||
|
||||
super(resource)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_registration_allowed
|
||||
return unless self_hosted_mode?
|
||||
return if valid_invitation_token?
|
||||
return if email_password_registration_allowed?
|
||||
|
||||
redirect_to root_path,
|
||||
alert: 'Registration is not available. Please contact your administrator for access.'
|
||||
end
|
||||
|
||||
def set_invitation
|
||||
return if invitation_token.blank?
|
||||
|
||||
@invitation = Family::Invitation.find_by(token: invitation_token)
|
||||
end
|
||||
|
||||
def self_hosted_mode?
|
||||
env_value = ENV['SELF_HOSTED']
|
||||
return ActiveModel::Type::Boolean.new.cast(env_value) unless env_value.nil?
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def valid_invitation_token?
|
||||
@invitation&.can_be_accepted?
|
||||
end
|
||||
|
||||
def invitation_token
|
||||
@invitation_token ||= params[:invitation_token] ||
|
||||
params.dig(:user, :invitation_token) ||
|
||||
session[:invitation_token]
|
||||
end
|
||||
|
||||
def accept_invitation_for_user(user)
|
||||
return unless @invitation&.can_be_accepted?
|
||||
|
||||
service = Families::AcceptInvitation.new(
|
||||
invitation: @invitation,
|
||||
user: user
|
||||
)
|
||||
|
||||
if service.call
|
||||
flash[:notice] = "Welcome to #{@invitation.family.name}! You're now part of the family."
|
||||
else
|
||||
flash[:alert] =
|
||||
"Account created successfully, but there was an issue accepting the invitation: #{service.error_message}"
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error accepting invitation during registration: #{e.message}"
|
||||
flash[:alert] =
|
||||
'Account created successfully, but there was an issue accepting the invitation. Please try accepting it again.'
|
||||
end
|
||||
|
||||
def sign_up_params
|
||||
super
|
||||
end
|
||||
|
||||
def email_password_registration_allowed?
|
||||
ALLOW_EMAIL_PASSWORD_REGISTRATION
|
||||
end
|
||||
end
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Users::SessionsController < Devise::SessionsController
|
||||
before_action :load_invitation_context, only: [:new]
|
||||
|
||||
def new
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_invitation_context
|
||||
return unless invitation_token.present?
|
||||
|
||||
@invitation = Family::Invitation.find_by(token: invitation_token)
|
||||
# Store token in session so it persists through the sign-in process
|
||||
session[:invitation_token] = invitation_token if invitation_token.present?
|
||||
end
|
||||
|
||||
def invitation_token
|
||||
@invitation_token ||= params[:invitation_token] || session[:invitation_token]
|
||||
end
|
||||
end
|
||||
|
|
@ -1,23 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ApplicationHelper
|
||||
def flash_alert_class(type)
|
||||
case type.to_sym
|
||||
when :notice, :success then 'alert-success'
|
||||
when :alert, :error then 'alert-error'
|
||||
when :warning then 'alert-warning'
|
||||
when :info then 'alert-info'
|
||||
else 'alert-info'
|
||||
end
|
||||
end
|
||||
|
||||
def flash_icon(type)
|
||||
case type.to_sym
|
||||
when :notice, :success then icon 'circle-check'
|
||||
when :alert, :error then icon 'circle-x'
|
||||
when :warning then icon 'circle-alert'
|
||||
def classes_for_flash(flash_type)
|
||||
case flash_type.to_sym
|
||||
when :error
|
||||
'bg-red-100 text-red-700 border-red-300'
|
||||
else
|
||||
icon 'info'
|
||||
'bg-blue-100 text-blue-700 border-blue-300'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -130,23 +119,4 @@ module ApplicationHelper
|
|||
'btn-success'
|
||||
end
|
||||
end
|
||||
|
||||
def oauth_provider_name(provider)
|
||||
return OIDC_PROVIDER_NAME if provider == :openid_connect
|
||||
|
||||
OmniAuth::Utils.camelize(provider)
|
||||
end
|
||||
|
||||
def email_password_registration_enabled?
|
||||
return true unless DawarichSettings.self_hosted?
|
||||
|
||||
ALLOW_EMAIL_PASSWORD_REGISTRATION
|
||||
end
|
||||
|
||||
def preferred_map_path
|
||||
return map_v1_path unless user_signed_in?
|
||||
|
||||
preferred_version = current_user.safe_settings.maps&.dig('preferred_version')
|
||||
preferred_version == 'v2' ? map_v2_path : map_v1_path
|
||||
end
|
||||
end
|
||||
|
|
|
|||