From 981c49e7fa3a8070de9fb513b0a69e7592d0474c Mon Sep 17 00:00:00 2001 From: Louis King Date: Wed, 6 May 2026 15:11:58 +0100 Subject: [PATCH] feat: replace MeshCore card with Members widget and add footer branding --- docs/i18n.md | 1 - .../20260506-1330-members-widget/plan.md | 410 ++++++++++++++++++ .../20260506-1330-members-widget/tasks.md | 50 +++ src/meshcore_hub/api/routes/dashboard.py | 37 +- src/meshcore_hub/common/schemas/messages.py | 4 + src/meshcore_hub/web/static/css/app.css | 18 + .../web/static/js/spa/pages/home.js | 52 ++- src/meshcore_hub/web/static/locales/en.json | 1 - src/meshcore_hub/web/static/locales/nl.json | 1 - src/meshcore_hub/web/templates/spa.html | 24 +- 10 files changed, 568 insertions(+), 30 deletions(-) create mode 100644 docs/plans/20260506-1330-members-widget/plan.md create mode 100644 docs/plans/20260506-1330-members-widget/tasks.md diff --git a/docs/i18n.md b/docs/i18n.md index 6a7f723..7e88294 100644 --- a/docs/i18n.md +++ b/docs/i18n.md @@ -244,7 +244,6 @@ Homepage-specific content: | `all_discovered_nodes` | All discovered nodes | Stat description | | `network_info` | Network Info | Network info card title | | `network_activity` | Network Activity | Activity chart title | -| `meshcore_attribution` | Our local off-grid mesh network is made possible by | Attribution text before MeshCore logo | | `frequency` | Frequency | Radio frequency label | | `bandwidth` | Bandwidth | Radio bandwidth label | | `spreading_factor` | Spreading Factor | LoRa spreading factor label | diff --git a/docs/plans/20260506-1330-members-widget/plan.md b/docs/plans/20260506-1330-members-widget/plan.md new file mode 100644 index 0000000..5c962c3 --- /dev/null +++ b/docs/plans/20260506-1330-members-widget/plan.md @@ -0,0 +1,410 @@ +# Plan: Members Widget on Homepage + MeshCore Logo in Footer + +**Date**: 2026-05-06 +**Status**: Draft + +--- + +## Summary + +Two changes to the homepage and footer: + +1. **Members Widget** — Replace the MeshCore logo/links card on the homepage with a new panel containing two stat tiles showing **operator count** and **member count**, derived from role data in the `UserProfile` table. + +2. **MeshCore Logo in Footer** — Move the existing MeshCore logo and project links from the homepage card into the footer bar. The footer becomes a two-column layout: MeshCore branding left-aligned, existing IPNet/MeshCore Hub info right-aligned. On mobile, MeshCore content stacks above the Hub info. + +--- + +## Current State + +### Homepage — MeshCore Attribution Card + +At `home.js:222–241`, a full-width card in the second row of the grid shows: + +``` +┌──────────────────────────────────────┐ +│ │ +│ "Off-Grid, Open-Source │ +│ Encrypted Messaging" │ +│ │ +│ [MeshCore Logo] │ +│ │ +│ [Website btn] [GitHub btn] │ +│ │ +└──────────────────────────────────────┘ +``` + +This card occupies one slot in the `grid-cols-1 md:grid-cols-2 lg:grid-cols-3` bottom row alongside the Network Info card and Activity Chart card. + +### Homepage — Stats Panel + +At `home.js:126–151`, the right column of the hero area shows three stat cards: Total Nodes, Advertisements (7d), Messages (7d). There are **no member/operator counts**. + +### Dashboard Stats API + +`GET /api/v1/dashboard/stats` (`dashboard.py:24–180`) returns `DashboardStats` (`messages.py:244–269`) with node, message, and advertisement counts. **No member/profile counts** are included. + +### Footer + +`spa.html:112–140` — A `footer-center` footer showing: +- Network name + city/country +- Contact links (email, discord, github, youtube) +- "Powered by MeshCore Hub {version}" + +All content is center-aligned. No MeshCore branding present. + +### UserProfile Model + +`user_profile.py:14–74` — `roles` field is a `Text` column containing comma-separated role strings (e.g. `"operator,member"`). The `role_list` property parses these into a `list[str]`. + +### Members Page — Role Grouping Logic + +`members.js:69–92` — Fetches `/api/v1/user/profiles?limit=500`, then groups: +- **Operators**: profiles where `roles` includes the operator role name +- **Members**: profiles where `roles` includes the member role name AND excludes operators + +### Key Files + +| File | Role | +|------|------| +| `src/meshcore_hub/web/static/js/spa/pages/home.js` | Homepage layout; MeshCore card at lines 222–241; stats panel at lines 126–151 | +| `src/meshcore_hub/web/static/js/spa/pages/members.js` | Members page; role grouping logic at lines 69–92 | +| `src/meshcore_hub/web/templates/spa.html` | Footer at lines 112–140 | +| `src/meshcore_hub/web/static/css/app.css` | Color palette, panel glow, footer styles | +| `src/meshcore_hub/web/static/js/spa/components.js` | `renderStatCard()`, `pageColors`, `t()` | +| `src/meshcore_hub/web/static/js/spa/icons.js` | SVG icon functions | +| `src/meshcore_hub/api/routes/dashboard.py` | Dashboard stats endpoint at line 24 | +| `src/meshcore_hub/common/schemas/messages.py` | `DashboardStats` schema at line 244 | +| `src/meshcore_hub/common/models/user_profile.py` | `UserProfile` model with `roles` field | +| `src/meshcore_hub/web/static/locales/en.json` | i18n strings | + +--- + +## Target State + +### 1. Homepage — Replace MeshCore Card with Members Widget + +The MeshCore attribution card (currently `home.js:222–241`) is replaced with a **Members panel** containing two stat tiles: + +``` +┌──────────────────────────────────────┐ +│ 👥 Members │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ [icon] │ │ [icon] │ │ +│ │ Operators │ │ Members │ │ +│ │ 3 │ │ 12 │ │ +│ └──────────────┘ └──────────────┘ │ +│ │ +└──────────────────────────────────────┘ +``` + +Each tile uses the existing `renderStatCard()` component (or a simplified variant) with the `--color-members` accent. The tiles show: +- **Operators**: count of `UserProfile` rows where `roles` contains the operator role +- **Members**: count of `UserProfile` rows where `roles` contains the member role AND excludes operators + +When OIDC is disabled (no `user_profiles` table populated), the panel shows "0" for both counts (graceful degradation — no error). + +### 2. Footer — Two-Column Layout with MeshCore Branding + +The footer changes from a centered single-column to a **horizontal two-column layout**: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ │ +│ [MeshCore Logo] Website · GitHub IPNet Network │ +│ City, Country │ +│ email | discord | github │ +│ Powered by MeshCore Hub │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Desktop (`lg+`)**: +- **Left side**: Small MeshCore logo + plain text links ("Website" and "GitHub") +- **Right side**: Existing footer content (network name, contacts, powered-by) + +**Mobile (< `lg`)**: +- MeshCore content stacks on top +- Hub/network content below +- Both center-aligned (matching current mobile behaviour) + +**MeshCore logo changes**: +- Smaller than the homepage version: `h-5` (20px) instead of `h-8` +- Links are plain `` text links with `link link-hover` class — not `btn btn-outline btn-sm` buttons +- Separator between links is a middot (`·`) + +--- + +## Implementation Plan + +### Step 1: Add Member Counts to Dashboard Stats API + +**Files**: `dashboard.py`, `messages.py` + +#### 1a. Extend `DashboardStats` schema + +Add two optional fields to `DashboardStats` in `messages.py:244`: + +```python +total_operators: int = Field(default=0, description="Number of operator-role users") +total_members: int = Field(default=0, description="Number of member-role users (excluding operators)") +``` + +Both default to `0` so the response is backward-compatible when OIDC is disabled or the table is empty. + +#### 1b. Add count queries to `get_stats()` + +In `dashboard.py:24`, add queries after the existing stats: + +```python +from meshcore_hub.common.models import UserProfile +from meshcore_hub.common.config import Settings + +settings = Settings() +operator_role = settings.oidc_role_operator +member_role = settings.oidc_role_member + +# Count operators (roles contains operator role) +total_operators = ( + session.execute( + select(func.count()) + .select_from(UserProfile) + .where(UserProfile.roles.contains(operator_role)) + ).scalar() + or 0 +) + +# Count members (roles contains member role AND does NOT contain operator role) +total_members = ( + session.execute( + select(func.count()) + .select_from(UserProfile) + .where( + UserProfile.roles.contains(member_role), + ~UserProfile.roles.contains(operator_role), + ) + ).scalar() + or 0 +) +``` + +Include `total_operators` and `total_members` in the returned `DashboardStats` instance. + +**Note**: The `roles` column is comma-separated text. Using `contains()` is a SQLite `LIKE` substring match — the same pattern already used in `metrics.py:309` for `UserProfile.roles.contains(role)`. This is acceptable for the small dataset sizes expected. + +### Step 2: Replace MeshCore Card with Members Panel in Homepage + +**File**: `home.js` + +#### 2a. Add new `renderMembersPanel()` function + +Create a new function that renders two stat tiles for operators and members: + +```javascript +function renderMembersPanel({ features, stats }) { + if (features.members === false) return nothing; + return html` +
+
+

