mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
fix(generic): browser UA default + per-source custom headers + 403 retry (#85)
The MeshAI/1.0 UA intermittently trips WAFs (Idaho Power's Azure Front Door 403s it ~2/30; a browser UA gets 200 every time). Default the adapter + preview to a browser User-Agent, retry once on 403/429, and add optional per-source custom headers (UA/auth) editable in the GUI. Makes WAF'd and keyed feeds pollable. 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:
parent
322793dab3
commit
1a1aef2e6e
5 changed files with 310 additions and 50 deletions
|
|
@ -31,6 +31,7 @@ function blankSource(n: number): GenericSource {
|
|||
field_mappings: [],
|
||||
summary_template: '',
|
||||
emoji: '',
|
||||
headers: {},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -176,12 +177,43 @@ export default function GenericSourcesEditor() {
|
|||
)
|
||||
}
|
||||
|
||||
// Headers are stored as a Record<string,string> on the source; edit them as
|
||||
// an ordered list of key/value rows (mirrors the field_mappings editor),
|
||||
// rebuilding the record on each change.
|
||||
const headerEntries = (s: GenericSource): [string, string][] =>
|
||||
Object.entries(s.headers ?? {})
|
||||
|
||||
const setHeaderEntries = (idx: number, entries: [string, string][]) => {
|
||||
const obj: Record<string, string> = {}
|
||||
for (const [k, v] of entries) obj[k] = v
|
||||
update(idx, { headers: obj })
|
||||
}
|
||||
|
||||
const addHeader = (idx: number) => {
|
||||
if (!sources) return
|
||||
setHeaderEntries(idx, [...headerEntries(sources[idx]), ['', '']])
|
||||
}
|
||||
|
||||
const updateHeader = (idx: number, hIdx: number, key: string, value: string) => {
|
||||
if (!sources) return
|
||||
const entries = headerEntries(sources[idx])
|
||||
entries[hIdx] = [key, value]
|
||||
setHeaderEntries(idx, entries)
|
||||
}
|
||||
|
||||
const removeHeader = (idx: number, hIdx: number) => {
|
||||
if (!sources) return
|
||||
const entries = headerEntries(sources[idx])
|
||||
entries.splice(hIdx, 1)
|
||||
setHeaderEntries(idx, entries)
|
||||
}
|
||||
|
||||
const runPreview = async (idx: number) => {
|
||||
if (!sources) return
|
||||
const src = sources[idx]
|
||||
setPreviewing((p) => ({ ...p, [idx]: true }))
|
||||
try {
|
||||
const res = await previewGenericSource(src.url, src.items_path)
|
||||
const res = await previewGenericSource(src.url, src.items_path, src.headers)
|
||||
setPreviews((p) => ({ ...p, [idx]: res }))
|
||||
} catch (e) {
|
||||
setPreviews((p) => ({
|
||||
|
|
@ -391,6 +423,55 @@ export default function GenericSourcesEditor() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* Headers (optional) */}
|
||||
<div className="border border-border p-3 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-[10px] font-sans font-medium uppercase tracking-widest text-[#666]">
|
||||
Headers (optional)
|
||||
</div>
|
||||
<button
|
||||
onClick={() => addHeader(i)}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs bg-accent/20 hover:bg-accent/30 text-accent border border-accent/30"
|
||||
>
|
||||
<Plus size={12} /> Add header
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-[#555]">
|
||||
Custom request headers, e.g. <code>User-Agent</code> or{' '}
|
||||
<code>Authorization</code>. Leave empty for the default browser UA.
|
||||
</p>
|
||||
{Object.entries(src.headers ?? {}).length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{Object.entries(src.headers ?? {}).map(([hKey, hVal], hIdx) => (
|
||||
<div key={hIdx} className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={hKey}
|
||||
onChange={(e) => updateHeader(i, hIdx, e.target.value, hVal)}
|
||||
placeholder="header name (e.g. Authorization)"
|
||||
className="flex-1 bg-[#0d0d0d] border border-border px-2 py-1.5 text-xs font-mono text-[#e0e0e0] placeholder:text-[#555]"
|
||||
/>
|
||||
<span className="text-[#555] text-xs">:</span>
|
||||
<input
|
||||
type="text"
|
||||
value={hVal}
|
||||
onChange={(e) => updateHeader(i, hIdx, hKey, e.target.value)}
|
||||
placeholder="value (e.g. Bearer …)"
|
||||
className="flex-1 bg-[#0d0d0d] border border-border px-2 py-1.5 text-xs font-mono text-[#e0e0e0] placeholder:text-[#555]"
|
||||
/>
|
||||
<button
|
||||
onClick={() => removeHeader(i, hIdx)}
|
||||
title="Remove header"
|
||||
className="flex items-center px-2 py-1.5 text-xs text-[#777] hover:text-red-400 border border-border"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Location */}
|
||||
<SectionLabel>Location — GeoJSON geometry OR lat + lon paths</SectionLabel>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
|
|
|
|||
|
|
@ -326,6 +326,9 @@ export interface GenericSource {
|
|||
field_mappings: FieldMapping[]
|
||||
summary_template?: string
|
||||
emoji?: string
|
||||
// Optional custom request headers (e.g. User-Agent override or Authorization).
|
||||
// Empty/absent = the default browser UA. Sent on both poll and preview.
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface GenericSourcePreview {
|
||||
|
|
@ -359,12 +362,13 @@ export async function saveGenericSources(
|
|||
|
||||
export async function previewGenericSource(
|
||||
url: string,
|
||||
items_path?: string
|
||||
items_path?: string,
|
||||
headers?: Record<string, string>
|
||||
): Promise<GenericSourcePreview> {
|
||||
const response = await fetch('/api/generic-sources/preview', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url, items_path }),
|
||||
body: JSON.stringify({ url, items_path, headers }),
|
||||
})
|
||||
// The endpoint never raises; it returns {ok:false,error} on failure too.
|
||||
return response.json()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue