feat(gui): fold custom sources into Data Feeds; retire orphan page (#82)

Move the generic-source editor into a reusable GenericSourcesEditor and render
it as a "Custom Sources" section on the Data Feeds (Environment) page, so a
custom source sits with the built-in feeds instead of an orphan page. Remove
the standalone /data-sources route + nav. Custom sources are now managed as
first-class data feeds; enable their family in Notifications to route them.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
malice 2026-07-07 01:34:26 -06:00 committed by GitHub
commit c229b1e574
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 39 additions and 13 deletions

View file

@ -1,4 +1,4 @@
import { Routes, Route } from 'react-router-dom'
import { Routes, Route, Navigate } from 'react-router-dom'
import Layout from './components/Layout'
import Dashboard from './pages/Dashboard'
import Mesh from './pages/Mesh'
@ -22,7 +22,6 @@ import ScheduledBroadcasts from './pages/ScheduledBroadcasts'
import MeshtasticDangerZones from './pages/MeshtasticDangerZones'
import MeshCoreDangerZones from './pages/MeshCoreDangerZones'
import Coverage from './pages/Coverage'
import GenericSources from './pages/GenericSources'
import { ToastProvider } from './components/ToastProvider'
import { DirtyProvider } from './context/DirtyContext'
@ -45,7 +44,8 @@ function App() {
{/* New aggregated pages */}
<Route path="/places" element={<Places />} />
<Route path="/coverage" element={<Coverage />} />
<Route path="/data-sources" element={<GenericSources />} />
{/* Custom sources folded into Data Feeds; keep old bookmark working */}
<Route path="/data-sources" element={<Navigate to="/environment" replace />} />
{/* De-navved routes still work */}
<Route path="/gauge-sites" element={<GaugeSites />} />

View file

@ -74,7 +74,15 @@ function SectionLabel({ children }: { children: React.ReactNode }) {
)
}
export default function GenericSources() {
/**
* Self-contained editor for generic (no-code) custom data sources.
*
* Owns its own data lifecycle: fetches generic_sources on mount, tracks a
* dirty flag, and saves via saveGenericSources (raising a restart banner when
* the backend reports one). Rendered as the "Custom Sources" section of the
* Data Feeds page a custom source is just another data feed.
*/
export default function GenericSourcesEditor() {
const [sources, setSources] = useState<GenericSource[] | null>(null)
const [original, setOriginal] = useState<string>('')
const [loading, setLoading] = useState(true)
@ -88,7 +96,6 @@ export default function GenericSources() {
const [previewing, setPreviewing] = useState<Record<number, boolean>>({})
useEffect(() => {
document.title = 'Data Sources — MeshAI'
fetchGenericSources()
.then((data) => {
const list = Array.isArray(data) ? data : []
@ -201,7 +208,7 @@ export default function GenericSources() {
try {
const result = await saveGenericSources(sources)
setOriginal(JSON.stringify(sources))
setSuccess('Data sources saved')
setSuccess('Custom sources saved')
setTimeout(() => setSuccess(null), 3000)
if (result.restart_required) {
notifyRestartRequired(Array.isArray(result.changed_keys) ? result.changed_keys : [])
@ -215,14 +222,14 @@ export default function GenericSources() {
if (loading) {
return (
<div className="flex items-center justify-center h-64 text-[#777]">
Loading data sources
<div className="flex items-center justify-center h-32 text-[#777]">
Loading custom sources
</div>
)
}
if (!sources) {
return (
<div className="flex items-center justify-center h-64 text-red-400">
<div className="flex items-center justify-center h-32 text-red-400">
{error || 'No config'}
</div>
)
@ -248,13 +255,18 @@ export default function GenericSources() {
return (
<div className="space-y-6 max-w-4xl">
{/* Page description + action bar */}
{/* Section description + action bar */}
<div className="flex items-start justify-between gap-4">
<p className="text-sm text-[#777]">
Point MeshAI at any public REST or GeoJSON feed no code. Each source polls a URL,
maps its JSON fields to an event via dotted paths, and broadcasts through the normal
coverage-gated pipeline. Use <span className="text-accent">Preview</span> to fetch a
URL and read its structure before filling in the paths.
URL and read its structure before filling in the paths. Once saved, a custom source
becomes a routable family {' '}
<a href="/notifications" className="text-accent hover:underline">
enable its family in Notifications
</a>{' '}
to route it to a channel.
</p>
{hasChanges && actionButtons}
</div>
@ -264,7 +276,7 @@ export default function GenericSources() {
{sources.length === 0 && (
<div className="border border-border p-6 text-center text-sm text-[#666]">
No data sources yet. Click Add source to wire up a public feed.
No custom sources yet. Click Add source to wire up a public feed.
</div>
)}

View file

@ -50,7 +50,6 @@ const navGroups: NavGroup[] = [
{ path: '/', label: 'Dashboard', icon: LayoutDashboard },
{ path: '/config', label: 'Settings', icon: Settings },
{ path: '/environment', label: 'Data Feeds', icon: Cloud },
{ path: '/data-sources', label: 'Data Sources', icon: Layers },
{ path: '/activity', label: 'Activity Log', icon: Activity },
{ path: '/places', label: 'Places', icon: MapPin },
{ path: '/coverage', label: 'Coverage', icon: Map },

View file

@ -15,6 +15,7 @@ import {
import { TOGGLE_FAMILY_META, type NotificationToggle, type NotificationsConfig } from './Notifications'
import AdapterConfig, { CURATED_KEYS } from './AdapterConfig'
import { ManagedSecret } from '@/components/ManagedSecret'
import GenericSourcesEditor from '@/components/GenericSourcesEditor'
type FeedSource = 'native' | 'central'
@ -1893,6 +1894,20 @@ const save = async () => {
</>
)}
{/* ── Custom Sources — generic no-code feeds, first-class data feeds ───── */}
<div className="pt-4 mt-2 border-t border-border space-y-4">
<div>
<h2 className="text-base font-semibold text-white flex items-center gap-2">
<Sliders size={16} /> Custom Sources
</h2>
<p className="text-xs text-[#666] mt-1">
Point MeshAI at any public REST/GeoJSON feed; it becomes a routable family in
Notifications sitting alongside the built-in feeds above.
</p>
</div>
<GenericSourcesEditor />
</div>
{/* ── Advanced: Geocoder — infra, not a feed ──────────────────────────── */}
<details className="group border border-border p-4">
<summary className="flex items-center gap-2 cursor-pointer text-sm font-medium text-[#e0e0e0] hover:text-white">