+ ${iconMembers('h-6 w-6')} + ${t('entities.members')} +

+
+ ${renderStatCard({ + icon: iconAntenna('h-6 w-6'), + color: pageColors.members, + title: t('members_page.operators'), + value: stats.total_operators ?? 0, + })} + ${renderStatCard({ + icon: iconUsers('h-6 w-6'), + color: pageColors.members, + title: t('members_page.members'), + value: stats.total_members ?? 0, + })} +
+
+
`; +} +``` + +#### 2b. Replace the MeshCore attribution card and update grid condition + +In the `render()` function's template (lines 210–244): + +1. Replace the inline MeshCore card HTML (lines 223–241) with: + ```javascript + ${renderMembersPanel({ features, stats })} + ``` + +2. Update the grid class at line 210. Currently the `lg:grid-cols-3` condition only checks `showActivityChart`. Since the Members panel is now feature-gated (was always-present MeshCore card), the 3-column layout should only apply when BOTH the Members panel AND the Activity Chart are visible: + ```javascript + const showMembersPanel = features.members !== false; +
+ ``` + +#### 2c. Update imports + +- **Add**: `iconAntenna` and `iconUsers` to the import from `icons.js` +- **Remove**: `iconGlobe` and `iconGithub` from the import (only used by the now-removed MeshCore card) + +### Step 3: Move MeshCore Branding to Footer + +**File**: `spa.html` + +#### 3a. Restructure footer markup + +Replace the current footer (lines 112–140) with a two-column layout: + +```html +
+ +
+
+ + + + Off-Grid, Open-Source Encrypted Messaging +
+ +
+ + +
+

