diff --git a/serverless/alpr_counts/src/alpr_counts.py b/serverless/alpr_counts/src/alpr_counts.py index bc3bdc6..459e9c2 100644 --- a/serverless/alpr_counts/src/alpr_counts.py +++ b/serverless/alpr_counts/src/alpr_counts.py @@ -3,8 +3,11 @@ import requests import boto3 from concurrent.futures import ThreadPoolExecutor, as_completed +DF_USER_AGENT = 'deflock-alpr-counts/1.0' + def fetch_alpr_surveillance_nodes(usOnly=True): overpass_url = "http://overpass-api.de/api/interpreter" + headers = {'User-Agent': DF_USER_AGENT} overpass_query = """ [out:json][timeout:180]; area["ISO3166-1"="US"]->.searchArea; @@ -12,7 +15,7 @@ def fetch_alpr_surveillance_nodes(usOnly=True): out count; """ - response = requests.get(overpass_url, params={'data': overpass_query}) + response = requests.get(overpass_url, params={'data': overpass_query}, headers=headers) if response.status_code == 200: response_json = response.json() @@ -25,7 +28,7 @@ def fetch_alpr_surveillance_nodes(usOnly=True): def fetch_wins_count(): cms_url = "https://cms.deflock.me/items/flockWins" - headers = {'User-Agent': 'deflock-alpr-counts/1.0'} + headers = {'User-Agent': DF_USER_AGENT} response = requests.get(cms_url, headers=headers) diff --git a/serverless/blog_scraper/.python-version b/serverless/blog_scraper/.python-version deleted file mode 100644 index 6324d40..0000000 --- a/serverless/blog_scraper/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.14 diff --git a/serverless/blog_scraper/README.md b/serverless/blog_scraper/README.md deleted file mode 100644 index 51a26af..0000000 --- a/serverless/blog_scraper/README.md +++ /dev/null @@ -1,192 +0,0 @@ -# Blog RSS Scraper - -This Lambda function ingests RSS feeds into a Directus CMS instance. It's specifically configured to pull from the "Have I Been Flocked?" RSS feed and sync the posts with your Directus blog collection. - -## Features - -- **RSS Feed Parsing**: Extracts title, link, pubDate, and description from RSS entries -- **Directus Integration**: Creates, updates, and deletes blog posts via Directus API -- **Idempotent Operation**: Safe to run multiple times - only makes necessary changes -- **Selective Sync**: Only manages RSS-ingested posts (identified by `externalUrl` field) -- **Error Handling**: Comprehensive logging and error recovery - -## Setup - -### Environment Variables - -Set the following environment variables: - -```bash -# Required -DIRECTUS_API_TOKEN=your_directus_api_token_here - -# Optional (defaults to https://cms.deflock.me) -DIRECTUS_BASE_URL=https://your-directus-instance.com -``` - -### Directus Collection Schema - -Your Directus `blog` collection should have the following fields: - -- `id` (integer, auto-increment) -- `title` (string, required) -- `description` (text) -- `content` (rich text, optional - RSS posts will have this as null) -- `externalUrl` (string, optional - identifies RSS-ingested posts) -- `published` (datetime) - -### Dependencies - -Install dependencies using uv: - -```bash -uv init -``` - -## Usage - -### Local Testing - -```bash -uv run main.py -``` - -### AWS Lambda - -Deploy as a Python 3.14 Lambda function. The `lambda_handler` function serves as the entry point. - -#### Sample Lambda Event - -The function doesn't require any specific event data: - -```json -{} -``` - -#### Sample Response - -Success: -```json -{ - "statusCode": 200, - "body": { - "message": "RSS synchronization completed successfully", - "stats": { - "created": 2, - "updated": 1, - "deleted": 0, - "errors": 0 - } - } -} -``` - -Error: -```json -{ - "statusCode": 500, - "body": { - "message": "RSS synchronization failed", - "error": "DIRECTUS_API_TOKEN environment variable is required" - } -} -``` - -## How It Works - -1. **Fetch RSS Feed**: Downloads and parses the RSS feed from `https://haveibeenflocked.com/feed.xml` - -2. **Get Existing Posts**: Queries Directus for all blog posts that have an `externalUrl` (these are RSS-managed posts) - -3. **Synchronization**: - - **Create**: New RSS entries that don't exist in Directus - - **Update**: Existing posts where title or description has changed - - **Delete**: Directus posts with `externalUrl` that no longer exist in the RSS feed - -4. **Preserve Manual Posts**: Posts without an `externalUrl` are left untouched - -## RSS Feed Structure - -The scraper expects standard RSS 2.0 format with the following elements: -- ``: Post title -- `<link>`: Post URL (becomes `externalUrl`) -- `<pubDate>`: Publication date (becomes `published`) -- `<description>` or `<content>`: Post description (HTML tags are stripped) - -## Error Handling - -- Invalid dates are logged as warnings but don't stop processing -- Individual post errors are logged and counted but don't stop the entire sync -- HTTP errors from Directus API are logged with full details -- Missing environment variables cause immediate failure with clear error messages - -## Logging - -The function uses Python's standard logging module with INFO level. Key events logged: - -- RSS feed fetch status -- Number of entries parsed -- Create/update/delete operations -- Errors and warnings -- Final synchronization statistics - -## Security Considerations - -- Store the Directus API token securely (AWS Secrets Manager recommended for production) -- Use HTTPS for all API communications (enforced by default) -- The function only modifies posts with `externalUrl` - manual posts are safe -- Consider rate limiting if running frequently - -## Deployment - -### AWS Lambda Deployment Package - -1. Navigate to [the terraform directory](../../terraform/). -2. Set the required variables in a local copy of `terraform.tfvars`. -3. Run `terraform apply`. - -### Environment Variables in Lambda - -Set in the Lambda function configuration: -- `DIRECTUS_API_TOKEN`: Your Directus API token -- `DIRECTUS_BASE_URL`: Your Directus instance URL (optional) - -### Scheduling - -The Terraform configutaiton sets up a CloudWatch Events rule to run this function periodically. - -## Troubleshooting - -### Common Issues - -1. **401 Unauthorized**: Check your `DIRECTUS_API_TOKEN` -2. **404 Not Found**: Verify `DIRECTUS_BASE_URL` and collection name (`blog`) -3. **RSS Parse Errors**: Check if the RSS feed is accessible and valid -4. **Date Parse Failures**: Usually logged as warnings and don't stop processing - -### Testing Connection - -The function will fail fast if it can't connect to Directus, making debugging easier. - -## Development - -### Local Development Setup - -```bash -# Clone and navigate to the blog_scraper directory -cd serverless/blog_scraper - -# Install dependencies -uv init - -# Set environment variables -export DIRECTUS_API_TOKEN="your_token" -export DIRECTUS_BASE_URL="https://cms.deflock.me" - -# Run locally -uv run main.py -``` - -### Testing with Different RSS Feeds - -To test with a different RSS feed, modify the `rss_url` in the `BlogScraper.__init__` method. diff --git a/serverless/blog_scraper/main.py b/serverless/blog_scraper/main.py deleted file mode 100644 index deae33d..0000000 --- a/serverless/blog_scraper/main.py +++ /dev/null @@ -1,310 +0,0 @@ -import os -import logging -import feedparser -import requests -import json -from datetime import datetime -from dateutil import parser as date_parser -from typing import List, Dict, Optional -from urllib.parse import urlencode, urlparse - -# Configure logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -class BlogScraper: - """RSS feed scraper that ingests blog posts into Directus CMS""" - - def __init__(self): - self.rss_url = "https://haveibeenflocked.com/feed.xml" - self.directus_base_url = os.getenv("DIRECTUS_BASE_URL", "https://cms.deflock.me") - self.directus_token = os.getenv("DIRECTUS_API_TOKEN") - - if not self.directus_token: - raise ValueError("DIRECTUS_API_TOKEN environment variable is required") - - self.headers = { - "Authorization": f"Bearer {self.directus_token}", - "Content-Type": "application/json", - "User-Agent": "deflock-blog-scraper/1.0" - } - - # Extract host from RSS URL for filtering - self.rss_host = urlparse(self.rss_url).netloc - - def fetch_rss_feed(self) -> Optional[feedparser.FeedParserDict]: - """Fetch and parse the RSS feed. Returns None if connection fails.""" - logger.info(f"Fetching RSS feed from {self.rss_url}") - - try: - feed = feedparser.parse(self.rss_url) - if feed.bozo: - logger.warning(f"Feed parsing warning: {feed.bozo_exception}") - - logger.info(f"Successfully parsed RSS feed with {len(feed.entries)} entries") - return feed - except Exception as e: - logger.error(f"Error fetching RSS feed: {e}. Skipping sync to prevent data loss.") - return None - - def get_existing_posts(self) -> List[Dict]: - """Get all existing blog posts from Directus that have external URLs""" - logger.info("Fetching existing blog posts from Directus") - - try: - # Filter for posts that have an externalUrl (RSS-ingested posts) - url = f"{self.directus_base_url}/items/blog" - - # Properly format the filter as JSON and URL encode it - filter_obj = { - "externalUrl": { - "_nnull": True # not null - } - } - - params = { - "filter": json.dumps(filter_obj), - "limit": -1 # Get all records - } - - response = requests.get(url, headers=self.headers, params=params) - response.raise_for_status() - - data = response.json() - posts = data.get("data", []) - logger.info(f"Found {len(posts)} existing RSS-ingested posts") - return posts - - except Exception as e: - logger.error(f"Error fetching existing posts: {e}") - raise - - def is_same_host_as_rss(self, url: str) -> bool: - """Check if the given URL has the same host as the RSS feed""" - try: - url_host = urlparse(url).netloc - return url_host == self.rss_host - except Exception: - return False - - def create_blog_post(self, post_data: Dict) -> Optional[Dict]: - """Create a new blog post in Directus""" - logger.info(f"Creating new blog post: {post_data['title']}") - - try: - url = f"{self.directus_base_url}/items/blog" - - response = requests.post(url, headers=self.headers, json=post_data) - - if response.status_code >= 400: - logger.error(f"HTTP {response.status_code} error response body: {response.text}") - - response.raise_for_status() - - created_post = response.json() - logger.info(f"Successfully created blog post with ID: {created_post['data']['id']}") - return created_post["data"] - - except requests.exceptions.HTTPError as e: - logger.error(f"HTTP error creating blog post '{post_data['title']}': {e}") - logger.error(f"Response content: {response.text if 'response' in locals() else 'No response available'}") - raise - except Exception as e: - logger.error(f"Error creating blog post '{post_data['title']}': {e}") - raise - - def update_blog_post(self, post_id: int, post_data: Dict) -> Optional[Dict]: - """Update an existing blog post in Directus""" - logger.info(f"Updating blog post ID {post_id}: {post_data['title']}") - - try: - url = f"{self.directus_base_url}/items/blog/{post_id}" - - response = requests.patch(url, headers=self.headers, json=post_data) - - if response.status_code >= 400: - logger.error(f"HTTP {response.status_code} error response body: {response.text}") - - response.raise_for_status() - - updated_post = response.json() - logger.info(f"Successfully updated blog post ID: {post_id}") - return updated_post["data"] - - except requests.exceptions.HTTPError as e: - logger.error(f"HTTP error updating blog post ID {post_id}: {e}") - logger.error(f"Response content: {response.text if 'response' in locals() else 'No response available'}") - raise - except Exception as e: - logger.error(f"Error updating blog post ID {post_id}: {e}") - raise - - def delete_blog_post(self, post_id: int) -> None: - """Delete a blog post from Directus""" - logger.info(f"Deleting blog post ID {post_id}") - - try: - url = f"{self.directus_base_url}/items/blog/{post_id}" - response = requests.delete(url, headers=self.headers) - response.raise_for_status() - - logger.info(f"Successfully deleted blog post ID: {post_id}") - - except Exception as e: - logger.error(f"Error deleting blog post ID {post_id}: {e}") - raise - - def parse_feed_entry(self, entry) -> Dict: - """Parse a feed entry into Directus blog post format""" - # Parse the publication date - pub_date = None - if hasattr(entry, 'published'): - try: - pub_date = date_parser.parse(entry.published).isoformat() - except Exception as e: - logger.warning(f"Could not parse date {entry.published}: {e}") - - # Extract description from summary or content - description = "" - if hasattr(entry, 'summary'): - description = entry.summary - elif hasattr(entry, 'content') and entry.content: - # Take the first content item's value - description = entry.content[0].value if entry.content else "" - - # Clean up the description (remove HTML tags if present) - # For production, you might want to use a proper HTML parser like BeautifulSoup - import re - description = re.sub(r'<[^>]+>', '', description) - description = description.strip() - - post_data = { - "title": entry.title, - "description": description, - "externalUrl": entry.link, - "content": None, # RSS posts don't have content, just external links - } - - if pub_date: - post_data["published"] = pub_date - - return post_data - - def sync_rss_posts(self) -> Dict[str, int]: - """Main synchronization logic - ensures RSS feed matches Directus""" - logger.info("Starting RSS to Directus synchronization") - - # Fetch RSS feed - feed = self.fetch_rss_feed() - - # If feed fetch failed, return early to prevent data loss - if feed is None: - logger.warning("Skipping synchronization due to RSS feed fetch failure") - return { - "created": 0, - "updated": 0, - "deleted": 0, - "errors": 1 - } - - # Get existing posts from Directus - all_existing_posts = self.get_existing_posts() - - # Filter existing posts to only include those from the same host as RSS feed - existing_posts = [post for post in all_existing_posts - if post.get("externalUrl") and self.is_same_host_as_rss(post["externalUrl"])] - - logger.info(f"Found {len(existing_posts)} existing posts from RSS host {self.rss_host}") - - # Create lookup by external URL - existing_by_url = {post["externalUrl"]: post for post in existing_posts} - - stats = { - "created": 0, - "updated": 0, - "deleted": 0, - "errors": 0 - } - - # Track URLs from RSS feed - rss_urls = set() - - # Process each RSS entry - for entry in feed.entries: - try: - post_data = self.parse_feed_entry(entry) - url = post_data["externalUrl"] - rss_urls.add(url) - - if url in existing_by_url: - # Update existing post if needed - existing_post = existing_by_url[url] - - # Check if update is needed (compare title and description) - needs_update = ( - existing_post["title"] != post_data["title"] or - existing_post["description"] != post_data["description"] - ) - - if needs_update: - self.update_blog_post(existing_post["id"], post_data) - stats["updated"] += 1 - else: - # Create new post - self.create_blog_post(post_data) - stats["created"] += 1 - - except Exception as e: - logger.error(f"Error processing RSS entry {entry.link}: {e}") - stats["errors"] += 1 - - # Delete posts that are no longer in RSS feed - for existing_post in existing_posts: - if existing_post["externalUrl"] not in rss_urls: - try: - self.delete_blog_post(existing_post["id"]) - stats["deleted"] += 1 - except Exception as e: - logger.error(f"Error deleting post {existing_post['id']}: {e}") - stats["errors"] += 1 - - logger.info(f"Synchronization complete. Stats: {stats}") - return stats - - -def lambda_handler(event, context): - """AWS Lambda handler function""" - try: - scraper = BlogScraper() - stats = scraper.sync_rss_posts() - - return { - 'statusCode': 200, - 'body': { - 'message': 'RSS synchronization completed successfully', - 'stats': stats - } - } - except Exception as e: - logger.error(f"Lambda execution failed: {e}") - return { - 'statusCode': 500, - 'body': { - 'message': 'RSS synchronization failed', - 'error': str(e) - } - } - - -def main(): - """Main function for local testing""" - try: - scraper = BlogScraper() - stats = scraper.sync_rss_posts() - print(f"Synchronization completed with stats: {stats}") - except Exception as e: - print(f"Error: {e}") - - -if __name__ == "__main__": - main() diff --git a/serverless/blog_scraper/pyproject.toml b/serverless/blog_scraper/pyproject.toml deleted file mode 100644 index ebef427..0000000 --- a/serverless/blog_scraper/pyproject.toml +++ /dev/null @@ -1,11 +0,0 @@ -[project] -name = "blog-scraper" -version = "0.1.0" -description = "Pulls RSS feed from HIBF and stores in Directus CMS" -readme = "README.md" -requires-python = ">=3.14" -dependencies = [ - "feedparser>=6.0.11", - "requests>=2.32.0", - "python-dateutil>=2.9.0" -] diff --git a/serverless/blog_scraper/uv.lock b/serverless/blog_scraper/uv.lock deleted file mode 100644 index 292b4c6..0000000 --- a/serverless/blog_scraper/uv.lock +++ /dev/null @@ -1,126 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.14" - -[[package]] -name = "blog-scraper" -version = "0.1.0" -source = { virtual = "." } -dependencies = [ - { name = "feedparser" }, - { name = "python-dateutil" }, - { name = "requests" }, -] - -[package.metadata] -requires-dist = [ - { name = "feedparser", specifier = ">=6.0.11" }, - { name = "python-dateutil", specifier = ">=2.9.0" }, - { name = "requests", specifier = ">=2.32.0" }, -] - -[[package]] -name = "certifi" -version = "2025.11.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, -] - -[[package]] -name = "feedparser" -version = "6.0.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sgmllib3k" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/79/db7edb5e77d6dfbc54d7d9df72828be4318275b2e580549ff45a962f6461/feedparser-6.0.12.tar.gz", hash = "sha256:64f76ce90ae3e8ef5d1ede0f8d3b50ce26bcce71dd8ae5e82b1cd2d4a5f94228", size = 286579, upload-time = "2025-09-10T13:33:59.486Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/eb/c96d64137e29ae17d83ad2552470bafe3a7a915e85434d9942077d7fd011/feedparser-6.0.12-py3-none-any.whl", hash = "sha256:6bbff10f5a52662c00a2e3f86a38928c37c48f77b3c511aedcd51de933549324", size = 81480, upload-time = "2025-09-10T13:33:58.022Z" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "requests" -version = "2.32.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, -] - -[[package]] -name = "sgmllib3k" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/bd/3704a8c3e0942d711c1299ebf7b9091930adae6675d7c8f476a7ce48653c/sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9", size = 5750, upload-time = "2010-08-24T14:33:52.445Z" } - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "urllib3" -version = "2.6.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, -] diff --git a/terraform/main.tf b/terraform/main.tf index be29272..22604b0 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -23,15 +23,6 @@ module "alpr_cache" { sns_topic_arn = aws_sns_topic.lambda_alarms.arn } -module "blog_scraper" { - module_name = "blog_scraper" - source = "./modules/blog_scraper" - rate = "rate(30 minutes)" - sns_topic_arn = aws_sns_topic.lambda_alarms.arn - directus_base_url = var.directus_base_url - directus_api_token = var.directus_api_token -} - resource "aws_sns_topic" "lambda_alarms" { name = "lambda_alarms_topic" } diff --git a/terraform/modules/blog_scraper/main.tf b/terraform/modules/blog_scraper/main.tf deleted file mode 100644 index d2366ed..0000000 --- a/terraform/modules/blog_scraper/main.tf +++ /dev/null @@ -1,133 +0,0 @@ -resource "aws_iam_role" "lambda_role" { - name = "blog_scraper_lambda_role" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = "lambda.amazonaws.com" - } - } - ] - }) -} - -resource "aws_iam_policy" "lambda_basic_execution_policy" { - name = "blog_scraper_lambda_basic_execution_policy" - description = "Basic execution policy for blog scraper Lambda function" - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Action = [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents" - ] - Resource = "arn:aws:logs:*:*:*" - } - ] - }) -} - -resource "aws_iam_role_policy_attachment" "lambda_basic_execution_attachment" { - role = aws_iam_role.lambda_role.name - policy_arn = aws_iam_policy.lambda_basic_execution_policy.arn -} - -# Install dependencies using uv (since it's a uv project) -resource "null_resource" "uv_install" { - provisioner "local-exec" { - command = <<EOT - cd ${path.root}/../serverless/${var.module_name} - # Create build directory (ignored by git) - rm -rf .build - mkdir -p .build - - # Install dependencies using uv into build directory - uv pip install --system --target .build -r pyproject.toml - - # Copy the main.py file to the build directory - cp main.py .build/ - EOT - } - - triggers = { - # Re-run if pyproject.toml or main.py changes - pyproject_hash = filemd5("${path.root}/../serverless/${var.module_name}/pyproject.toml") - main_py_hash = filemd5("${path.root}/../serverless/${var.module_name}/main.py") - } -} - -data "archive_file" "python_lambda_package" { - type = "zip" - source_dir = "${path.root}/../serverless/${var.module_name}/.build" - output_path = "${path.root}/../serverless/${var.module_name}/lambda.zip" - - depends_on = [null_resource.uv_install] -} - -resource "aws_lambda_function" "blog_scraper_lambda" { - filename = data.archive_file.python_lambda_package.output_path - function_name = var.module_name - role = aws_iam_role.lambda_role.arn - handler = "main.lambda_handler" - runtime = "python3.14" - source_code_hash = data.archive_file.python_lambda_package.output_base64sha256 - timeout = 300 - - environment { - variables = { - DIRECTUS_BASE_URL = var.directus_base_url - DIRECTUS_API_TOKEN = var.directus_api_token - } - } -} - -resource "aws_cloudwatch_event_rule" "lambda_rule" { - name = "${var.module_name}_rule" - description = "Rule to trigger ${var.module_name} lambda every 30 minutes" - schedule_expression = var.rate -} - -resource "aws_cloudwatch_event_target" "lambda_target" { - target_id = "${var.module_name}_target" - rule = aws_cloudwatch_event_rule.lambda_rule.name - arn = aws_lambda_function.blog_scraper_lambda.arn -} - -resource "aws_lambda_permission" "allow_cloudwatch_to_call_lambda" { - statement_id = "AllowExecutionFromCloudWatch" - action = "lambda:InvokeFunction" - function_name = aws_lambda_function.blog_scraper_lambda.function_name - principal = "events.amazonaws.com" - source_arn = aws_cloudwatch_event_rule.lambda_rule.arn -} - -resource "aws_cloudwatch_log_group" "lambda_log_group" { - name = "/aws/lambda/${var.module_name}" - retention_in_days = 14 -} - -# CloudWatch alarm for Lambda errors -resource "aws_cloudwatch_metric_alarm" "lambda_error_alarm" { - alarm_name = "${var.module_name}_error_alarm" - comparison_operator = "GreaterThanThreshold" - evaluation_periods = "2" - metric_name = "Errors" - namespace = "AWS/Lambda" - period = "300" - statistic = "Sum" - threshold = "0" - alarm_description = "This metric monitors lambda errors for ${var.module_name}" - alarm_actions = [var.sns_topic_arn] - - dimensions = { - FunctionName = aws_lambda_function.blog_scraper_lambda.function_name - } -} \ No newline at end of file diff --git a/terraform/modules/blog_scraper/outputs.tf b/terraform/modules/blog_scraper/outputs.tf deleted file mode 100644 index 2f5328a..0000000 --- a/terraform/modules/blog_scraper/outputs.tf +++ /dev/null @@ -1,14 +0,0 @@ -output "lambda_function_arn" { - description = "ARN of the blog scraper Lambda function" - value = aws_lambda_function.blog_scraper_lambda.arn -} - -output "lambda_function_name" { - description = "Name of the blog scraper Lambda function" - value = aws_lambda_function.blog_scraper_lambda.function_name -} - -output "cloudwatch_event_rule_arn" { - description = "ARN of the CloudWatch Event Rule" - value = aws_cloudwatch_event_rule.lambda_rule.arn -} \ No newline at end of file diff --git a/terraform/modules/blog_scraper/variables.tf b/terraform/modules/blog_scraper/variables.tf deleted file mode 100644 index 6967a6f..0000000 --- a/terraform/modules/blog_scraper/variables.tf +++ /dev/null @@ -1,28 +0,0 @@ -variable "module_name" { - description = "Name of the module" - type = string - default = "blog_scraper" -} - -variable "rate" { - description = "Rate expression for CloudWatch Events rule" - type = string - default = "rate(30 minutes)" -} - -variable "sns_topic_arn" { - description = "SNS topic ARN for Lambda alarms" - type = string -} - -variable "directus_base_url" { - description = "Base URL for Directus CMS" - type = string - default = "https://cms.deflock.me" -} - -variable "directus_api_token" { - description = "API token for Directus CMS" - type = string - sensitive = true -} \ No newline at end of file diff --git a/webapp/bun.lock b/webapp/bun.lock new file mode 100644 index 0000000..2928984 --- /dev/null +++ b/webapp/bun.lock @@ -0,0 +1,355 @@ +{ + "lockfileVersion": 1, + "configVersion": 0, + "workspaces": { + "": { + "name": "deflock", + "dependencies": { + "@types/leaflet.markercluster": "^1.5.5", + "@unhead/vue": "^1.11.14", + "axios": "^1.7.7", + "countup.js": "^2.8.0", + "js-md5": "^0.8.3", + "leaflet.markercluster": "^1.5.3", + "pinia": "^2.3.0", + "vue": "^3.4.29", + "vue-router": "^4.3.3", + "vuetify": "^3.7.2", + }, + "devDependencies": { + "@mdi/font": "^7.4.47", + "@tsconfig/node20": "^20.1.4", + "@types/leaflet": "^1.9.15", + "@types/node": "^20.14.5", + "@vitejs/plugin-vue": "^6.0.2", + "@vue/tsconfig": "^0.5.1", + "leaflet": "^1.9.4", + "npm-run-all2": "^6.2.0", + "typescript": "~5.4.0", + "vite": "^7.2.6", + "vue-tsc": "^2.0.21", + }, + }, + }, + "packages": { + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@mdi/font": ["@mdi/font@7.4.47", "", {}, "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.50", "", {}, "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.3", "", { "os": "android", "cpu": "arm64" }, "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ=="], + + "@tsconfig/node20": ["@tsconfig/node20@20.1.8", "", {}, "sha512-Em+IdPfByIzWRRpqWL4Z7ArLHZGxmc36BxE3jCz9nBFSm+5aLaPMZyjwu4yetvyKXeogWcxik4L1jB5JTWfw7A=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + + "@types/leaflet": ["@types/leaflet@1.9.21", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w=="], + + "@types/leaflet.markercluster": ["@types/leaflet.markercluster@1.5.6", "", { "dependencies": { "@types/leaflet": "^1.9" } }, "sha512-I7hZjO2+isVXGYWzKxBp8PsCzAYCJBc29qBdFpquOCkS7zFDqUsUvkEOyQHedsk/Cy5tocQzf+Ndorm5W9YKTQ=="], + + "@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + + "@unhead/dom": ["@unhead/dom@1.11.20", "", { "dependencies": { "@unhead/schema": "1.11.20", "@unhead/shared": "1.11.20" } }, "sha512-jgfGYdOH+xHJF/j8gudjsYu3oIjFyXhCWcgKaw3vQnT616gSqyqnGQGOItL+BQtQZACKNISwIfx5PuOtztMKLA=="], + + "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], + + "@unhead/shared": ["@unhead/shared@1.11.20", "", { "dependencies": { "@unhead/schema": "1.11.20", "packrup": "^0.1.2" } }, "sha512-1MOrBkGgkUXS+sOKz/DBh4U20DNoITlJwpmvSInxEUNhghSNb56S0RnaHRq0iHkhrO/cDgz2zvfdlRpoPLGI3w=="], + + "@unhead/vue": ["@unhead/vue@1.11.20", "", { "dependencies": { "@unhead/schema": "1.11.20", "@unhead/shared": "1.11.20", "hookable": "^5.5.3", "unhead": "1.11.20" }, "peerDependencies": { "vue": ">=2.7 || >=3" } }, "sha512-sqQaLbwqY9TvLEGeq8Fd7+F2TIuV3nZ5ihVISHjWpAM3y7DwNWRU7NmT9+yYT+2/jw1Vjwdkv5/HvDnvCLrgmg=="], + + "@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.2", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.50" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vue": "^3.2.25" } }, "sha512-iHmwV3QcVGGvSC1BG5bZ4z6iwa1SOpAPWmnjOErd4Ske+lZua5K9TtAVdx0gMBClJ28DViCbSmZitjWZsWO3LA=="], + + "@volar/language-core": ["@volar/language-core@2.4.15", "", { "dependencies": { "@volar/source-map": "2.4.15" } }, "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA=="], + + "@volar/source-map": ["@volar/source-map@2.4.15", "", {}, "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg=="], + + "@volar/typescript": ["@volar/typescript@2.4.15", "", { "dependencies": { "@volar/language-core": "2.4.15", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg=="], + + "@vue/compiler-core": ["@vue/compiler-core@3.5.25", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/shared": "3.5.25", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw=="], + + "@vue/compiler-dom": ["@vue/compiler-dom@3.5.25", "", { "dependencies": { "@vue/compiler-core": "3.5.25", "@vue/shared": "3.5.25" } }, "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q=="], + + "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.25", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/compiler-core": "3.5.25", "@vue/compiler-dom": "3.5.25", "@vue/compiler-ssr": "3.5.25", "@vue/shared": "3.5.25", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag=="], + + "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.25", "", { "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/shared": "3.5.25" } }, "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A=="], + + "@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="], + + "@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="], + + "@vue/language-core": ["@vue/language-core@2.2.12", "", { "dependencies": { "@volar/language-core": "2.4.15", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^1.0.3", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" } }, "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA=="], + + "@vue/reactivity": ["@vue/reactivity@3.5.25", "", { "dependencies": { "@vue/shared": "3.5.25" } }, "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA=="], + + "@vue/runtime-core": ["@vue/runtime-core@3.5.25", "", { "dependencies": { "@vue/reactivity": "3.5.25", "@vue/shared": "3.5.25" } }, "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA=="], + + "@vue/runtime-dom": ["@vue/runtime-dom@3.5.25", "", { "dependencies": { "@vue/reactivity": "3.5.25", "@vue/runtime-core": "3.5.25", "@vue/shared": "3.5.25", "csstype": "^3.1.3" } }, "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA=="], + + "@vue/server-renderer": ["@vue/server-renderer@3.5.25", "", { "dependencies": { "@vue/compiler-ssr": "3.5.25", "@vue/shared": "3.5.25" }, "peerDependencies": { "vue": "3.5.25" } }, "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ=="], + + "@vue/shared": ["@vue/shared@3.5.25", "", {}, "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg=="], + + "@vue/tsconfig": ["@vue/tsconfig@0.5.1", "", {}, "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ=="], + + "alien-signals": ["alien-signals@1.0.13", "", {}, "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "countup.js": ["countup.js@2.9.0", "", {}, "sha512-llqrvyXztRFPp6+i8jx25phHWcVWhrHO4Nlt0uAOSKHB8778zzQswa4MU3qKBvkXfJKftRYFJuVHez67lyKdHg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": "bin/esbuild" }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "he": ["he@1.2.0", "", { "bin": "bin/he" }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "js-md5": ["js-md5@0.8.3", "", {}, "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@3.0.2", "", {}, "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ=="], + + "leaflet": ["leaflet@1.9.4", "", {}, "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="], + + "leaflet.markercluster": ["leaflet.markercluster@1.5.3", "", { "peerDependencies": { "leaflet": "^1.3.1" } }, "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "npm-normalize-package-bin": ["npm-normalize-package-bin@3.0.1", "", {}, "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ=="], + + "npm-run-all2": ["npm-run-all2@6.2.6", "", { "dependencies": { "ansi-styles": "^6.2.1", "cross-spawn": "^7.0.3", "memorystream": "^0.3.1", "minimatch": "^9.0.0", "pidtree": "^0.6.0", "read-package-json-fast": "^3.0.2", "shell-quote": "^1.7.3", "which": "^3.0.1" }, "bin": { "npm-run-all": "bin/npm-run-all/index.js", "npm-run-all2": "bin/npm-run-all/index.js", "run-p": "bin/run-p/index.js", "run-s": "bin/run-s/index.js" } }, "sha512-tkyb4pc0Zb0oOswCb5tORPk9MvVL6gcDq1cMItQHmsbVk1skk7YF6cH+UU2GxeNLHMuk6wFEOSmEmJ2cnAK1jg=="], + + "packrup": ["packrup@0.1.2", "", {}, "sha512-ZcKU7zrr5GlonoS9cxxrb5HVswGnyj6jQvwFBa6p5VFw7G71VAHcUKL5wyZSU/ECtPM/9gacWxy2KFQKt1gMNA=="], + + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pidtree": ["pidtree@0.6.0", "", { "bin": "bin/pidtree.js" }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], + + "pinia": ["pinia@2.3.1", "", { "dependencies": { "@vue/devtools-api": "^6.6.3", "vue-demi": "^0.14.10" }, "peerDependencies": { "typescript": ">=4.4.4", "vue": "^2.7.0 || ^3.5.11" } }, "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "read-package-json-fast": ["read-package-json-fast@3.0.2", "", { "dependencies": { "json-parse-even-better-errors": "^3.0.0", "npm-normalize-package-bin": "^3.0.0" } }, "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw=="], + + "rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "unhead": ["unhead@1.11.20", "", { "dependencies": { "@unhead/dom": "1.11.20", "@unhead/schema": "1.11.20", "@unhead/shared": "1.11.20", "hookable": "^5.5.3" } }, "sha512-3AsNQC0pjwlLqEYHLjtichGWankK8yqmocReITecmpB1H0aOabeESueyy+8X1gyJx4ftZVwo9hqQ4O3fPWffCA=="], + + "vite": ["vite@7.2.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ=="], + + "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "vue": ["vue@3.5.25", "", { "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/compiler-sfc": "3.5.25", "@vue/runtime-dom": "3.5.25", "@vue/server-renderer": "3.5.25", "@vue/shared": "3.5.25" }, "peerDependencies": { "typescript": "*" } }, "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g=="], + + "vue-demi": ["vue-demi@0.14.10", "", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="], + + "vue-router": ["vue-router@4.6.3", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg=="], + + "vue-tsc": ["vue-tsc@2.2.12", "", { "dependencies": { "@volar/typescript": "2.4.15", "@vue/language-core": "2.2.12" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": "bin/vue-tsc.js" }, "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw=="], + + "vuetify": ["vuetify@3.11.2", "", { "peerDependencies": { "typescript": ">=4.7", "vite-plugin-vuetify": ">=2.1.0", "vue": "^3.5.0", "webpack-plugin-vuetify": ">=3.1.0" }, "optionalPeers": ["vite-plugin-vuetify", "webpack-plugin-vuetify"] }, "sha512-1lL0qN6JIdbx6xGYpo6dnx378EfC0t4EotPJdP4go8ThmIdRO3xLva1ALxhxi5lSYTht4R9OVk9miVnwVfDx3A=="], + + "which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="], + + "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], + + "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + } +} diff --git a/webapp/public/map-interface-nationwide.webp b/webapp/public/map-interface-nationwide.webp index feebbc5..c359e37 100644 Binary files a/webapp/public/map-interface-nationwide.webp and b/webapp/public/map-interface-nationwide.webp differ diff --git a/webapp/public/new-df-map.webp b/webapp/public/new-df-map.webp new file mode 100644 index 0000000..759aa25 Binary files /dev/null and b/webapp/public/new-df-map.webp differ diff --git a/webapp/public/sitemap.xml b/webapp/public/sitemap.xml index 72b6de3..2baa89e 100644 --- a/webapp/public/sitemap.xml +++ b/webapp/public/sitemap.xml @@ -4,10 +4,6 @@ <loc>https://deflock.org/</loc> <changefreq>daily</changefreq> </url> - <url> - <loc>https://deflock.org/map</loc> - <changefreq>daily</changefreq> - </url> <url> <loc>https://deflock.org/about</loc> <changefreq>weekly</changefreq> diff --git a/webapp/src/App.vue b/webapp/src/App.vue index e7c24c6..9ae03f4 100644 --- a/webapp/src/App.vue +++ b/webapp/src/App.vue @@ -35,9 +35,9 @@ onMounted(() => { const items = [ { title: 'Home', icon: 'mdi-home', to: '/' }, - { title: 'Map', icon: 'mdi-map', to: '/map' }, + { title: 'Map', icon: 'mdi-map', href: 'https://maps.deflock.org' }, { title: 'Learn', icon: 'mdi-school', to: '/what-is-an-alpr' }, - { title: 'News', icon: 'mdi-newspaper', to: '/blog' }, + { title: 'News', icon: 'mdi-newspaper', href: 'https://footnote4a.org/', target: '_blank' }, ] const contributeItems = [ @@ -95,10 +95,12 @@ watch(() => theme.global.name.value, (newTheme) => { <div class="d-none d-md-flex ml-8 flex-grow-1"> <!-- Main navigation items --> <div class="d-flex align-center"> - <v-btn - v-for="item in items.slice(1)" + <v-btn + v-for="item in items.slice(1)" :key="item.title" :to="item.to" + :href="item.href" + :target="item.target" variant="text" class="mx-1" :prepend-icon="item.icon" @@ -196,6 +198,8 @@ watch(() => theme.global.name.value, (newTheme) => { :key="item.title" link :to="item.to" + :href="item.href" + :target="item.target" role="option" > <v-icon start>{{ item.icon }}</v-icon> diff --git a/webapp/src/components/NewVisitor.vue b/webapp/src/components/NewVisitor.vue deleted file mode 100644 index c00cf54..0000000 --- a/webapp/src/components/NewVisitor.vue +++ /dev/null @@ -1,63 +0,0 @@ -<template> - <v-dialog v-model="show" max-width="900"> - <v-card> - <v-card-title class="text-center py-4 font-weight-bold"> - <h3 class="headline">Welcome to DeFlock</h3> - </v-card-title> - <p class="mx-8 text-center"> - DeFlock is powered by <b>crowdsourced data</b> from the OpenStreetMap community. - </p> - - <v-divider class="mx-4 mt-4" /> - - <v-list class="text-center"> - <v-list-item class="my-4"> - <v-icon size="x-large" color="primary" class="mb-2">mdi-progress-pencil</v-icon> - <v-list-item-title class="font-weight-bold">The map is incomplete!</v-list-item-title> - <v-list-item-subtitle> - New locations are always being added. - </v-list-item-subtitle> - </v-list-item> - <v-list-item class="my-4"> - <v-icon size="x-large" color="primary" class="mb-2">mdi-square-edit-outline</v-icon> - <v-list-item-title class="font-weight-bold">Add missing points!</v-list-item-title> - <v-list-item-subtitle> - Know of a missing ALPR? <router-link to="/report/id">Contribute</router-link> to the map. - </v-list-item-subtitle> - </v-list-item> - </v-list> - - <div class="small text-grey-darken-2 text-center mx-4 mb-2">By using this site, you agree to our <router-link to="/terms">Terms of Service</router-link>.</div> - <v-card-actions> - <v-btn flat class="w-100" size="x-large" color="rgb(18, 151, 195)" variant="elevated" @click="acknowledge">Got it</v-btn> - </v-card-actions> - </v-card> - </v-dialog> -</template> - -<script setup lang="ts"> -import { onMounted, ref } from 'vue'; - -const show = ref(false); - -onMounted(() => { - if (!localStorage.getItem('acknowledged')) { - show.value = true; - } -}); - -function acknowledge() { - show.value = false; - localStorage.setItem('acknowledged', 'true'); -} -</script> - -<style scoped> -.no-small { - min-width: 160px; -} - -.small { - font-size: 0.8rem; -} -</style> diff --git a/webapp/src/router/index.ts b/webapp/src/router/index.ts index f2b87bb..d9c4035 100644 --- a/webapp/src/router/index.ts +++ b/webapp/src/router/index.ts @@ -1,6 +1,5 @@ import { createRouter, createWebHistory } from 'vue-router' import Landing from '../views/Landing.vue' -import Map from '../views/Map.vue' import { useHead } from '@unhead/vue' const router = createRouter({ @@ -25,11 +24,11 @@ const router = createRouter({ } }, { - path: '/map', + path: '/legacy-map', name: 'map', - component: Map, + component: () => import('../views/Map.vue'), meta: { - title: 'ALPR Map | DeFlock' + title: 'ALPR Map (Legacy) | DeFlock' } }, { @@ -204,7 +203,7 @@ router.beforeEach((to, from, next) => { } if (to.path === '/' && to.hash) { - next({ path: '/map', hash: to.hash }) + next({ path: '/legacy-map', hash: to.hash }) } else { next() } diff --git a/webapp/src/views/Donate.vue b/webapp/src/views/Donate.vue index d88cf7c..4ceb699 100644 --- a/webapp/src/views/Donate.vue +++ b/webapp/src/views/Donate.vue @@ -3,11 +3,10 @@ <template #header> <Hero title="Support DeFlock" - subtitle="Your contributions help us fight for privacy and raise awareness about ALPR technology." image-url="/donate.webp" background-position="0 25%" button-text="Donate Now" - button-href="https://github.com/sponsors/frillweeman" + button-href="https://buymeacoffee.com/deflock" :opacity="0.25" /> </template> @@ -16,9 +15,9 @@ <!-- GitHub Sponsors Section --> <v-row justify="center" class="sponsors-section text-center"> <v-col cols="12" md="10"> - <h2 class="mb-2">Our Amazing Sponsors</h2> + <h2 class="mb-2">Founding Sponsors</h2> <p class="mb-8"> - Want to see your name here? <a target="_blank" href="https://github.com/sponsors/frillweeman">Become a sponsor</a>, and your name will appear on this page! + <a target="_blank" href="https://buymeacoffee.com/deflock">We've made donating easier</a>, but if you want to see your name on this list, <a target="_blank" href="https://github.com/sponsors/frillweeman">sponsor us on GitHub!</a> </p> <v-row> @@ -37,7 +36,7 @@ </v-col> </v-row> - <!-- GitHub Sponsors Section --> + <!-- Special Thanks Section --> <v-row justify="center" class="sponsors-section text-center mt-4"> <v-col cols="12" md="10"> <h2 class="mb-2">Special Thanks</h2> diff --git a/webapp/src/views/Landing.vue b/webapp/src/views/Landing.vue index a2c22a8..6344072 100644 --- a/webapp/src/views/Landing.vue +++ b/webapp/src/views/Landing.vue @@ -48,8 +48,8 @@ <ALPRCounter class="my-6" /> - <v-btn size="large" color="rgb(18, 151, 195)" large @click="goToMap({ withCurrentLocation: true })"> - Explore the Map + <v-btn size="large" color="rgb(18, 151, 195)" large href="https://maps.deflock.org"> + Launch the Map <v-icon end>mdi-map</v-icon> </v-btn> </v-col> @@ -151,8 +151,8 @@ <!-- Map Section --> <v-container fluid class="map-section py-10 text-center"> <h2 class="display-2 mb-4">Explore ALPR Locations Near You</h2> - <v-btn color="white" large @click="goToMap({ withCurrentLocation: true })"> - View the Map + <v-btn color="white" large href="https://maps.deflock.org"> + Launch the Map <v-icon end>mdi-map</v-icon> </v-btn> </v-container> @@ -259,14 +259,9 @@ <script setup lang="ts"> import { ref, onMounted } from 'vue'; -import { useRouter } from 'vue-router'; import ALPRCounter from '@/components/ALPRCounter.vue'; -import { useGlobalStore } from '@/stores/global'; import DefaultLayout from '@/layouts/DefaultLayout.vue'; -const router = useRouter(); -const { setCurrentLocation } = useGlobalStore(); - interface ServiceAlert { id: number; date_updated: string | null; @@ -294,22 +289,4 @@ onMounted(() => { fetchServiceAlert(); }); -interface GoToMapOptions { - withCurrentLocation?: boolean; -} - -async function goToMap(options: GoToMapOptions = {}) { - if (options.withCurrentLocation) { - setCurrentLocation() - .then((currentLocation) => { - const [lat, lon] = currentLocation; - router.push({ path: '/map', hash: `#map=12/${lat.toFixed(6)}/${lon.toFixed(6)}` }); - }) - .catch(() => { - router.push({ path: '/map' }); - }); - } else { - router.push({ path: '/map' }); - } -} </script> diff --git a/webapp/src/views/Map.vue b/webapp/src/views/Map.vue index 71d102a..d7c3d2a 100644 --- a/webapp/src/views/Map.vue +++ b/webapp/src/views/Map.vue @@ -1,5 +1,4 @@ <template> - <NewVisitor v-if="!isIframe" /> <ShareDialog v-model="shareDialogOpen" /> <div class="map-container" @keyup="handleKeyUp"> @@ -61,6 +60,12 @@ import 'leaflet/dist/leaflet.css'; import { ref, onMounted, computed } from 'vue'; import { useRouter } from 'vue-router' +import { useHead } from '@unhead/vue' + +useHead({ + link: [{ rel: 'canonical', href: 'https://maps.deflock.org' }], + meta: [{ name: 'robots', content: 'noindex, nofollow' }] +}) import type { Ref } from 'vue'; import { BoundingBox } from '@/services/apiService'; import { geocodeQuery } from '@/services/apiService'; @@ -72,7 +77,6 @@ import L from 'leaflet'; globalThis.L = L; import 'leaflet/dist/leaflet.css' import LeafletMap from '@/components/LeafletMap.vue'; -import NewVisitor from '@/components/NewVisitor.vue'; import ShareDialog from '@/components/ShareDialog.vue'; const DEFAULT_ZOOM = 12; diff --git a/webapp/src/views/Press.vue b/webapp/src/views/Press.vue index f5c3224..50c4630 100644 --- a/webapp/src/views/Press.vue +++ b/webapp/src/views/Press.vue @@ -28,17 +28,7 @@ <h2>Embedding our Map</h2> <p> - You are welcome to embed our map on your website. Use the following HTML snippet: - </p> - - <p> - <DFCode> - <iframe src="https://deflock.org/map" width="100%" height="600" style="border: none;"></iframe> - </DFCode> - </p> - - <p> - If you would like to <b>localize the URL</b> to a specific region, please zoom to the area at <router-link to="/map">https://deflock.org/map</router-link> and copy the URL from your browser's address bar. + You are welcome to embed our map on your website. Simply <a href="https://maps.deflock.org" target="_blank">click the share button on our map</a>, and copy the embed code. </p> <h2>Contact Us</h2> @@ -53,5 +43,4 @@ <script setup lang="ts"> import DefaultLayout from '@/layouts/DefaultLayout.vue'; import Hero from '@/components/layout/Hero.vue'; -import DFCode from "@/components/DFCode.vue"; </script> diff --git a/webapp/src/views/PrivacyPolicy.vue b/webapp/src/views/PrivacyPolicy.vue index 7b5541e..fa9d351 100644 --- a/webapp/src/views/PrivacyPolicy.vue +++ b/webapp/src/views/PrivacyPolicy.vue @@ -13,6 +13,11 @@ DeFlock does not collect, store, or process any personal information about our users. We use local storage in your browser to anonymously identify first-time visitors for the purpose of showing an introductory message as well as to persist application state, including your dark/light theme preference. This data cannot be used to identify you personally. We do not use cookies, analytics, or tracking technologies on our website. </p> + <h2>Route Calculation (maps.deflock.org)</h2> + <p> + On our maps page (<a href="https://maps.deflock.org">maps.deflock.org</a>), we offer a route planning feature that calculates routes designed to avoid ALPR cameras. When you request a route, your start and end coordinates are sent to our server solely for the purpose of computing the route. These coordinates are not logged or retained long-term. + </p> + <h2>Third-Party Services</h2> <p> DeFlock relies on OpenStreetMap (OSM) for map data and functionality. If you choose to contribute Automatic License Plate Recognition (ALPR) data or other content to OSM, you will interact directly with their platform. OSM may request personal information, such as your email address and name, to facilitate your contributions. Please refer to the OpenStreetMap Privacy Policy for details on their data practices.