Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/GustavoNightmare/InformacionMuseo/llms.txt

Use this file to discover all available pages before exploring further.

BioScan Museo records a ScanEvent every time a visitor interacts with a species — whether by scanning a physical QR code, visiting the exhibit URL directly in a browser, or opening the exhibit from the admin panel. These events are the source of truth for all metrics. The admin dashboard at /admin/metricas visualizes scan volume over time, origin breakdowns, and per-species rankings, all filterable by date range, species, and origin.

Data models

ScanEvent

ScanEvent is written on every exhibit access, including anonymous visits.
FieldTypeDescription
idIntegerAuto-increment primary key.
species_idString(64)Foreign key to the scanned species. Indexed.
user_idIntegerForeign key to the authenticated user. NULL for anonymous scans. Indexed.
qr_idString(64)The qr_id slug of the species at the time of the scan.
originString(20)How the scan was initiated: qr, web, or manual. Indexed.
scanned_atDateTimeUTC timestamp of the scan event. Indexed.
The following database indexes are created on scan_event to keep filter queries fast:
Index nameColumn
idx_scan_speciesspecies_id
idx_scan_useruser_id
idx_scan_timescanned_at
idx_scan_originorigin

Visit

Visit tracks whether an authenticated user has visited a specific species at least once. It is used for the “tour completion” celebration and for personalizing the RAG chatbot context.
FieldTypeDescription
idIntegerAuto-increment primary key.
user_idIntegerForeign key to the user.
species_idString(64)Foreign key to the species.
visited_atDateTimeUTC timestamp of the first visit.
When an authenticated user’s visit count equals the total number of species in the catalog, a celebration screen is shown on the exhibit page.
ScanEvent is written for all visitors (authenticated and anonymous). Visit is only written for authenticated users, and only once per user+species pair.

Metrics dashboard

The admin dashboard is available at GET /admin/metricas. It presents four summary cards, a ranked species table, and two charts.

Summary cards

CardFieldDescription
Total scanssummary.total_scansTotal ScanEvent rows matching the current filters.
Unique userssummary.unique_usersCount of distinct authenticated user_id values in the filtered range.
Species scannedsummary.scanned_species_countCount of distinct species_id values in the filtered range.
Most scannedsummary.most_scanned_speciesSpecies with the highest scan count in the filtered range (name, qr_id, and total).

Species ranking table

Each row represents one species and shows:
ColumnSource field
Species namenombre_comun
Scientific namenombre_cientifico
QR IDqr_id
Total scanstotal_scans
Unique usersunique_users
Last scanlast_scan (formatted YYYY-MM-DD HH:MM)
Rows are ordered by total_scans descending.

Charts

  • Daily scan counts — a time-series chart with one data point per calendar day in the selected date range. Days with zero scans are filled in automatically so the series is always continuous.
  • Origin breakdown — a breakdown chart showing counts for each of the three origins: QR, Web, and Manual.

Dashboard filters

All filters are passed as query parameters to both the HTML dashboard and the JSON API.
ParameterDescriptionDefault
startStart date (YYYY-MM-DD)30 days before today
endEnd date (YYYY-MM-DD)Today
species_idFilter by a single species ID(all species)
originFilter by scan origin: qr, web, or manual(all origins)
If start is after end, the two values are swapped automatically. Invalid dates fall back to the default 30-day window.

JSON API

The same data available in the dashboard is also exposed as a JSON endpoint:
GET /api/admin/metricas
The endpoint accepts the same query parameters as the dashboard and returns a single JSON object.

Response structure

The table below documents every key in the response from build_scan_metrics_context().
KeyTypeDescription
filters.startstringEffective start date used for the query (YYYY-MM-DD).
filters.endstringEffective end date used for the query (YYYY-MM-DD).
filters.species_idstringActive species filter, or empty string.
filters.originstringActive origin filter, or empty string.
filters.species_optionsarrayAll species available for the filter dropdown (id, qr_id, nombre_comun, nombre_cientifico).
summary.total_scansintegerTotal scan events in the filtered range.
summary.unique_usersintegerDistinct authenticated users in the filtered range.
summary.scanned_species_countintegerDistinct species scanned in the filtered range.
summary.most_scanned_speciesobject | nullTop species object (id, qr_id, nombre_comun, nombre_cientifico, total_scans), or null if no scans.
rankingarrayOrdered list of species objects with species_id, qr_id, nombre_comun, nombre_cientifico, total_scans, unique_users, last_scan.
chart_data.daily.datesarray[string]Full list of dates in the range (YYYY-MM-DD), one entry per day.
chart_data.daily.countsarray[integer]Scan count per day, aligned to dates. Zero-filled for days without scans.
chart_data.origin.labelsarray[string]Always ["QR", "Web", "Manual"].
chart_data.origin.countsarray[integer]Scan counts for each origin label, in the same order.

Example response

{
  "filters": {
    "start": "2025-05-01",
    "end": "2025-05-31",
    "species_id": "",
    "origin": "",
    "species_options": [
      {
        "id": "condor-001",
        "qr_id": "condor-001",
        "nombre_comun": "Cóndor Andino",
        "nombre_cientifico": "Vultur gryphus"
      }
    ]
  },
  "summary": {
    "total_scans": 312,
    "unique_users": 47,
    "scanned_species_count": 18,
    "most_scanned_species": {
      "id": "condor-001",
      "qr_id": "condor-001",
      "nombre_comun": "Cóndor Andino",
      "nombre_cientifico": "Vultur gryphus",
      "total_scans": 54
    }
  },
  "ranking": [
    {
      "species_id": "condor-001",
      "qr_id": "condor-001",
      "nombre_comun": "Cóndor Andino",
      "nombre_cientifico": "Vultur gryphus",
      "total_scans": 54,
      "unique_users": 22,
      "last_scan": "2025-05-31 14:07"
    }
  ],
  "chart_data": {
    "daily": {
      "dates": ["2025-05-01", "2025-05-02"],
      "counts": [8, 11]
    },
    "origin": {
      "labels": ["QR", "Web", "Manual"],
      "counts": [240, 58, 14]
    }
  }
}

Seeding demo data

To populate the database with realistic-looking scan events for testing the dashboard, run:
flask seed-demo-data
This command generates approximately 1,500 ScanEvent rows distributed across the last 90 days. It applies a realistic daily volume pattern — higher counts on weekends and typical museum visiting hours — and a popularity curve where the first species in the catalog receive proportionally more scans than later ones. Origins are weighted toward qr (the most common physical use case), followed by web, then manual.
seed-demo-data requires at least one species (run flask seed first) and at least one user (run flask create-admin and flask create-user first). The command appends to existing data, so running it multiple times will accumulate additional events.
After seeding, open the metrics dashboard and set the date range to the last 90 days to see the full demo dataset. You can then test filtering by species or origin to verify the JSON API response shape before integrating a custom frontend.

Build docs developers (and LLMs) love