+ {{ network_name }} + {% if network_city and network_country %} + — {{ network_city }}, {{ network_country }} + {% endif %} +

+

+ {% if network_contact_email %} + {{ network_contact_email }} + {% endif %} + {% if network_contact_email and network_contact_discord %} | {% endif %} + {% if network_contact_discord %} + {{ t('links.discord') }} + {% endif %} + {% if (network_contact_email or network_contact_discord) and network_contact_github %} | {% endif %} + {% if network_contact_github %} + {{ t('links.github') }} + {% endif %} + {% if (network_contact_email or network_contact_discord or network_contact_github) and network_contact_youtube %} | {% endif %} + {% if network_contact_youtube %} + {{ t('links.youtube') }} + {% endif %} +

+

{{ t('footer.powered_by') }} MeshCore Hub {{ version }}

+
+
+``` + +**Key changes**: +- `footer-center` → `footer` (remove center alignment to allow two columns) +- DaisyUI `footer` class provides `flex flex-wrap` which enables the two-column layout +- `order-2 lg:order-1` / `order-1 lg:order-2` ensures MeshCore stacks on top on mobile +- Logo is `h-5` (smaller than the `h-8` from the homepage card) +- Links are plain `` — no buttons + +#### 3b. Add footer CSS (if needed) + +The DaisyUI `footer` component already provides flex-wrap layout. Minimal custom CSS should be needed. If the default spacing needs adjustment, add to `app.css`: + +```css +/* Footer two-column layout */ +footer.footer { + justify-content: space-between; + align-items: center; +} +``` + +### Step 4: i18n Updates + +**File**: `en.json` (and check `nl.json` for the same key) + +No new keys required — reuse existing: +- `entities.members` = "Members" +- `members_page.operators` = "Operators" +- `members_page.members` = "Members" +- `links.website` = "Website" +- `links.github` = "GitHub" +- `footer.powered_by` = "Powered by" + +Remove orphaned key (no longer referenced after MeshCore card removal): +- `home.meshcore_attribution` = "Our local off-grid mesh network is made possible by" + +### Step 5: Update `docs/i18n.md` + +Remove the `home.meshcore_attribution` key from the i18n reference if documented. + +--- + +## Files Changed — Summary + +| File | Change | +|------|--------| +| `src/meshcore_hub/common/schemas/messages.py` | Add `total_operators` and `total_members` fields to `DashboardStats` | +| `src/meshcore_hub/api/routes/dashboard.py` | Add UserProfile count queries for operators and members; import `UserProfile` model and `Settings` | +| `src/meshcore_hub/web/static/js/spa/pages/home.js` | Replace MeshCore card with `renderMembersPanel()`; add `iconAntenna`/`iconUsers` imports; remove `iconGlobe`/`iconGithub` imports; update grid `lg:grid-cols-3` condition | +| `src/meshcore_hub/web/templates/spa.html` | Restructure footer into two-column layout with MeshCore branding left, network/hub info right | +| `src/meshcore_hub/web/static/css/app.css` | Add footer layout styles if DaisyUI defaults insufficient | +| `src/meshcore_hub/web/static/locales/en.json` | Remove orphaned `home.meshcore_attribution` key | + +--- + +## Testing + +### Visual Verification + +1. Start the app with OIDC enabled and seeded user profiles +2. Navigate to `/` (home page): + - Verify the MeshCore card is gone + - Verify the Members panel shows with operator and member counts + - Verify counts match the `/members` page grouping +3. Scroll to footer: + - Verify MeshCore logo (small) and text links appear left-aligned + - Verify network info and "Powered by" appear right-aligned + - Resize to mobile width — verify MeshCore stacks above network info, both center-aligned +4. Test with OIDC disabled — verify Members panel shows "0" for both counts (no error) + +### API Verification + +```bash +curl -s http://localhost:8000/api/v1/dashboard/stats -H "Authorization: Bearer $API_READ_KEY" | jq '.total_operators, .total_members' +``` + +### Automated Tests + +```bash +# API tests (dashboard stats changes) +pytest tests/test_api/ -v + +# Web tests (homepage rendering) +pytest tests/test_web/ -v + +# Quality checks +pre-commit run --all-files +``` + +### Cross-Theme Testing + +- Toggle between dark and light themes +- Verify footer MeshCore logo inverts correctly in light mode (existing `theme-logo--invert-light` class) +- Verify Members panel tiles use `--color-members` accent in both themes + +--- + +## Risks & Mitigations + +| Risk | Mitigation | +|------|------------| +| `UserProfile.roles.contains()` is a substring match — role "member" could match "nonmember" | In practice, roles are standard OIDC values (`admin`, `operator`, `member`). The comma-separated format (`"operator,member"`) prevents false positives since `contains("member")` matches the whole token. For extra safety, could use `LIKE '%,member,%' OR roles LIKE 'member,%' OR roles LIKE '%,member' OR roles = 'member'`, but this is over-engineering for the current data model. | +| Footer two-column layout may not look balanced with minimal network info | The `justify-between` flex layout with center-aligned fallback on mobile adapts well. If the left side is too sparse, the tagline text fills the space. | +| Dashboard stats endpoint re-reads Settings for role names | `Settings` is a Pydantic BaseSettings class — instantiation is cheap and reads from env vars. Alternatively, pass role names via dependency injection from the app lifespan. For now, direct instantiation is acceptable. | +| Removing the MeshCore card reduces attribution visibility | Moving it to the footer (present on every page, not just home) increases overall visibility. The footer is persistent across all routes. | diff --git a/docs/plans/20260506-1330-members-widget/tasks.md b/docs/plans/20260506-1330-members-widget/tasks.md new file mode 100644 index 0000000..901c2a4 --- /dev/null +++ b/docs/plans/20260506-1330-members-widget/tasks.md @@ -0,0 +1,50 @@ +# Task List: Members Widget on Homepage + MeshCore Logo in Footer + +**Plan**: [plan.md](./plan.md) + +--- + +## Tasks + +### 1. Extend `DashboardStats` schema + +- [ ] Add `total_operators: int = Field(default=0, ...)` and `total_members: int = Field(default=0, ...)` to `DashboardStats` in `src/meshcore_hub/common/schemas/messages.py:244` + +### 2. Add member count queries to dashboard endpoint + +- [ ] In `src/meshcore_hub/api/routes/dashboard.py`, import `UserProfile` from `meshcore_hub.common.models` and `Settings` from `meshcore_hub.common.config` +- [ ] In `get_stats()`, add two count queries: operators (`roles.contains(operator_role)`) and members (`roles.contains(member_role) AND ~roles.contains(operator_role)`) +- [ ] Pass `total_operators` and `total_members` to the `DashboardStats()` return at line 203 + +### 3. Replace MeshCore card with Members panel in `home.js` + +- [ ] Add `showMembersPanel = features.members !== false` variable (after `showActivityChart` at line 188) +- [ ] Create `renderMembersPanel({ features, stats })` function with two `renderStatCard()` tiles (operators/members) +- [ ] Replace the inline MeshCore card HTML (lines 223–241) with `${renderMembersPanel({ features, stats })}` +- [ ] Update grid class (line 210): change `${showActivityChart ? 'lg:grid-cols-3' : ''}` to `${showMembersPanel && showActivityChart ? 'lg:grid-cols-3' : ''}` +- [ ] Update imports: add `iconAntenna`, `iconUsers`; remove `iconGlobe`, `iconGithub` + +### 4. Restructure footer in `spa.html` + +- [ ] Replace `footer footer-center` (line 113) with two-column layout: left side = MeshCore branding (logo `h-5` + text links), right side = existing network/hub info +- [ ] Use `order-2 lg:order-1` / `order-1 lg:order-2` for mobile stacking (MeshCore on top) + +### 5. Add footer CSS + +- [ ] Add `footer.footer { justify-content: space-between; align-items: center; }` to `src/meshcore_hub/web/static/css/app.css` + +### 6. Remove orphaned i18n key + +- [ ] Remove `home.meshcore_attribution` from `src/meshcore_hub/web/static/locales/en.json` +- [ ] Remove `home.meshcore_attribution` from `src/meshcore_hub/web/static/locales/nl.json` + +### 7. Update i18n docs + +- [ ] Remove `meshcore_attribution` entry from `docs/i18n.md:247` + +### 8. Test + +- [ ] Run `pytest tests/test_api/ -v` (dashboard stats changes) +- [ ] Run `pytest tests/test_web/ -v` (homepage rendering) +- [ ] Run `pre-commit run --all-files` +- [ ] Visual: verify Members panel renders with correct counts, footer shows two-column layout, mobile stacks correctly, OIDC-disabled shows zeros diff --git a/src/meshcore_hub/api/routes/dashboard.py b/src/meshcore_hub/api/routes/dashboard.py index 8875a70..a21e90a 100644 --- a/src/meshcore_hub/api/routes/dashboard.py +++ b/src/meshcore_hub/api/routes/dashboard.py @@ -7,7 +7,13 @@ from sqlalchemy import func, select from meshcore_hub.api.auth import RequireRead from meshcore_hub.api.dependencies import DbSession -from meshcore_hub.common.models import Advertisement, Message, Node, NodeTag +from meshcore_hub.common.models import ( + Advertisement, + Message, + Node, + NodeTag, + UserProfile, +) from meshcore_hub.common.schemas.messages import ( ChannelMessage, DailyActivity, @@ -200,6 +206,33 @@ async def get_stats( for m in channel_msgs ] + from meshcore_hub.common.config import get_web_settings + + web_settings = get_web_settings() + operator_role = web_settings.oidc_role_operator + member_role = web_settings.oidc_role_member + + total_operators = ( + session.execute( + select(func.count()) + .select_from(UserProfile) + .where(UserProfile.roles.contains(operator_role)) + ).scalar() + or 0 + ) + + total_members = ( + session.execute( + select(func.count()) + .select_from(UserProfile) + .where( + UserProfile.roles.contains(member_role), + ~UserProfile.roles.contains(operator_role), + ) + ).scalar() + or 0 + ) + return DashboardStats( total_nodes=total_nodes, active_nodes=active_nodes, @@ -212,6 +245,8 @@ async def get_stats( recent_advertisements=recent_advertisements, channel_message_counts=channel_message_counts, channel_messages=channel_messages, + total_operators=total_operators, + total_members=total_members, ) diff --git a/src/meshcore_hub/common/schemas/messages.py b/src/meshcore_hub/common/schemas/messages.py index c551d19..10dc2c0 100644 --- a/src/meshcore_hub/common/schemas/messages.py +++ b/src/meshcore_hub/common/schemas/messages.py @@ -267,6 +267,10 @@ class DashboardStats(BaseModel): default_factory=dict, description="Recent messages per channel (up to 5 each)", ) + total_operators: int = Field(default=0, description="Number of operator-role users") + total_members: int = Field( + default=0, description="Number of member-role users (excluding operators)" + ) class DailyActivityPoint(BaseModel): diff --git a/src/meshcore_hub/web/static/css/app.css b/src/meshcore_hub/web/static/css/app.css index 215f537..57cbe71 100644 --- a/src/meshcore_hub/web/static/css/app.css +++ b/src/meshcore_hub/web/static/css/app.css @@ -404,3 +404,21 @@ font-size: 0.9375rem; } } + +/* ========================================================================== + Footer Two-Column Layout + ========================================================================== */ + +footer.footer { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + gap: 1rem; +} + +@media (min-width: 1024px) { + footer.footer { + gap: 3rem; + } +} diff --git a/src/meshcore_hub/web/static/js/spa/pages/home.js b/src/meshcore_hub/web/static/js/spa/pages/home.js index 7c1bdc7..0a1ad12 100644 --- a/src/meshcore_hub/web/static/js/spa/pages/home.js +++ b/src/meshcore_hub/web/static/js/spa/pages/home.js @@ -5,7 +5,7 @@ import { } from '../components.js'; import { iconDashboard, iconNodes, iconAdvertisements, iconMessages, iconMembers, iconMap, - iconPage, iconInfo, iconChart, iconGlobe, iconGithub, + iconPage, iconInfo, iconChart, iconAntenna, iconUsers, iconSettings, iconFrequency, iconBandwidth, iconSpreadingFactor, iconCodingRate, iconTxPower, } from '../icons.js'; @@ -166,6 +166,33 @@ function renderActivityChartCard({ showAdvertSeries, showMessageSeries }) {
`; } +function renderMembersPanel({ features, stats }) { + if (features.members === false) return nothing; + return html` +
+
+

