### Setup Working Directory and Serve Report in Python Source: https://context7.com/fastah/ip-geofeed-skills/llms.txt Configures cross-platform working directories for generated files and reports using `pathlib`. It also starts a local HTTP server to serve the generated HTML report and opens it in the default web browser. ```python from pathlib import Path # Resolve FASTAH_WORK_DIR (cross-platform) FASTAH_WORK_DIR = Path.home() / "fastah" FASTAH_DATA_DIR = FASTAH_WORK_DIR / "data" FASTAH_REPORT_DIR = FASTAH_WORK_DIR / "report" # Create directories FASTAH_DATA_DIR.mkdir(parents=True, exist_ok=True) FASTAH_REPORT_DIR.mkdir(parents=True, exist_ok=True) # File locations for pipeline outputs REPORT_DATA_FILE = FASTAH_DATA_DIR / "report-data.json" COMMENTS_FILE = FASTAH_DATA_DIR / "comments.json" MCP_PAYLOAD_FILE = FASTAH_DATA_DIR / "mcp-server-payload.json" HTML_REPORT_FILE = FASTAH_REPORT_DIR / "geofeed-report.html" # Serve report with local HTTP server import subprocess import webbrowser # Start server in background subprocess.Popen(['python3', '-m', 'http.server', '-d', str(FASTAH_REPORT_DIR), '4242']) # Open in browser webbrowser.open('http://127.0.0.1:4242/geofeed-report.html') ``` -------------------------------- ### Start Local Python HTTP Server Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/SKILL.md Starts a local Python HTTP server to serve the report directory on port 4242. This command should be run as a background process. ```bash python3 -m http.server -d FASTAH_REPORT_DIR 4242 ``` -------------------------------- ### Examples of IP Geofeed Entries Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/references/rfc8805.txt Provides concrete examples of IP Geofeed entries with different levels of geolocation detail. ```APIDOC ## Examples of IP Geofeed Entries ### Description This section provides examples of IP Geofeed entries demonstrating various IP address formats and geolocation granularities. ### Example Entries **Basic Examples**: ``` 192.0.2.0/25,US,US-AL,, 192.0.2.5,US,US-AL,Alabaster, 192.0.2.128/25,PL,PL-MZ,, 2001:db8::/32,PL,,, 2001:db8:cafe::/48,PL,PL-MZ,, ``` **IETF Network Geolocation Examples (Singapore)**: ``` # IETF106 (Singapore) - November 2019 - Singapore, SG 130.129.0.0/16,SG,SG-01,Singapore, 2001:df8::/32,SG,SG-01,Singapore, 31.133.128.0/18,SG,SG-01,Singapore, 31.130.224.0/20,SG,SG-01,Singapore, 2001:67c:1230::/46,SG,SG-01,Singapore, 2001:67c:370::/48,SG,SG-01,Singapore, ``` **RIPE NCC Conference Network Examples (Rotterdam)**: ``` 193.0.24.0/21,NL,NL-ZH,Rotterdam, 2001:67c:64::/48,NL,NL-ZH,Rotterdam, ``` **ICANN Portable Conference Network Examples (Marrakech)**: ``` 199.91.192.0/21,MA,MA-07,Marrakech 2620:f:8000::/48,MA,MA-07,Marrakech ``` ``` -------------------------------- ### AI Skill Invocation Examples Source: https://context7.com/fastah/ip-geofeed-skills/llms.txt Examples of how to invoke the Geofeed Tuner AI skill using different platforms like GitHub Copilot, Amp Code CLI, and the Context7 marketplace. ```bash # GitHub Copilot / Claude Code / Amp / ChatGPT Codex Hey chat, help me get started with the "Geofeed Tuner" agent skill from the SKILL.md file! :) ``` ```bash # Amp Code CLI amp skill add --overwrite https://github.com/fastah/ip-geofeed-skills.git /skill geofeed-tuner ``` ```bash # Context7 marketplace npx ctx7 skills search Geofeed Tuner for RFC 8805 CSV feeds ``` -------------------------------- ### Illustrative MCP Server Request Example Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/SKILL.md An illustrative example of data sent to the MCP server for processing. This structure includes rowKeys for mapping responses and country codes. Always defer to the schema returned by `tools/list` for authoritative field names and types. ```json [ {"rowKey": "550e8400-...", "countryCode":"CA", ...}, {"rowKey": "690e9301-...", "countryCode":"ZZ", ...} ] ``` -------------------------------- ### Fastah MCP Server Configuration Example Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/SKILL.md An example configuration for the Fastah MCP server, specifying the HTTP type and the server URL. This configuration is used to establish communication with the MCP service for geofeed data processing. ```json "fastahIpGeofeed": { "type": "http", "url": "https://mcp.fastah.ai/mcp" } ``` -------------------------------- ### Install and Invoke Geofeed Tuner via Amp Source: https://github.com/fastah/ip-geofeed-skills/blob/main/README.md Commands to add the Geofeed Tuner skill to the Amp CLI and subsequently invoke it for feed tuning tasks. ```shell amp skill add --overwrite https://github.com/fastah/ip-geofeed-skills.git /skill geofeed-tuner ``` -------------------------------- ### Comment Map JSON Example Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/SKILL.md An example of the JSON structure used to store comments and blank lines from a CSV file, keyed by line number. ```json { "4": "# It's OK for small city states to leave state ISO2 code unspecified" } ``` -------------------------------- ### RFC 8805 Geofeed CSV Format Example Source: https://context7.com/fastah/ip-geofeed-skills/llms.txt Example of the 5-column CSV format for IP geolocation feeds, including IP prefix, country code, region, city, and deprecated postal code. Comments are allowed and start with '#'. ```csv # RFC 8805 Geofeed CSV format # Columns: ip_prefix, alpha2code, region, city, postal_code (deprecated) 202.125.100.144/28,ID,,Jakarta, 2605:59c8:2700::/40,CA,CA-QC,"Montreal" 150.228.170.0/24,SG,SG-01,Singapore, # Comments are allowed and start with # 2406:2d40:8100::/42,SG,SG,Singapore, ``` -------------------------------- ### Validate ISO 3166 country and region codes Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/references/snippets-python3.md Examples for loading ISO 3166-1 alpha-2 country codes and ISO 3166-2 region codes from JSON files into sets for efficient validation. ```python import json # Load ISO 3166-1 country codes with open('assets/iso3166-1.json') as f: data = json.load(f) valid_countries = {c['alpha_2'] for c in data['3166-1']} # Load ISO 3166-2 region codes with open('assets/iso3166-2.json') as f: data = json.load(f) valid_regions = {r['code'] for r in data['3166-2']} ``` -------------------------------- ### MCP Server Payload Example Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/SKILL.md This JSON structure represents a payload for the MCP server, including a rowKey for matching responses and geographical data. Ensure the rowKey is a UUID and fields like countryCode, regionCode, and cityName are correctly formatted. ```json [ {"rowKey": "550e8400-e29b-41d4-a716-446655440000", "countryCode":"CA","regionCode":"CA-ON","cityName":"Toronto"}, {"rowKey": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "countryCode":"IN","regionCode":"IN-KA","cityName":"Bangalore"}, {"rowKey": "6ba7b811-9dad-11d1-80b4-00c04fd430c8", "countryCode":"IN","regionCode":"IN-KA"} ] ``` -------------------------------- ### Initialize Pagination and Event Listeners Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/scripts/templates/index.html Sets up the initial pagination state, handles the 'select all' checkbox functionality, and configures expandable rows with event listeners for checkboxes and row clicks. ```javascript window.addEventListener('DOMContentLoaded', function() { allRows = Array.from(document.querySelectorAll('.expandable-row')); initializePagination(); const selectAllCheckbox = document.getElementById('selectAll'); const rowCheckboxes = document.querySelectorAll('.row-checkbox'); selectAllCheckbox.addEventListener('change', function() { rowCheckboxes.forEach(checkbox => { checkbox.checked = selectAllCheckbox.checked; }); }); rowCheckboxes.forEach(checkbox => { checkbox.addEventListener('click', function(e) { e.stopPropagation(); }); checkbox.addEventListener('keydown', function(e) { if (e.key === ' ' || e.key === 'Enter') { e.stopPropagation(); } }); checkbox.addEventListener('change', function(e) { e.stopPropagation(); const allChecked = Array.from(rowCheckboxes).every(cb => cb.checked); const noneChecked = Array.from(rowCheckboxes).every(cb => !cb.checked); selectAllCheckbox.checked = allChecked; selectAllCheckbox.indeterminate = !allChecked && !noneChecked; }); }); const expandableRows = document.querySelectorAll('.expandable-row'); expandableRows.forEach(row => { const hasError = row.getAttribute('data-has-error') === 'true'; const hasWarning = row.getAttribute('data-has-warning') === 'true'; const hasSuggestion = row.getAttribute('data-has-suggestion') === 'true'; if (hasError || hasWarning || hasSuggestion) { let detailRow = row.nextElementSibling; while (detailRow && detailRow.classList.contains('expand-details-row')) { if (detailRow.classList.contains('issues-row')) { detailRow.classList.add('show'); } detailRow = detailRow.nextElementSibling; } } }); }); ``` -------------------------------- ### Define FASTAH Working Directories Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/SKILL.md Sets up the necessary working directories for Fastah data and reports. Ensures directories exist before use. ```python from pathlib import Path FASTAH_WORK_DIR = Path.home() / "fastah" FASTAH_DATA_DIR = FASTAH_WORK_DIR / "data" FASTAH_REPORT_DIR = FASTAH_WORK_DIR / "report" FASTAH_DATA_DIR.mkdir(parents=True, exist_ok=True) FASTAH_REPORT_DIR.mkdir(parents=True, exist_ok=True) ``` -------------------------------- ### Open Report in Browser Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/SKILL.md Opens the generated geofeed report in the system's default web browser. This is typically used after starting the local HTTP server. ```python webbrowser.open("http://127.0.0.1:4242/geofeed-report.html") ``` -------------------------------- ### Initialize Leaflet Map for Match Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/scripts/templates/index.html Initializes a Leaflet map with location boundary and marker for a match. It sets up the map, adds a tile layer, draws a rectangle for the bounding box, and places a marker. ```javascript function initializeMapForMatch(mapId, match) { try { const mapInstance = L.map(mapId, { scrollWheelZoom: false, dragging: true, zoomControl: true }); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap', maxZoom: 19 }).addTo(mapInstance); const bbox = match.boundingBox; if (bbox && bbox.length === 4) { const [west, north, east, south] = bbox; const centerLat = (south + north) / 2; const centerLng = (west + east) / 2; const isPoint = (west === east && north === south); if (isPoint) { const offset = 0.05; const bounds = [ [south - offset, west - offset], [north + offset, east + offset] ]; L.rectangle(bounds, { color: '#dc3545', weight: 3, opacity: 1, fillColor: '#dc3545', fillOpacity: 0.2 }).addTo(mapInstance); mapInstance.fitBounds(bounds, { padding: [30, 30] }); } else { const bounds = [ [south, west], [north, east] ]; L.rectangle(bounds, { color: '#dc3545', weight: 3, opacity: 1, fillColor: '#dc3545', fillOpacity: 0.2 }).addTo(mapInstance); mapInstance.fitBounds(bounds, { padding: [20, 20] }); } L.marker([centerLat, centerLng]).addTo(mapInstance) .bindPopup(`${escapeHtml(match.placeName)}
${escapeHtml(match.stateCode)}`); maps[mapId] = mapInstance; setTimeout(() => { mapInstance.invalidateSize(); }, TIMING.MAP_RESIZE_DELAY); } } catch (error) { console.error('Error initializing map:', error); } } ``` -------------------------------- ### Get Modal and Close Button Elements Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/scripts/templates/index.html Retrieves references to the modal and its close button from the DOM. These elements are used to control modal visibility and handle user interactions. ```javascript const modal = document.getElementById('locationModal'); const modalContent = document.querySelector('.modal-content'); const closeBtn = document.querySelector('.close'); ``` -------------------------------- ### Search for Geofeed Tuner via Context7 Source: https://github.com/fastah/ip-geofeed-skills/blob/main/README.md Uses the Context7 CLI to search for the Geofeed Tuner skill within the marketplace. ```shell npx ctx7 skills search Geofeed Tuner for RFC 8805 CSV feeds ``` -------------------------------- ### Fastah Place Search API Source: https://context7.com/fastah/ip-geofeed-skills/llms.txt Use the REST API or MCP tool to geocode place names and get tuning suggestions with bounding boxes and H3 cells. ```APIDOC ## Fastah Place Search API Use the REST API or MCP tool to geocode place names and get tuning suggestions with bounding boxes and H3 cells. ### POST /rest/geofeeds/place-search #### Description Searches for place location data using the Fastah API and returns matches with bounding boxes and H3 cells. #### Method POST #### Endpoint `https://mcp.fastah.ai/rest/geofeeds/place-search` #### Parameters ##### Request Body - **rows** (array) - Required - A list of geofeed entries to search for. - **rowKey** (string) - Required - A unique identifier for the row. - **countryCode** (string) - Required - The ISO 3166-1 alpha-2 country code. - **regionCode** (string) - Optional - The ISO 3166-2 region code (e.g., US-CA). - **cityName** (string) - Optional - The name of the city. - **searchMode** (string) - Optional - The search mode, defaults to 'auto'. ### Request Example ```json { "rows": [ { "rowKey": "unique-id-123", "countryCode": "CA", "regionCode": "CA-ON", "cityName": "Toronto", "searchMode": "auto" } ] } ``` ### Response #### Success Response (200) - **results** (array) - A list of search results. - **rowKey** (string) - The unique identifier for the row. - **matches** (array) - A list of matched places. - **placeName** (string) - The name of the place. - **countryCode** (string) - The country code. - **stateCode** (string) - The state or region code. - **placeType** (string) - The type of place (e.g., city). - **boundingBox** (array) - The bounding box coordinates [minLon, minLat, maxLon, maxLat]. - **h3Cells** (array) - A list of H3 cell identifiers. #### Response Example ```json { "results": [ { "rowKey": "unique-id-123", "matches": [ { "placeName": "Toronto", "countryCode": "CA", "stateCode": "CA-ON", "placeType": "city", "boundingBox": [-79.64, 43.85, -79.12, 43.58], "h3Cells": ["872a92d5bffffff", "872a92d59ffffff"] } ] } ] } ``` ### Batch Search #### Description Allows for batch geocoding of multiple geofeed entries in a single request (up to 1000 entries per request). #### Method POST #### Endpoint `https://mcp.fastah.ai/rest/geofeeds/place-search` #### Parameters ##### Request Body - **rows** (array) - Required - A list of geofeed entries to search for. - **rowKey** (string) - Required - A unique identifier for the row. - **countryCode** (string) - Required - The ISO 3166-1 alpha-2 country code. - **regionCode** (string) - Optional - The ISO 3166-2 region code (e.g., US-CA). - **cityName** (string) - Optional - The name of the city. - **searchMode** (string) - Optional - The search mode, defaults to 'auto'. ### Request Example ```python entries = [ {'countryCode': 'CA', 'regionCode': 'CA-ON', 'cityName': 'Toronto'}, {'countryCode': 'US', 'regionCode': 'US-CA', 'cityName': 'Los Angeles'} ] # Assuming batch_search function is defined as in the provided text # response = batch_search(entries) ``` ``` -------------------------------- ### Toggle Between Viewing and Tuning Modes Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/scripts/templates/index.html Switches the application's mode between viewing and tuning. It checks the current body class to determine the mode and applies the appropriate classes. ```javascript function toggleMode() { const isTuning = document.body.classList.contains('tuning-mode'); setMode(isTuning ? 'viewing' : 'tuning'); } ``` -------------------------------- ### Initialize Geofeed Tuner in AI Chat Source: https://github.com/fastah/ip-geofeed-skills/blob/main/README.md A prompt to initiate the Geofeed Tuner skill within an AI coding assistant environment. This command instructs the agent to read the SKILL.md file and begin the tuning process. ```text Hey chat, help me get started with the "Geofeed Tuner" agent skill from the SKILL.md file! :) ``` -------------------------------- ### Handle Tune Button Click Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/scripts/templates/index.html Initiates fetching location matches when a tune button is clicked. It identifies selected issues and prepares data for the API call. ```javascript async function handleTuneButtonClick(button) { currentTuningButton = button; const issuesContainer = button.closest('td'); const checkedBoxes = issuesContainer.querySelectorAll('.message-line input[type="checkbox"]:checked'); const selectedIssues = Array.from(checkedBoxes).map(cb => { const messageLine = cb.closest('.message-line'); return { id: messageLine.getAttribute('data-id'), message: messageLine.querySelector('span').textContent }; }); if (selectedIssues.length === 0) { alert('Please select at least one issue to tune.'); return; } const detailsRow = button.closest('tr'); let dataRow = detailsRow.previousElementSibling; while (dataRow && dataRow.classList.contains('expand-details-row')) { dataRow = dataRow.previousElementSibling; } if (!dataRow || !dataRow.classList.contains('expandable-row')) { console.error('Could not find data row'); return; } const rowData = prepareRowForApi(dataRow); if (!rowData) { alert('Missing required location data for API call'); return; } const apiResponse = await fetchPlaceMatches([rowData]); if (!apiResponse || !apiResponse.results || apiResponse.results.length === 0) { alert('No results returned from API'); return; } const result = apiResponse.results[0]; if (result.matches && result.matches.length > 0) { showLocationPopup(result.matches, selectedIssues); } else { alert('No location matches found for this entry'); } } ``` -------------------------------- ### Parse Geofeed CSV with Encoding Detection in Python Source: https://context7.com/fastah/ip-geofeed-skills/llms.txt Parses RFC 8805 geofeed CSV files, handling encoding detection (UTF-8, UTF-8-SIG, Latin-1), comments starting with '#', and missing columns. It normalizes entries and saves comments to a JSON file. ```python import csv import json from pathlib import Path def parse_geofeed_csv(filepath): """ Parse RFC 8805 geofeed CSV with encoding detection. Handles comments, missing columns, and UTF-8 BOM. """ # Try encodings in order encodings = ['utf-8', 'utf-8-sig', 'latin-1'] content = None for encoding in encodings: try: with open(filepath, encoding=encoding) as f: content = f.read() break except UnicodeDecodeError: continue if content is None: raise ValueError("Unable to decode input file") entries = [] comments = {} # line_number -> comment text for line_num, line in enumerate(content.splitlines(), 1): stripped = line.strip() # Handle comments and blank lines if not stripped or stripped.startswith('#'): comments[str(line_num)] = line continue # Parse CSV row reader = csv.reader([line]) row = list(reader)[0] # Pad to 4 columns minimum while len(row) < 4: row.append('') entries.append({ 'Line': line_num, 'IPPrefix': row[0].strip(), 'CountryCode': row[1].strip().upper(), 'RegionCode': row[2].strip(), 'City': row[3].strip(), 'PostalCode': row[4].strip() if len(row) > 4 else '' }) # Save comments for report generation with open(FASTAH_DATA_DIR / 'comments.json', 'w') as f: json.dump(comments, f) return entries, len(row) if entries else 4 ``` -------------------------------- ### Generate and Download CSV Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/scripts/templates/index.html Generates a CSV content string, creates a Blob, and simulates a download link for the user. The filename is derived from input metrics and ensured to have a .csv extension. ```javascript const csvContent = csvLines.join('\n'); // Create download link const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); // Get filename from inputFileMetrics, ensure it has .csv extension let filename = document.getElementById('inputFileMetrics') .textContent .trim() .split(/[/[]/) // Corrected split regex .pop() || 'geofeed_report.csv'; if (!filename.toLowerCase().endsWith('.csv')) { filename += '.csv'; } link.setAttribute('href', url); link.setAttribute('download', filename); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); ``` -------------------------------- ### Initialize Global State Variables Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/scripts/templates/index.html Initializes global variables for managing map instances, modal state, and selected items. Ensure these are properly updated during application runtime. ```javascript let maps = {}; let currentTuningButton = null; let selectedMatchIndex = 0; let matchItems = []; let isModalOpen = false; let summaryMapInstance = null; let summaryLayerGroup = null; let currentMapMode = 'bbox'; let summaryRowData = []; ``` -------------------------------- ### Derive Entry-Level Flags Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/SKILL.md After populating all messages for an entry, use this code to determine the overall status flags for the entry. These flags indicate the presence of errors, warnings, suggestions, or tunable messages. ```python entry["HasError"] = any(m["Type"] == "ERROR" for m in entry["Messages"]) entry["HasWarning"] = any(m["Type"] == "WARNING" for m in entry["Messages"]) entry["HasSuggestion"] = any(m["Type"] == "SUGGESTION" for m in entry["Messages"]) entry["Tunable"] = any(m["Checked"] for m in entry["Messages"]) ``` -------------------------------- ### IP Address and Subnet Validation in Python Source: https://context7.com/fastah/ip-geofeed-skills/llms.txt Demonstrates parsing and validating IPv4 and IPv6 addresses and subnets using Python's `ipaddress` module. Strict mode is used to catch host bit errors, and a helper function checks for non-public IP ranges. ```python import ipaddress # Parse IPv4 and IPv6 addresses ipaddress.ip_address('192.168.0.1') # IPv4Address ipaddress.ip_address('2001:db8::') # IPv6Address # Parse subnets with strict=True to catch errors ipaddress.ip_network('192.168.0.0/28', strict=True) # Valid ipaddress.ip_network('192.168.0.1/30', strict=True) # Raises ValueError: host bits set # Single-host representations ipaddress.ip_network('192.168.1.1/32', strict=True) # IPv4 single host ipaddress.ip_network('2001:db8::1/128', strict=True) # IPv6 single host # Detect non-public IP ranges (private, loopback, multicast, etc.) def is_non_public(network): """Check if a network is non-public for RFC 8805 purposes.""" return ( network.is_private or network.is_loopback or network.is_link_local or network.is_multicast or network.is_reserved or not network.is_global ) # Example usage net = ipaddress.ip_network('10.0.0.0/8', strict=True) print(is_non_public(net)) # True - private range not allowed in geofeeds ``` -------------------------------- ### Initialize Pagination Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/scripts/templates/index.html Initializes the pagination controls on page load by calling the updatePagination function to set the initial state. ```javascript function initializePagination() { updatePagination(); } ``` -------------------------------- ### Handle Row Click and Keyboard Events for Expansion Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/scripts/templates/index.html Attaches click and keyboard event listeners to rows to toggle the display of detail rows, including issues and previous values. Ensures that only rows with errors, warnings, or suggestions are focusable and expandable. Updates pagination after toggling. ```javascript === 'true'; // Only make rows with issues focusable if (hasError || hasWarning || hasSuggestion) { row.setAttribute('tabindex', '0'); // Click handler row.addEventListener('click', function(e) { // Don't expand if clicking on checkbox if (e.target.classList.contains('row-checkbox')) { return; } let detailRow = this.nextElementSibling; let expanding = false; // First pass: check if we are expanding (issues-row will be shown) while (detailRow && detailRow.classList.contains('expand-details-row')) { if (detailRow.classList.contains('issues-row')) { expanding = !detailRow.classList.contains('show'); break; } detailRow = detailRow.nextElementSibling; } detailRow = this.nextElementSibling; while (detailRow && detailRow.classList.contains('expand-details-row')) { if (detailRow.classList.contains('issues-row')) { // Always toggle issues-row detailRow.classList.toggle('show'); } else if (detailRow.classList.contains('previous-values-row')) { if (expanding) { // Show previous-values-row if this row has original data (was tuned) if (row.hasAttribute('data-original-country') || row.hasAttribute('data-original-region') || row.hasAttribute('data-original-city')) { detailRow.classList.add('show'); } } else { // If collapsing, hide previous-values-row detailRow.classList.remove('show'); } } detailRow = detailRow.nextElementSibling; } // Refresh pagination to show/hide the toggled detail rows updatePagination(); }); // Keyboard handler row.addEventListener('keydown', function(e) { // Don't handle if focus is on checkbox if (document.activeElement.classList.contains('row-checkbox')) { return; } if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); let detailRow = this.nextElementSibling; let expanding = false; // First pass: check if we are expanding (issues-row will be shown) while (detailRow && detailRow.classList.contains('expand-details-row')) { if (detailRow.classList.contains('issues-row')) { expanding = !detailRow.classList.contains('show'); break; } detailRow = detailRow.nextElementSibling; } detailRow = this.nextElementSibling; while (detailRow && detailRow.classList.contains('expand-details-row')) { if (detailRow.classList.contains('issues-row')) { // Always toggle issues-row detailRow.classList.toggle('show'); } else if (detailRow.classList.contains('previous-values-row')) { if (expanding) { // Show previous-values-row if this row has original data (was tuned) if (row.hasAttribute('data-original-country') || row.hasAttribute('data-original-region') || row.hasAttribute('data-original-city')) { detailRow.classList.add('show'); } } else { // If collapsing, hide previous-values-row detailRow.classList.remove('show'); } } detailRow = detailRow.nextElementSibling; } // Refresh pagination to show/hide the toggled detail rows updatePagination(); } }); } else { row.style.cursor = 'default'; } }); ``` -------------------------------- ### Show Location Matches Popup Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/scripts/templates/index.html Displays a modal popup with location matches, including maps and details. Allows users to select a match and handles the initial state of the popup. ```javascript function showLocationPopup(matches, selectedIssues = []) { if (!matches || matches.length === 0) { return; } const matchesList = document.getElementById('matchesList'); matchesList.innerHTML = ''; maps = {}; matchItems = []; selectedMatchIndex = 0; matches.forEach((match, index) => { const matchItem = document.createElement('div'); matchItem.className = 'match-item'; if (index === 0) { matchItem.classList.add('selected'); } const mapId = `map-${index}`; let primaryLocation = match.placeName; let locationType = 'city'; if (!primaryLocation || primaryLocation.trim() === '') { primaryLocation = match.stateCode; locationType = 'region'; } if (!primaryLocation || primaryLocation.trim() === '') { primaryLocation = match.countryCode; locationType = 'country'; } let secondaryInfo = []; if (match.placeName && locationType !== 'city') { secondaryInfo.push(`City: ${escapeHtml(match.placeName)}`); } if (match.stateCode && locationType !== 'region') { secondaryInfo.push(`Region: ${escapeHtml(match.stateCode)}`); } if (match.countryCode && locationType !== 'country') { secondaryInfo.push(`Country: ${escapeHtml(match.countryCode)}`); } matchItem.innerHTML = `
${escapeHtml(primaryLocation)}
${secondaryInfo.length > 0 ? `
${secondaryInfo.join(' \u2022 ')}
` : ''}
`; matchItem.addEventListener('click', function() { selectLocationMatch(match); }); matchesList.appendChild(matchItem); matchItems.push(matchItem); setTimeout(() => { }); }); } ``` -------------------------------- ### Parse IP addresses and subnets with Python ipaddress Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/references/snippets-python3.md Demonstrates the use of factory functions to parse IP addresses and network strings. It highlights the importance of using strict=True to ensure network integrity and catch invalid host bits in subnet definitions. ```python import ipaddress # Parsing addresses ipaddress.ip_address('192.168.0.1') ipaddress.ip_address('2001:db8::') # Parsing networks ipaddress.ip_network('192.168.0.0/28', strict=True) # This will raise ValueError: 192.168.0.1/30 has host bits set try: ipaddress.ip_network('192.168.0.1/30', strict=True) except ValueError as e: print(e) ``` -------------------------------- ### MCP Server Configuration Source: https://context7.com/fastah/ip-geofeed-skills/llms.txt Configure the Fastah MCP server for place name geocoding in your IDE settings. ```APIDOC ## MCP Server Configuration Configure the Fastah MCP server for place name geocoding in your IDE settings. ```json { "mcpServers": { "fastahIpGeofeed": { "type": "http", "url": "https://mcp.fastah.ai/mcp" } } } ``` ``` -------------------------------- ### Handle Modal Keyboard Navigation Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/scripts/templates/index.html Event listener for keyboard input within the modal. Supports closing the modal with Escape, selecting matches with Enter, and navigating with arrow keys. ```javascript modal.addEventListener('keydown', function(event) { if (!isModalOpen) return; if (event.key === 'Escape' || event.key === 'Esc') { event.preventDefault(); closeModal(); return; } if (event.key === 'Enter') { event.preventDefault(); if (matchItems[selectedMatchIndex]) { matchItems[selectedMatchIndex].click(); } return; } if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') { event.preventDefault(); navigateMatches(-1); return; } if (event.key === 'ArrowRight' || event.key === 'ArrowDown') { event.preventDefault(); navigateMatches(1); return; } if (event.key === '+' || event.key === '=') { event.preventDefault(); zoomCurrentMap(1); return; } if (event.key === '-' || event.key === '_') { event.preventDefault(); zoomCurrentMap(-1); return; } }); ``` -------------------------------- ### Initialize Map and Parse Row Data Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/scripts/templates/index.html Initializes a Leaflet map instance, sets up tile layers, and parses expandable row data to cache it for summary display. Includes error handling for map initialization. ```javascript function initSummaryMap() { try { const summaryMapEl = document.getElementById('summaryMap'); if (!summaryMapEl) return; summaryMapInstance = L.map('summaryMap', { scrollWheelZoom: true, dragging: true, zoomControl: true, center: [20, 0], zoom: 2 }); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '\u00a9 OpenStreetMap contributors', maxZoom: 19 }).addTo(summaryMapInstance); summaryLayerGroup = L.layerGroup().addTo(summaryMapInstance); // Parse and cache all row data once summaryRowData = []; const expandableRows = document.querySelectorAll('.expandable-row'); expandableRows.forEach(row => { const cells = row.querySelectorAll('td'); const hasError = row.getAttribute('data-has-error') === 'true'; const hasWarning = row.getAttribute('data-has-warning') === 'true'; const hasSugg = row.getAttribute('data-has-suggestion') === 'true'; let color = '#28a745'; if (hasError) color = '#dc3545'; else if (hasWarning) color = '#fd7e14'; else if (hasSugg) color = '#17a2b8'; const country = cells[CELL_INDEX.COUNTRY] ? cells[CELL_INDEX.COUNTRY].textContent.trim() : ''; const region = cells[CELL_INDEX.REGION] ? cells[CELL_INDEX.REGION].textContent.trim() : ''; const city = cells[CELL_INDEX.CITY] ? cells[CELL_INDEX.CITY].textContent.trim() : ''; const parts = []; if (city && region && country) { parts.push('' + escapeHtml(city) + '' + ' • ' + escapeHtml(region) + ' • ' + escapeHtml(country)); } else if (city && country) { parts.push('' + escapeHtml(city) + '' + ' • ' + escapeHtml(country)); } else if (region && country) { parts.push('' + escapeHtml(region) + '' + ' • ' + escapeHtml(country)); } else if (country) { parts.push('' + escapeHtml(country) + ''); } summaryRowData.push({ rowId: row.id, bbox: row.getAttribute('data-bounding-box') || '', h3cells: row.getAttribute('data-h3-cells') || '', color: color, popup: parts.join('
') }); }); setTimeout(() => { summaryMapInstance.invalidateSize(); switchMapMode('bbox'); }, 350); } catch (err) { console.error('initSummaryMap error:', err); } } ``` -------------------------------- ### Navigate Between Location Matches Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/scripts/templates/index.html Updates the selected match index and applies a 'selected' class for visual feedback. Scrolls the newly selected item into view. Handles wrapping around the list. ```javascript function navigateMatches(direction) { if (matchItems.length === 0) return; if (matchItems[selectedMatchIndex]) { matchItems[selectedMatchIndex].classList.remove('selected'); } selectedMatchIndex = (selectedMatchIndex + direction + matchItems.length) % matchItems.length; if (matchItems[selectedMatchIndex]) { matchItems[selectedMatchIndex].classList.add('selected'); matchItems[selectedMatchIndex].scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); } } ``` -------------------------------- ### Fetch Geolocation Matches from API Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/scripts/templates/index.html Asynchronously fetches geolocation matches from the API in batches. Handles potential errors and aggregates results. The maximum batch size is 1000. ```javascript async function fetchPlaceMatches(rows) { const MAX_BATCH_SIZE = 1000; let allResults = []; let batchCount = Math.ceil(rows.length / MAX_BATCH_SIZE); let lastError = null; for (let i = 0; i < batchCount; i++) { const batchRows = rows.slice(i * MAX_BATCH_SIZE, (i + 1) * MAX_BATCH_SIZE); try { const response = await fetch(API_ENDPOINT, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ rows: batchRows }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); allResults = allResults.concat(data.results); } catch (error) { console.error('Error fetching place matches:', error); lastError = error; // Optionally, decide whether to break or continue on error } } if (lastError && allResults.length === 0) { return null; // Indicate failure if no results and an error occurred } return { results: allResults }; } ``` -------------------------------- ### Add Page Button Function Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/scripts/templates/index.html Creates and appends a button for a specific page number to the pagination container. Highlights the button if it represents the current page. ```javascript /** * Creates and adds a page number button to pagination controls * @param {number} pageNum - Page number for the button * @returns {void} */ function addPageButton(pageNum) { const pageNumbersContainer = document.getElementById('pageNumbers'); const btn = document.createElement('button'); btn.className = 'pagination-btn' + (pageNum === currentPage ? ' active' : ''); btn.textContent = pageNum; btn.addEventListener('click', function() { currentPage = pageNum; updatePagination(); }); pageNumbersContainer.appendChild(btn); } ``` -------------------------------- ### Tune All Button Functionality Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/scripts/templates/index.html Processes all selected rows to apply tuning based on country, region, and city attributes. It updates the displayed cells and tracks updated and skipped rows. Ensure 'expandable-row' elements have 'data-tunable' and 'data-tuned-country/region/city' attributes. ```javascript document.getElementById('tuneAll').addEventListener('click', async function() { const expandableRows = document.querySelectorAll('.expandable-row'); let updatedCount = 0; let skippedCount = 0; expandableRows.forEach(dataRow => { const checkbox = dataRow.querySelector('.row-checkbox'); if (!checkbox || !checkbox.checked) { skippedCount++; return; // Only work on checked rows } const isTunable = dataRow.getAttribute('data-tunable') === 'true'; if (!isTunable) { skippedCount++; return; } const tunedCountry = dataRow.getAttribute('data-tuned-country'); const tunedRegion = dataRow.getAttribute('data-tuned-region'); const tunedCity = dataRow.getAttribute('data-tuned-city'); if (!tunedCountry && !tunedRegion && !tunedCity) { skippedCount++; return; } const cells = dataRow.querySelectorAll('td'); if (cells.length <= CELL_INDEX.STATUS) { skippedCount++; return; } if (!dataRow.hasAttribute('data-original-country')) { dataRow.setAttribute('data-original-country', cells[CELL_INDEX.COUNTRY].textContent); dataRow.setAttribute('data-original-region', cells[CELL_INDEX.REGION].textContent); dataRow.setAttribute('data-original-city', cells[CELL_INDEX.CITY].textContent); } if (tunedCountry) cells[CELL_INDEX.COUNTRY].textContent = tunedCountry; if (tunedRegion) cells[CELL_INDEX.REGION].textContent = tunedRegion; if (tunedCity) cells[CELL_INDEX.CITY].textContent = tunedCity; // Find the previous values row const previousValuesRow = dataRow.nextElementSibling; if (previousValuesRow && previousValuesRow.classList.contains('previous-values-row')) { previousValuesRow.classList.add('show'); const defaultCountry = previousValuesRow.querySelector('.default-country'); const defaultRegion = previousValuesRow.querySelector('.default-region'); const defaultCity = previousValuesRow.querySelector('.default-city'); if (defaultCountry) defaultCountry.textContent = dataRow.getAttribute('data-original-country'); if (defaultRegion) defaultRegion.textContent = dataRow.getAttribute('data-original-region'); if (defaultCity) defaultCity.textContent = dataRow.getAttribute('data-original-city'); } updatedCount++; }); // Refresh pagination to update displayed values updatePagination(); alert(`Successfully updated ${updatedCount} rows (${skippedCount} rows skipped - not tunable or no tuned values available)`); }); ``` -------------------------------- ### Set Report Mode Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/scripts/templates/index.html Sets the report mode between 'viewing' and 'tuning'. Tuning mode shows checkboxes and expands rows with issues, indicated by body and toggle class changes. ```javascript function setMode(mode) { const toggle = document.getElementById('modeToggle'); const viewingText = document.getElementById('viewingText'); const tuningText = document.getElementById('tuningText'); if (mode === 'tuning') { document.body.classList.add('tuning-mode'); toggle.classList.add('tuning'); viewingText.classList.remove('active'); tuningText.classList.add('active'); // Only expand rows with is ``` -------------------------------- ### Fastah Place Search API - Batch Search Source: https://context7.com/fastah/ip-geofeed-skills/llms.txt Performs batch geocoding for multiple geofeed entries using the Fastah API. Each entry requires at least a country code. Supports up to 1000 entries per request. ```python import requests import json API_ENDPOINT = 'https://mcp.fastah.ai/rest/geofeeds/place-search' def batch_search(entries): """ Batch geocode multiple geofeed entries. Each entry needs: countryCode, regionCode (optional), cityName (optional) """ rows = [] for i, entry in enumerate(entries): rows.append({ 'rowKey': f'row-{i}', 'countryCode': entry.get('countryCode', ''), 'regionCode': entry.get('regionCode', ''), 'cityName': entry.get('cityName', ''), 'searchMode': 'auto' }) response = requests.post( API_ENDPOINT, headers={'Content-Type': 'application/json'}, json={'rows': rows} ) return response.json() ``` -------------------------------- ### Select Location Match and Update Table Row Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/scripts/templates/index.html Selects a location match and updates the corresponding table row. It finds the data row preceding any detail rows and updates it with the provided match data. ```javascript function selectLocationMatch(match) { if (!currentTuningButton) { console.error('No tuning button reference found'); return; } const detailsRow = currentTuningButton.closest('tr'); // Find the data row - it's before the expand-details-rows let dataRow = detailsRow.previousElementSibling; while (dataRow && dataRow.classList.contains('expand-details-row')) { dataRow = dataRow.previousElementSibling; } if (!dataRow || !dataRow.classList.contains('expandable-row')) { console.error('Could not find data row'); return; } updateRowWithMatchData(dataRow, match); closeModal(); } ``` -------------------------------- ### Additional Parsing Requirements Source: https://github.com/fastah/ip-geofeed-skills/blob/main/skills/geofeed-tuner/references/rfc8805.txt Specifies rules for handling IP addresses, prefixes, and data parsing errors. ```APIDOC ## Additional Parsing Requirements ### Description This section outlines the mandatory and recommended parsing behaviors for IP Geofeed data consumers. ### Requirements - **Discard Invalid Entries**: Feed entries without a valid IP address or prefix MUST be discarded. - **IPv6 Prefix Handling**: Publishers SHOULD follow [RFC5952] for IPv6 prefixes, but consumers MUST accept all valid string representations. - **Duplicate Entries**: Duplicate IP address or prefix entries MUST be considered an error. Consumer implementations SHOULD log these entries. - **Nested Prefixes**: Multiple entries constituting nested prefixes are permitted. Consumers SHOULD consider the entry with the longest matching prefix (most specific) as the best match. - **Invalid Optional Fields**: Feed entries with non-empty optional fields that fail to parse SHOULD be discarded and logged. - **Ignoring Unknown Fields**: Parsers MUST ignore any fields beyond those they expect to maintain compatibility with future extensions. Data from expected and successfully parsed fields MUST still be considered valid. ```