Browse Source

fix media note

cleaned up terminal
imwald
Silberengel 4 weeks ago
parent
commit
dcd22ea496
  1. 74
      src/components/PostEditor/PostContent.tsx
  2. 74
      src/lib/error-suppression.ts
  3. 21
      src/lib/languagetool-client.ts
  4. 8
      src/lib/logger.ts
  5. 130
      vite.config.ts

74
src/components/PostEditor/PostContent.tsx

@ -383,6 +383,9 @@ export default function PostContent({
/** Accumulates imeta tags across uploads (short note or multi-attachment) so files are not dropped. */ /** Accumulates imeta tags across uploads (short note or multi-attachment) so files are not dropped. */
const composerImetaTagsRef = useRef<string[][]>([]) const composerImetaTagsRef = useRef<string[][]>([])
const mediaNoteKindRef = useRef<number | null>(null) const mediaNoteKindRef = useRef<number | null>(null)
/** True when the hidden uploader was opened from Note type → Media Note (not toolbar paste/drop). */
const mediaNoteUploaderIntentRef = useRef(false)
const [mediaNoteUploadPending, setMediaNoteUploadPending] = useState(false)
/** Stable auto d-tag when the field is left empty; `{ slug, value }` resets when article subtype changes. */ /** Stable auto d-tag when the field is left empty; `{ slug, value }` resets when article subtype changes. */
const articleDTagFallbackRef = useRef<{ slug: string; value: string } | null>(null) const articleDTagFallbackRef = useRef<{ slug: string; value: string } | null>(null)
@ -1520,8 +1523,14 @@ export default function PostContent({
} }
} }
const handlePlainNoteMode = () => { const clearMediaNoteUploadIntent = useCallback(() => {
if (parentEvent) return mediaNoteUploaderIntentRef.current = false
setMediaNoteUploadPending(false)
}, [])
const isMediaNoteComposerMode = mediaNoteKind !== null || mediaNoteUploadPending
const clearNonMediaNoteComposerModes = () => {
setIsPoll(false) setIsPoll(false)
setIsPublicMessage(false) setIsPublicMessage(false)
setIsHighlight(false) setIsHighlight(false)
@ -1534,6 +1543,21 @@ export default function PostContent({
setIsCitationHardcopy(false) setIsCitationHardcopy(false)
setIsCitationPrompt(false) setIsCitationPrompt(false)
setIsDiscussionThread(false) setIsDiscussionThread(false)
}
const beginMediaNoteUpload = () => {
if (parentEvent) return
clearNonMediaNoteComposerModes()
clearMediaNoteUploadIntent()
setMediaNoteUploadPending(true)
mediaNoteUploaderIntentRef.current = true
mediaUploaderBtnRef.current?.click()
}
const handlePlainNoteMode = () => {
if (parentEvent) return
clearNonMediaNoteComposerModes()
clearMediaNoteUploadIntent()
// Short note (kind 1) still supports NIP-94 imeta; only Clear should drop uploads/tags. // Short note (kind 1) still supports NIP-94 imeta; only Clear should drop uploads/tags.
setMediaNoteKind(null) setMediaNoteKind(null)
} }
@ -1657,7 +1681,7 @@ export default function PostContent({
!isCitationHardcopy && !isCitationHardcopy &&
!isCitationPrompt && !isCitationPrompt &&
!isDiscussionThread && !isDiscussionThread &&
mediaNoteKind === null, !isMediaNoteComposerMode,
[ [
parentEvent, parentEvent,
isPoll, isPoll,
@ -1672,7 +1696,7 @@ export default function PostContent({
isCitationHardcopy, isCitationHardcopy,
isCitationPrompt, isCitationPrompt,
isDiscussionThread, isDiscussionThread,
mediaNoteKind isMediaNoteComposerMode
] ]
) )
@ -1888,6 +1912,7 @@ export default function PostContent({
selectedKind?: number, selectedKind?: number,
opts?: { skipComposerUrlAppend?: boolean } opts?: { skipComposerUrlAppend?: boolean }
) => { ) => {
const fromMediaNoteMenu = mediaNoteUploaderIntentRef.current
try { try {
let resolvedKind: number let resolvedKind: number
if (selectedKind !== undefined) { if (selectedKind !== undefined) {
@ -1896,8 +1921,11 @@ export default function PostContent({
resolvedKind = await getMediaKindFromFile(uploadingFile, false) resolvedKind = await getMediaKindFromFile(uploadingFile, false)
} }
// New-post composer: images stay kind 1 (short text + imeta + URL), not kind 20 picture notes. // Toolbar/drop: images stay kind 1 (short text + imeta). Media Note menu: use NIP-94 media kinds (20/21/22/1222).
if (resolvedKind === ExtendedKind.PICTURE) { if (fromMediaNoteMenu) {
setMediaNoteKind(resolvedKind)
setMediaUrl(url)
} else if (resolvedKind === ExtendedKind.PICTURE) {
setMediaNoteKind(null) setMediaNoteKind(null)
setMediaUrl('') setMediaUrl('')
} else { } else {
@ -1964,6 +1992,11 @@ export default function PostContent({
if (mediaNoteKindRef.current !== null) { if (mediaNoteKindRef.current !== null) {
setMediaUrl((prev) => prev || url) setMediaUrl((prev) => prev || url)
} }
} finally {
if (fromMediaNoteMenu) {
mediaNoteUploaderIntentRef.current = false
setMediaNoteUploadPending(false)
}
} }
} }
@ -1992,6 +2025,10 @@ export default function PostContent({
} }
if (!uploadingFile) { if (!uploadingFile) {
logger.warn('Media upload succeeded but file not found') logger.warn('Media upload succeeded but file not found')
if (mediaNoteUploaderIntentRef.current) {
mediaNoteUploaderIntentRef.current = false
setMediaNoteUploadPending(false)
}
return return
} }
@ -2120,19 +2157,9 @@ export default function PostContent({
// Don't throw - just log the error so the upload doesn't fail completely // Don't throw - just log the error so the upload doesn't fail completely
} }
// Clear other note types when media is selected if (!mediaNoteUploaderIntentRef.current) {
setIsPoll(false) clearNonMediaNoteComposerModes()
setIsPublicMessage(false) }
setIsHighlight(false)
setIsLongFormArticle(false)
setIsWikiArticle(false)
setIsNostrSpecification(false)
setIsPublicationContent(false)
setIsCitationInternal(false)
setIsCitationExternal(false)
setIsCitationHardcopy(false)
setIsCitationPrompt(false)
setIsDiscussionThread(false)
// Clear uploaded file map (upload finished). Keep composerImetaTagsRef in sync with mediaImetaTags — do not wipe here. // Clear uploaded file map (upload finished). Keep composerImetaTagsRef in sync with mediaImetaTags — do not wipe here.
uploadedMediaFileMap.current.clear() uploadedMediaFileMap.current.clear()
@ -2241,6 +2268,7 @@ export default function PostContent({
setText('') setText('')
setMediaNoteKind(null) setMediaNoteKind(null)
setMediaUrl('') setMediaUrl('')
clearMediaNoteUploadIntent()
setMediaImetaTags([]) setMediaImetaTags([])
setMentions([]) setMentions([])
setExtractedMentions([]) setExtractedMentions([])
@ -3043,7 +3071,7 @@ export default function PostContent({
isPublicMessage ? MessageCircle : isPublicMessage ? MessageCircle :
isPoll ? ListTodo : isPoll ? ListTodo :
isDiscussionThread ? MessagesSquare : isDiscussionThread ? MessagesSquare :
mediaNoteKind !== null ? Upload : isMediaNoteComposerMode ? Upload :
StickyNote StickyNote
const activeLabel = const activeLabel =
isLongFormArticle ? t('Long-form Article') : isLongFormArticle ? t('Long-form Article') :
@ -3058,7 +3086,7 @@ export default function PostContent({
isPublicMessage ? t('Public Message') : isPublicMessage ? t('Public Message') :
isPoll ? t('Poll') : isPoll ? t('Poll') :
isDiscussionThread ? t('Thread') : isDiscussionThread ? t('Thread') :
mediaNoteKind !== null ? t('Media Note') : isMediaNoteComposerMode ? t('Media Note') :
t('Short Note') t('Short Note')
return ( return (
<div className="flex flex-wrap items-center justify-end gap-1.5"> <div className="flex flex-wrap items-center justify-end gap-1.5">
@ -3111,13 +3139,13 @@ export default function PostContent({
</div> </div>
{isPlainShortNoteToolbar && <Check className="h-4 w-4 shrink-0 text-primary" />} {isPlainShortNoteToolbar && <Check className="h-4 w-4 shrink-0 text-primary" />}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => mediaUploaderBtnRef.current?.click()} className="gap-3 py-2 cursor-pointer"> <DropdownMenuItem onClick={beginMediaNoteUpload} className="gap-3 py-2 cursor-pointer">
<Upload className="h-4 w-4 shrink-0 text-muted-foreground" /> <Upload className="h-4 w-4 shrink-0 text-muted-foreground" />
<div className="flex flex-col flex-1 min-w-0"> <div className="flex flex-col flex-1 min-w-0">
<span className="font-medium leading-none">{t('Media Note')}</span> <span className="font-medium leading-none">{t('Media Note')}</span>
<span className="text-xs text-muted-foreground mt-0.5">{t('Attach image, audio, or video')}</span> <span className="text-xs text-muted-foreground mt-0.5">{t('Attach image, audio, or video')}</span>
</div> </div>
{mediaNoteKind !== null && <Check className="h-4 w-4 shrink-0 text-primary" />} {isMediaNoteComposerMode && <Check className="h-4 w-4 shrink-0 text-primary" />}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={handleHighlightToggle} className="gap-3 py-2 cursor-pointer"> <DropdownMenuItem onClick={handleHighlightToggle} className="gap-3 py-2 cursor-pointer">

74
src/lib/error-suppression.ts

@ -44,6 +44,68 @@ function isExpectedFaviconNetworkNoise(message: string): boolean {
) )
} }
/** Expected dev-only noise: optional proxies, profile cache misses, wallet timeouts, editor quirks. */
function isExpectedDevAppNoise(message: string): boolean {
if (
message.includes('[ReplaceableEventService] Profile batch network load timed out') ||
message.includes('[ReplaceableEventService] fetchProfilesForPubkeys exceeded wall timeout')
) {
return true
}
if (
message.includes('[LanguageTool] HTTP error') ||
message.includes('LanguageTool: 5') ||
message.includes('[Translate] Optional translate proxy offline') ||
message.includes('[Translate] /languages skipped') ||
message.includes('[Optional proxy] Sites proxy returned')
) {
return true
}
if (
message.includes('Failed to request get_info') ||
message.includes('Using minimal getInfo') ||
message.includes('reply timeout: event')
) {
return true
}
if (message.includes('NIP-04 encryption is about to be deprecated')) {
return true
}
if (
message.includes('TextSelection endpoint not pointing into a node with inline content') ||
message.includes('scroll-verknüpften Positionierungseffekt') ||
message.includes('scroll-linked positioning effect')
) {
return true
}
if (
message.includes('Cookie') &&
(message.includes('abgelehnt') || message.includes('rejected')) &&
message.includes('SameSite')
) {
return true
}
if (
message.includes('Cross-Origin-Resource-Policy') ||
(message.includes('CORP') && message.includes('blockiert'))
) {
return true
}
if (
message.includes('[QueryService] req_end') ||
message.includes('[relay-req]') ||
message.includes('[FeedPaint]') ||
message.includes('[LiveActivities] poll done') ||
message.includes('[RelayPoolIdle]')
) {
return true
}
if (message.includes('[vite]') && (message.includes('connected') || message.includes('connecting'))) {
return true
}
return false
}
function isExpectedRelayWebSocketNoise(message: string): boolean { function isExpectedRelayWebSocketNoise(message: string): boolean {
if (message.includes('WebSocket connection to') || message.includes('Close received after close')) { if (message.includes('WebSocket connection to') || message.includes('Close received after close')) {
return true return true
@ -92,6 +154,10 @@ function suppressExpectedErrors() {
return return
} }
if (import.meta.env.DEV && isExpectedDevAppNoise(message)) {
return
}
if (message.includes('NS_BINDING_ABORTED')) { if (message.includes('NS_BINDING_ABORTED')) {
return return
} }
@ -242,6 +308,10 @@ function suppressExpectedErrors() {
return return
} }
if (import.meta.env.DEV && isExpectedDevAppNoise(message)) {
return
}
// Suppress invalid URI / failed media resource (e.g. empty img src) // Suppress invalid URI / failed media resource (e.g. empty img src)
if (message.includes('Ungültige URI') || if (message.includes('Ungültige URI') ||
message.includes('Invalid URI') || message.includes('Invalid URI') ||
@ -356,6 +426,10 @@ function suppressExpectedErrors() {
return return
} }
if (import.meta.env.DEV && isExpectedDevAppNoise(message)) {
return
}
// Firefox ORB: cross-origin favicon / relay icon requests often hit HTML or wrong MIME; not actionable in-app. // Firefox ORB: cross-origin favicon / relay icon requests often hit HTML or wrong MIME; not actionable in-app.
if ( if (
message.includes('OpaqueResponseBlocking') || message.includes('OpaqueResponseBlocking') ||

21
src/lib/languagetool-client.ts

@ -2,6 +2,14 @@ import { LANGUAGE_TOOL_URL } from '@/constants'
import { electronAwareFetch } from '@/lib/electron-aware-fetch' import { electronAwareFetch } from '@/lib/electron-aware-fetch'
import logger from '@/lib/logger' import logger from '@/lib/logger'
/** After proxy/backend 502/503/504, skip further grammar HTTP this tab (optional dev proxy). */
let languageToolBackendGoneThisSession = false
let languageToolOptionalLogged = false
export function isLanguageToolBackendUnreachableThisSession(): boolean {
return languageToolBackendGoneThisSession
}
export type LanguageToolMatch = { export type LanguageToolMatch = {
offset: number offset: number
length: number length: number
@ -28,7 +36,7 @@ export async function languageToolCheck(
signal?: AbortSignal signal?: AbortSignal
): Promise<LanguageToolCheckResponse> { ): Promise<LanguageToolCheckResponse> {
const url = checkUrl() const url = checkUrl()
if (!url) { if (!url || languageToolBackendGoneThisSession) {
return { matches: [] } return { matches: [] }
} }
const body = new URLSearchParams() const body = new URLSearchParams()
@ -44,6 +52,17 @@ export async function languageToolCheck(
}) })
if (!res.ok) { if (!res.ok) {
const errText = await res.text().catch(() => '') const errText = await res.text().catch(() => '')
if ([502, 503, 504].includes(res.status)) {
languageToolBackendGoneThisSession = true
if (import.meta.env.DEV && !languageToolOptionalLogged) {
languageToolOptionalLogged = true
logger.debug(
'[LanguageTool] Optional grammar proxy offline; skipping further checks this session.',
{ status: res.status, errText: errText.slice(0, 120) }
)
}
return { matches: [] }
}
logger.warn('[LanguageTool] HTTP error', { status: res.status, errText: errText.slice(0, 200) }) logger.warn('[LanguageTool] HTTP error', { status: res.status, errText: errText.slice(0, 200) })
throw new Error(`LanguageTool: ${res.status}`) throw new Error(`LanguageTool: ${res.status}`)
} }

8
src/lib/logger.ts

@ -2,8 +2,8 @@
* Centralized logging utility. * Centralized logging utility.
* *
* Level matrix: * Level matrix:
* dev (default) debug / info / warn / error (full formatted output; `logger.debug` on) * dev (default) info / warn / error (quiet console; relay/query traces off)
* dev + opt-out info / warn / error (set `imwald-debug` or `jumble-debug` to `false`) * dev + opt-in debug / info / warn / error (`imwald-debug` / `jumble-debug` `true`, or `VITE_DEBUG=true`)
* production warn / error only (bare console no timestamp string built) * production warn / error only (bare console no timestamp string built)
* *
* Opt out of debug in dev: `localStorage.setItem('imwald-debug', 'false')` then reload. * Opt out of debug in dev: `localStorage.setItem('imwald-debug', 'false')` then reload.
@ -27,8 +27,8 @@ class Logger {
localStorage.getItem('imwald-debug') === 'true' || localStorage.getItem('imwald-debug') === 'true' ||
localStorage.getItem('jumble-debug') === 'true' || localStorage.getItem('jumble-debug') === 'true' ||
import.meta.env.VITE_DEBUG === 'true' import.meta.env.VITE_DEBUG === 'true'
// `npm run dev`: debug on by default so relay/query/cache traces are visible without localStorage. // `npm run dev`: quiet by default; opt in via localStorage / `VITE_DEBUG` / `imwaldDebug.enable()`.
this.enableDebug = this.isDev && (explicitOn || !explicitOff) this.enableDebug = this.isDev && explicitOn && !explicitOff
// In production only warn/error reach the console — info is noise for end-users. // In production only warn/error reach the console — info is noise for end-users.
this.minLevel = this.enableDebug ? 'debug' : this.isDev ? 'info' : 'warn' this.minLevel = this.enableDebug ? 'debug' : this.isDev ? 'info' : 'warn'

130
vite.config.ts

@ -45,35 +45,50 @@ function fullReloadOnProvidersAndPages(): Plugin {
} }
} }
/** /** Loopback targets in `server.proxy` — optional in dev; see PROXY_SETUP.md. */
* `http-proxy` logs `Error: connect ECONNREFUSED …` via `console.error`, bypassing Vite's `logger.error`. const OPTIONAL_DEV_PROXY_LOOPBACK_PORTS = [4000, 5000, 8010, 8090, 9876] as const
*/
function isOptionalDevProxyConnRefusedNoise(args: unknown[]): boolean { function blobFromLogArgs(args: unknown[]): string {
const blob = args return args
.map((a) => { .map((a) => {
if (typeof a === 'string') return a if (typeof a === 'string') return a
if (a instanceof Error) return `${a.message}\n${a.stack ?? ''}` if (a instanceof Error) return `${a.message}\n${a.stack ?? ''}`
return '' return ''
}) })
.join('\n') .join('\n')
}
/**
* `http-proxy` logs `Error: connect ECONNREFUSED …` via `console.error`, bypassing Vite's `logger.error`.
*/
function isOptionalDevProxyConnRefusedNoise(args: unknown[]): boolean {
const blob = blobFromLogArgs(args)
if (!blob.includes('ECONNREFUSED')) return false if (!blob.includes('ECONNREFUSED')) return false
return ( if (blob.includes('127.0.0.1:') || blob.includes('localhost:')) return true
blob.includes('127.0.0.1:5000') || return OPTIONAL_DEV_PROXY_LOOPBACK_PORTS.some((port) => new RegExp(`\\b:${port}\\b`).test(blob))
blob.includes('127.0.0.1:8090') || }
/\b:5000\b/.test(blob) ||
/\b:8090\b/.test(blob) function isOptionalDevProxyHttpError(text: string): boolean {
) if (!text.includes('http proxy error')) return false
if (!text.includes('ECONNREFUSED')) return false
if (
text.includes('/api/languagetool') ||
text.includes('/v2/') ||
text.includes('/api/piper-tts') ||
text.includes('/api/translate') ||
text.includes('/sites') ||
text.includes('/dev-index-relay') ||
text.includes('/api/events')
) {
return true
}
return OPTIONAL_DEV_PROXY_LOOPBACK_PORTS.some((port) => text.includes(`127.0.0.1:${port}`))
} }
/** /**
* When optional localhost backends are down, `http-proxy` otherwise logs a multiline stack per request. * When optional localhost backends are down, `http-proxy` otherwise logs a multiline stack per request.
* Throttle to one hint per category (cooldown), matching paths only real misconfigurations still log.
*/ */
function quietOptionalDevProxyErrors(devIndexRelayTarget: string): Plugin { function quietOptionalDevProxyErrors(): Plugin {
let lastIndexRelaySuppressed = 0
let lastTranslateSitesSuppressed = 0
const COOLDOWN_MS = 60_000
return { return {
name: 'quiet-optional-dev-proxy-errors', name: 'quiet-optional-dev-proxy-errors',
apply: 'serve', apply: 'serve',
@ -91,31 +106,27 @@ function quietOptionalDevProxyErrors(devIndexRelayTarget: string): Plugin {
const prevError = config.logger.error.bind(config.logger) const prevError = config.logger.error.bind(config.logger)
config.logger.error = (msg, options) => { config.logger.error = (msg, options) => {
const text = typeof msg === 'string' ? msg : '' const text = typeof msg === 'string' ? msg : ''
if (text.includes('http proxy error') && text.includes('ECONNREFUSED')) { if (isOptionalDevProxyHttpError(text)) return
if (text.includes('/api/events') || text.includes('/dev-index-relay')) { prevError(msg, options)
const now = Date.now()
if (now - lastIndexRelaySuppressed >= COOLDOWN_MS) {
lastIndexRelaySuppressed = now
config.logger.warn(
`[vite] Dev index relay not reachable (${devIndexRelayTarget}). Start it or set VITE_DEV_INDEX_RELAY_TARGET. Suppressing duplicate proxy errors for ${COOLDOWN_MS / 1000}s.`
)
}
return
} }
if (text.includes('/api/translate') || text.includes('/sites')) {
const now = Date.now()
if (now - lastTranslateSitesSuppressed >= COOLDOWN_MS) {
lastTranslateSitesSuppressed = now
config.logger.warn(
`[vite] Optional dev proxies unreachable (LibreTranslate /api/translate → :5000, OG /sites → :8090). Start them or ignore — see PROXY_SETUP.md. Suppressing duplicate proxy errors for ${COOLDOWN_MS / 1000}s.`
)
} }
return
} }
} }
prevError(msg, options)
function jsonProxyErrorHandler(status: number, body: Record<string, unknown>) {
return (proxy: { on: (event: string, handler: (...args: unknown[]) => void) => void }) => {
proxy.on('error', (_err, _req, res) => {
const r = res as {
headersSent?: boolean
writeHead?: (c: number, h: Record<string, string>) => void
end?: (b: string) => void
} }
if (r.headersSent) return
if (typeof r?.writeHead === 'function' && typeof r?.end === 'function') {
r.writeHead(status, { 'Content-Type': 'application/json' })
r.end(JSON.stringify(body))
} }
})
} }
} }
@ -202,52 +213,41 @@ export default defineConfig(({ mode }) => {
// Read-aloud Piper: same path as production Apache → aitherboard (avoid cross-origin CORS in dev). // Read-aloud Piper: same path as production Apache → aitherboard (avoid cross-origin CORS in dev).
'/api/piper-tts': { '/api/piper-tts': {
target: 'http://127.0.0.1:9876', target: 'http://127.0.0.1:9876',
changeOrigin: true changeOrigin: true,
configure: jsonProxyErrorHandler(503, {
ok: false,
error: 'piper_proxy_unreachable',
hint: 'Start Piper TTS on :9876 — see PROXY_SETUP.md'
})
}, },
'/api/languagetool': { '/api/languagetool': {
target: 'http://127.0.0.1:8010', target: 'http://127.0.0.1:8010',
changeOrigin: true, changeOrigin: true,
rewrite: (p) => p.replace(/^\/api\/languagetool/u, '') || '/' rewrite: (p) => p.replace(/^\/api\/languagetool/u, '') || '/',
configure: jsonProxyErrorHandler(503, {
ok: false,
error: 'languagetool_proxy_unreachable',
hint: 'Start LanguageTool on :8010 — see PROXY_SETUP.md'
})
}, },
'/api/translate': { '/api/translate': {
target: 'http://127.0.0.1:5000', target: 'http://127.0.0.1:5000',
changeOrigin: true, changeOrigin: true,
rewrite: (p) => p.replace(/^\/api\/translate/u, '') || '/', rewrite: (p) => p.replace(/^\/api\/translate/u, '') || '/',
/** Match `/sites`: when LibreTranslate is not running, return JSON instead of a broken proxy response. */ configure: jsonProxyErrorHandler(503, {
configure(proxy) {
proxy.on('error', (_err, _req, res) => {
const r = res as {
headersSent?: boolean
writeHead?: (c: number, h: Record<string, string>) => void
end?: (b: string) => void
}
if (r.headersSent) return
if (typeof r?.writeHead === 'function' && typeof r?.end === 'function') {
r.writeHead(503, { 'Content-Type': 'application/json' })
r.end(
JSON.stringify({
ok: false, ok: false,
error: 'translate_proxy_unreachable', error: 'translate_proxy_unreachable',
hint: 'Start LibreTranslate (or compatible API) on :5000 — see PROXY_SETUP.md' hint: 'Start LibreTranslate (or compatible API) on :5000 — see PROXY_SETUP.md'
}) })
)
}
})
}
}, },
'/sites': { '/sites': {
target: 'http://127.0.0.1:8090', target: 'http://127.0.0.1:8090',
changeOrigin: true, changeOrigin: true,
/** Without OG proxy on :8090, Node was returning 500 HTML; return JSON so callers fail softly in dev. */ configure: jsonProxyErrorHandler(502, {
configure(proxy) { ok: false,
proxy.on('error', (_err, _req, res) => { error: 'og_proxy_unreachable',
const r = res as { writeHead?: (c: number, h: Record<string, string>) => void; end?: (b: string) => void } hint: 'Start OG scraper on :8090 (see PROXY_SETUP.md)'
if (typeof r?.writeHead === 'function' && typeof r?.end === 'function') {
r.writeHead(502, { 'Content-Type': 'application/json' })
r.end(JSON.stringify({ ok: false, error: 'og_proxy_unreachable', hint: 'Start OG scraper on :8090 (see PROXY_SETUP.md)' }))
}
}) })
}
}, },
// Loopback HTTP index relay: `import.meta.env.DEV` rewrites kind 10243 URLs through this path. // Loopback HTTP index relay: `import.meta.env.DEV` rewrites kind 10243 URLs through this path.
'/dev-index-relay': { '/dev-index-relay': {
@ -437,7 +437,7 @@ export default defineConfig(({ mode }) => {
plugins: [ plugins: [
react(), react(),
fullReloadOnProvidersAndPages(), fullReloadOnProvidersAndPages(),
quietOptionalDevProxyErrors(devIndexRelayTarget), quietOptionalDevProxyErrors(),
VitePWA({ VitePWA({
// Prompt mode + virtual:pwa-register (main bundle) — do not auto-activate; VersionUpdateBanner calls updateSW(). // Prompt mode + virtual:pwa-register (main bundle) — do not auto-activate; VersionUpdateBanner calls updateSW().
registerType: 'prompt', registerType: 'prompt',

Loading…
Cancel
Save