+ ${iconMembers('h-6 w-6')} + ${t('entities.members')} +

+
+ ${renderStatCard({ + icon: iconAntenna('h-6 w-6'), + color: pageColors.members, + title: t('members_page.operators'), + value: stats.total_operators ?? 0, + })} + ${renderStatCard({ + icon: iconUsers('h-6 w-6'), + color: pageColors.members, + title: t('members_page.members'), + value: stats.total_members ?? 0, + })} +
+
+
`; +} + export async function render(container, params, router) { try { const config = getConfig(); @@ -186,6 +213,7 @@ export async function render(container, params, router) { const showAdvertSeries = features.advertisements !== false; const showMessageSeries = features.messages !== false; const showActivityChart = showAdvertSeries || showMessageSeries; + const showMembersPanel = features.members !== false; const heroSection = renderHeroSection({ networkName, logoUrl, logoInvertLight, @@ -207,7 +235,7 @@ export async function render(container, params, router) { ${showStats ? statsPanel : nothing} -
+`, container); diff --git a/src/meshcore_hub/web/static/locales/en.json b/src/meshcore_hub/web/static/locales/en.json index eb4543d..2331c64 100644 --- a/src/meshcore_hub/web/static/locales/en.json +++ b/src/meshcore_hub/web/static/locales/en.json @@ -141,7 +141,6 @@ "all_discovered_nodes": "All discovered nodes", "network_info": "Network Info", "network_activity": "Network Activity", - "meshcore_attribution": "Our local off-grid mesh network is made possible by", "frequency": "Frequency", "bandwidth": "Bandwidth", "spreading_factor": "Spreading Factor", diff --git a/src/meshcore_hub/web/static/locales/nl.json b/src/meshcore_hub/web/static/locales/nl.json index 819617c..bdd6e76 100644 --- a/src/meshcore_hub/web/static/locales/nl.json +++ b/src/meshcore_hub/web/static/locales/nl.json @@ -130,7 +130,6 @@ "all_discovered_nodes": "Alle ontdekte knooppunten", "network_info": "Netwerkinfo", "network_activity": "Netwerkactiviteit", - "meshcore_attribution": "Ons lokale off-grid mesh-netwerk is mogelijk gemaakt door", "frequency": "Frequentie", "bandwidth": "Bandbreedte", "spreading_factor": "Spreading Factor", diff --git a/src/meshcore_hub/web/templates/spa.html b/src/meshcore_hub/web/templates/spa.html index 0092573..3f583ea 100644 --- a/src/meshcore_hub/web/templates/spa.html +++ b/src/meshcore_hub/web/templates/spa.html @@ -110,14 +110,29 @@ -