diff --git a/htmx/index.htm b/htmx/index.htm index 77a484c..ed55bbc 100644 --- a/htmx/index.htm +++ b/htmx/index.htm @@ -29,25 +29,39 @@ <body hx-ext="pronom-api-params" hx-vals='{"baseURLLocal": "http://0.0.0.0:24001", "baseURL": "https://itn.0.orcfax.io"}'> - <h1>ITN diagnostics</h1> - <div> - <h2>License holders</h2> - <div> - <div> - <div - hx-get="{baseURL}/api/participants" - hx-trigger="load" - hx-swap="innerHTML" - /> - </div> + <section id="license-holders"> + <h1>ITN diagnostics</h1> + <div> + <h2>License holders</h2> + <div> + <div> + <div + hx-get="{baseURL}/api/participants" + hx-trigger="load" + hx-swap="innerHTML" + /> + </div> + </section> <hr> - <h2>Active collector counts</h2> - <div - hx-get="{baseURL}/api/online_collectors" - hx-trigger="load" - hx-swap="innerHTML" - /> - </div> + <section id="collector-counts"> + <h2>Active collector counts</h2> + <div + hx-get="{baseURL}/api/online_collectors" + hx-trigger="load" + hx-swap="innerHTML" + /> + </div> + </section> + <hr> + <section id="locations"> + <h2>Collector locations</h2> + <div + hx-get="{baseURL}/locations" + hx-trigger="load" + hx-swap="innerHTML" + /> + </div> + </section> <hr> <div class="footer__bottom text--center"> <div class="footer__copyright">ITN | Diagnostics</div> diff --git a/src/itn_api/api.py b/src/itn_api/api.py index 9e80fd6..047709a 100644 --- a/src/itn_api/api.py +++ b/src/itn_api/api.py @@ -201,6 +201,17 @@ async def get_itn_aliases_and_staking_csv( return reports.get_all_license_holders_csv(app, min_stake, sort) +@app.get("/geo", tags=[TAG_STATISTICS]) +async def get_locations(): + """Return countries participating in the ITN.""" + return await reports.get_locations(app) + + +# HTMX ################################################################# +# HTMX ################################################################# +# HTMX ################################################################# + + @app.get("/participants", tags=[TAG_HTMX], response_class=HTMLResponse) async def get_itn_participants() -> str: """Return ITN aliases and licenses.""" @@ -223,6 +234,13 @@ async def get_online_collectors() -> str: return htmx.strip() +@app.get("/locations", tags=[TAG_HTMX], response_class=HTMLResponse) +async def get_locations_hx(): + """Return countries participating in the ITN.""" + locations = await reports.get_locations(app) + return htm_helpers.locations_table(locations) + + def main(): """Primary entry point for this script.""" diff --git a/src/itn_api/htm_helpers.py b/src/itn_api/htm_helpers.py index f2122e2..feec128 100644 --- a/src/itn_api/htm_helpers.py +++ b/src/itn_api/htm_helpers.py @@ -81,3 +81,38 @@ def participants_count_table(participants_count_total): rows = f"{rows}{row}\n" return f"{head}\n{rows}</table>\n" + + +def locations_table(locations): + """Create a table for participant locations.""" + + logging.info("formatting participants table") + + if not locations: + return "no locations available" + + head = """ +<table> + <tr> + <th>Region</th> + <th>Country</th> + </tr> + """.strip() + + seen = [] + rows = "" + for locale in locations: + region = locale["region"] + country = locale["country"] + if (region, country) in seen: + continue + row = f""" +<tr> + <td>{region}</td> + <td nowrap> {country} </td> +</tr> + """.strip() + seen.append((region, country)) + rows = f"{rows}{row}\n" + + return f"{head}\n{rows}</table>\n" diff --git a/src/itn_api/reports.py b/src/itn_api/reports.py index 8a679ce..8997df9 100644 --- a/src/itn_api/reports.py +++ b/src/itn_api/reports.py @@ -2,11 +2,13 @@ # pylint: disable=R0917,R0913,R0914 +import json import logging from collections import Counter from dataclasses import dataclass from typing import List, Tuple +import apsw import humanize from fastapi import FastAPI @@ -303,3 +305,32 @@ async def get_date_ranges(app: FastAPI): "earliest_date": dates[0], "latest_date": dates[1], } + + +async def get_locations(app: FastAPI) -> list: + """Return locations from the database.""" + try: + unique_raw_data = app.state.connection.execute( + "select min(node_id), raw_data from data_points group by node_id;" + ) + except apsw.SQLError: + return "zero collectors online" + res = list(unique_raw_data) + countries = [] + for item in res: + node = item[0] + message = json.loads(item[1]) + try: + loc = message["message"]["identity"]["location"] + countries.append( + ( + { + "geo": loc.get("loc"), + "region": loc.get("region"), + "country": loc.get("country"), + } + ) + ) + except KeyError as err: + logger.error("node: '%s' not reporting location (%s)", node, err) + return countries