Fix async error handling + build.sh copy/inject separation
wrapper.tsx: - Added error state — shows graceful message instead of silent vanish - useEffect catch sets error string, not null data - refresh() catch sets error string, not empty catch - Error UI shows gray card with message in widget slot build.sh: - ALWAYS copies TSX files (even on reinstall — fixes stale component bug) - Separated copy step from injection step - ErrorBoundary upgrade path: removes bare <ModpackVersionCard />, replaces with wrapped version - Imports added independently (not as one sed block) - Renumbered sections for clarity Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b20f342bd3
commit
0caddef86d
@@ -0,0 +1,70 @@
|
||||
# MSG-2026-04-13-async-error-handling
|
||||
|
||||
**From:** Chronicler #85
|
||||
**Date:** 2026-04-13
|
||||
**Priority:** HIGH — required before live panel deploy
|
||||
**Status:** OPEN
|
||||
|
||||
## Context
|
||||
|
||||
Gemini flagged async error handling as a blind spot in our ErrorBoundary approach.
|
||||
ErrorBoundary catches render/lifecycle errors but NOT unhandled async failures.
|
||||
|
||||
## Current Problems in wrapper.tsx
|
||||
|
||||
**1. useEffect catch (line ~49):**
|
||||
```tsx
|
||||
.catch(() => setData(null))
|
||||
.finally(() => setLoading(false))
|
||||
```
|
||||
On failure: `data = null` → `if (!data) return null` → widget vanishes silently.
|
||||
User sees nothing, no explanation.
|
||||
|
||||
**2. refresh() catch (line ~59):**
|
||||
```tsx
|
||||
} catch {}
|
||||
```
|
||||
Completely empty. Fails silently, user has no idea the refresh failed.
|
||||
|
||||
## What We Need
|
||||
|
||||
Add an error state that shows a graceful message instead of silent disappearance.
|
||||
|
||||
Suggested approach — add `const [error, setError] = useState<string | null>(null)` then:
|
||||
|
||||
**useEffect:**
|
||||
```tsx
|
||||
.catch(() => setError('Unable to load modpack status.'))
|
||||
.finally(() => setLoading(false))
|
||||
```
|
||||
|
||||
**refresh:**
|
||||
```tsx
|
||||
} catch {
|
||||
setError('Check failed. Try again.');
|
||||
}
|
||||
```
|
||||
|
||||
**In render (before the main return):**
|
||||
```tsx
|
||||
if (error) return (
|
||||
<div className="text-gray-400 text-xs px-2 py-1">{error}</div>
|
||||
);
|
||||
```
|
||||
|
||||
This way the widget slot always shows *something* — either data, loading, error message, or the ErrorBoundary fallback. No silent disappearance.
|
||||
|
||||
## Gemini's Exact Words
|
||||
|
||||
> "If an async API call fails and you don't have a .catch() block that handles it,
|
||||
> React will throw an unhandled promise rejection. Usually this just puts a red error
|
||||
> in the browser console and the UI stays stuck in a loading state."
|
||||
> "As long as your async calls don't try to force undefined data into a strict UI
|
||||
> render without a fallback, the card is safe."
|
||||
|
||||
## After Code Pushes
|
||||
|
||||
Chronicler deploys to live panel immediately after.
|
||||
|
||||
---
|
||||
*— Chronicler #85*
|
||||
@@ -0,0 +1,100 @@
|
||||
# MSG-2026-04-13-dev-panel-manual-changes
|
||||
|
||||
**From:** Chronicler #85
|
||||
**Date:** 2026-04-13
|
||||
**Priority:** HIGH — repo does not reflect Dev Panel state
|
||||
**Status:** OPEN
|
||||
|
||||
## What I Did on Dev Panel That's Not in the Repo
|
||||
|
||||
During Dev Panel validation I made several manual fixes that exposed gaps in how
|
||||
Blueprint handles the extension package. These need to be properly encoded in the
|
||||
repo so live panel deploy and future installs work correctly.
|
||||
|
||||
---
|
||||
|
||||
### 1. AfterInformation.tsx — ErrorBoundary wrapper added manually
|
||||
|
||||
Blueprint's `blueprint -install` skipped the ErrorBoundary injection because
|
||||
`ModpackVersionCard` was already present (the "already present" check in build.sh
|
||||
only looks for the import, not whether it's wrapped).
|
||||
|
||||
I manually patched the file directly:
|
||||
```tsx
|
||||
// Before (on server):
|
||||
<ModpackVersionCard />
|
||||
|
||||
// After (manually patched):
|
||||
<ModpackErrorBoundary><ModpackVersionCard /></ModpackErrorBoundary>
|
||||
```
|
||||
|
||||
**The build.sh injection logic needs to handle the update case** — if
|
||||
`ModpackVersionCard` is present but NOT wrapped in `ModpackErrorBoundary`,
|
||||
it should add the wrapper. Currently it just skips entirely.
|
||||
|
||||
---
|
||||
|
||||
### 2. wrapper.tsx never replaced ModpackVersionCard.tsx via build.sh
|
||||
|
||||
build.sh copies `wrapper.tsx → ModpackVersionCard.tsx` but skips if
|
||||
`ModpackVersionCard` import already exists in `AfterInformation.tsx`.
|
||||
The "already present" check is too broad — it prevents the component file
|
||||
itself from being updated on subsequent deploys.
|
||||
|
||||
I manually copied: `wrapper.tsx → resources/scripts/components/server/ModpackVersionCard.tsx`
|
||||
|
||||
**build.sh needs to always copy wrapper.tsx regardless of injection state.**
|
||||
Separate the "copy the component file" step from the "inject into AfterInformation" step.
|
||||
|
||||
---
|
||||
|
||||
### 3. Blueprint extension package was stale
|
||||
|
||||
After `blueprint -install`, Blueprint restores files from its own internal
|
||||
package — overwriting anything we deployed. I had to manually copy updated files
|
||||
INTO the Blueprint extension package:
|
||||
|
||||
```
|
||||
.blueprint/extensions/modpackchecker/build.sh ← copied v1.1.0
|
||||
.blueprint/extensions/modpackchecker/views/server/ErrorBoundary.tsx ← copied
|
||||
.blueprint/extensions/modpackchecker/views/server/wrapper.tsx ← copied
|
||||
.blueprint/extensions/modpackchecker/routes/client.php ← copied
|
||||
.blueprint/extensions/modpackchecker/routers/client.php ← copied
|
||||
```
|
||||
|
||||
**The `.conf` Blueprint archive needs to be rebuilt to include all v1.1.0 files.**
|
||||
Otherwise every `blueprint -install` will regress to stale files and require
|
||||
manual re-copying after each install.
|
||||
|
||||
---
|
||||
|
||||
### 4. Routes deployed to 3 locations manually
|
||||
|
||||
`routes/client.php` exists in the repo but Blueprint wasn't picking it up.
|
||||
Had to copy to all three locations Blueprint uses:
|
||||
```
|
||||
.blueprint/extensions/modpackchecker/routes/client.php
|
||||
.blueprint/extensions/modpackchecker/routers/client.php
|
||||
routes/blueprint/client/modpackchecker.php
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary of What Needs to Be Fixed in the Repo
|
||||
|
||||
1. **build.sh** — separate component copy from injection; always copy wrapper.tsx;
|
||||
add ErrorBoundary wrapper detection for update path
|
||||
2. **Blueprint .conf archive** — rebuild to include v1.1.0 files so fresh installs
|
||||
don't regress (this may require Blueprint-specific tooling)
|
||||
3. **Deployment docs** — document that after `blueprint -install`, manually copy
|
||||
wrapper.tsx and routes until the .conf is rebuilt
|
||||
|
||||
## What's Currently Working on Dev Panel
|
||||
|
||||
Despite all of the above, Dev Panel IS working correctly right now because I
|
||||
manually fixed everything. The widget loads zero-click, ErrorBoundary is in place,
|
||||
routes are registered. But it's fragile — another `blueprint -install` would
|
||||
regress it.
|
||||
|
||||
---
|
||||
*— Chronicler #85*
|
||||
@@ -46,53 +46,73 @@ if [ "$NODE_MAJOR_VERSION" -ge 17 ]; then
|
||||
fi
|
||||
|
||||
# ===========================================
|
||||
# 1. CONSOLE WIDGET + ERROR BOUNDARY
|
||||
# 1. COPY TSX COMPONENTS (always — even on reinstall)
|
||||
# ===========================================
|
||||
echo ""
|
||||
echo "--- Console Widget ---"
|
||||
echo "--- Copy Components ---"
|
||||
|
||||
if [ -f "$EXT_DIR/views/server/wrapper.tsx" ]; then
|
||||
cp "$EXT_DIR/views/server/wrapper.tsx" resources/scripts/components/server/ModpackVersionCard.tsx
|
||||
echo "✓ Copied ModpackVersionCard.tsx"
|
||||
else
|
||||
echo "⚠ wrapper.tsx not found, skipping console widget"
|
||||
echo "⚠ wrapper.tsx not found"
|
||||
fi
|
||||
|
||||
if [ -f "$EXT_DIR/views/server/ErrorBoundary.tsx" ]; then
|
||||
cp "$EXT_DIR/views/server/ErrorBoundary.tsx" resources/scripts/components/server/ModpackErrorBoundary.tsx
|
||||
echo "✓ Copied ModpackErrorBoundary.tsx"
|
||||
else
|
||||
echo "⚠ ErrorBoundary.tsx not found, skipping error boundary"
|
||||
echo "⚠ ErrorBoundary.tsx not found"
|
||||
fi
|
||||
|
||||
# Inject into AfterInformation.tsx (wrapped in ErrorBoundary)
|
||||
# ===========================================
|
||||
# 2. INJECT INTO AFTERINFORMATION.TSX
|
||||
# ===========================================
|
||||
echo ""
|
||||
echo "--- Console Widget Injection ---"
|
||||
|
||||
AFTER_INFO="resources/scripts/blueprint/components/Server/Terminal/AfterInformation.tsx"
|
||||
if [ -f "$AFTER_INFO" ]; then
|
||||
# Step A: Add imports if missing
|
||||
if ! grep -q "ModpackVersionCard" "$AFTER_INFO" 2>/dev/null; then
|
||||
sed -i '/\/\* blueprint\/import \*\//a import ModpackVersionCard from "@/components/server/ModpackVersionCard";\nimport ModpackErrorBoundary from "@/components/server/ModpackErrorBoundary";' "$AFTER_INFO"
|
||||
sed -i '/\/\* blueprint\/import \*\//a import ModpackVersionCard from "@/components/server/ModpackVersionCard";' "$AFTER_INFO"
|
||||
echo "✓ Added ModpackVersionCard import"
|
||||
fi
|
||||
if ! grep -q "ModpackErrorBoundary" "$AFTER_INFO" 2>/dev/null; then
|
||||
sed -i '/\/\* blueprint\/import \*\//a import ModpackErrorBoundary from "@/components/server/ModpackErrorBoundary";' "$AFTER_INFO"
|
||||
echo "✓ Added ModpackErrorBoundary import"
|
||||
fi
|
||||
|
||||
# Step B: Add wrapped component if not present
|
||||
if ! grep -q "ModpackErrorBoundary" "$AFTER_INFO" 2>/dev/null || ! grep -q "<ModpackErrorBoundary>" "$AFTER_INFO" 2>/dev/null; then
|
||||
# Remove bare <ModpackVersionCard /> if it exists (upgrade path)
|
||||
sed -i 's|<ModpackVersionCard />||g' "$AFTER_INFO"
|
||||
# Inject wrapped version
|
||||
sed -i 's|{/\* blueprint/react \*/}|{/* blueprint/react */}\n <ModpackErrorBoundary><ModpackVersionCard /></ModpackErrorBoundary>|' "$AFTER_INFO"
|
||||
echo "✓ Injected ModpackVersionCard (with ErrorBoundary) into AfterInformation.tsx"
|
||||
echo "✓ Injected wrapped ModpackVersionCard into AfterInformation.tsx"
|
||||
else
|
||||
echo "○ ModpackVersionCard already present in AfterInformation.tsx"
|
||||
echo "○ ModpackVersionCard (with ErrorBoundary) already present"
|
||||
fi
|
||||
else
|
||||
echo "⚠ AfterInformation.tsx not found, skipping injection"
|
||||
fi
|
||||
|
||||
# ===========================================
|
||||
# 2. DASHBOARD BADGE INJECTION
|
||||
# 3. DASHBOARD BADGE
|
||||
# ===========================================
|
||||
echo ""
|
||||
echo "--- Dashboard Badge ---"
|
||||
|
||||
# Always copy the component file
|
||||
if [ -f "$EXT_DIR/views/dashboard/UpdateBadge.tsx" ]; then
|
||||
mkdir -p resources/scripts/components/dashboard
|
||||
cp "$EXT_DIR/views/dashboard/UpdateBadge.tsx" resources/scripts/components/dashboard/UpdateBadge.tsx
|
||||
echo "✓ Copied UpdateBadge.tsx"
|
||||
else
|
||||
echo "⚠ UpdateBadge.tsx not found, skipping dashboard badge"
|
||||
echo "⚠ UpdateBadge.tsx not found"
|
||||
fi
|
||||
|
||||
# Inject into ServerRow.tsx only if not already present
|
||||
if ! grep -q "UpdateBadge" resources/scripts/components/dashboard/ServerRow.tsx 2>/dev/null; then
|
||||
sed -i '1i import UpdateBadge from "@/components/dashboard/UpdateBadge";' resources/scripts/components/dashboard/ServerRow.tsx
|
||||
sed -i 's|{server.name}</p>|{server.name}<UpdateBadge serverUuid={server.uuid} /></p>|' resources/scripts/components/dashboard/ServerRow.tsx
|
||||
|
||||
@@ -35,6 +35,7 @@ const ModpackVersionCard: React.FC = () => {
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data?.uuid);
|
||||
const [data, setData] = useState<StatusData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [showCalibrate, setShowCalibrate] = useState(false);
|
||||
const [releases, setReleases] = useState<Release[]>([]);
|
||||
@@ -44,8 +45,8 @@ const ModpackVersionCard: React.FC = () => {
|
||||
useEffect(() => {
|
||||
if (!uuid) return;
|
||||
http.get(`/api/client/extensions/modpackchecker/servers/${uuid}/status`)
|
||||
.then((res) => setData(res.data))
|
||||
.catch(() => setData(null))
|
||||
.then((res) => { setData(res.data); setError(null); })
|
||||
.catch(() => setError('Unable to load modpack status.'))
|
||||
.finally(() => setLoading(false));
|
||||
}, [uuid]);
|
||||
|
||||
@@ -53,11 +54,14 @@ const ModpackVersionCard: React.FC = () => {
|
||||
const refresh = async () => {
|
||||
if (!uuid || checking) return;
|
||||
setChecking(true);
|
||||
setError(null);
|
||||
try {
|
||||
await http.post(`/api/client/extensions/modpackchecker/servers/${uuid}/check`);
|
||||
const res = await http.get(`/api/client/extensions/modpackchecker/servers/${uuid}/status`);
|
||||
setData(res.data);
|
||||
} catch {}
|
||||
} catch {
|
||||
setError('Check failed. Try again.');
|
||||
}
|
||||
setChecking(false);
|
||||
};
|
||||
|
||||
@@ -102,8 +106,17 @@ const ModpackVersionCard: React.FC = () => {
|
||||
};
|
||||
|
||||
if (loading) return null;
|
||||
if (!data) return null;
|
||||
if (data.is_ignored) return null;
|
||||
if (data?.is_ignored) return null;
|
||||
|
||||
if (error && !data) return (
|
||||
<div className={classNames(
|
||||
'rounded shadow-lg bg-gray-600 text-gray-400 text-xs',
|
||||
'col-span-3 md:col-span-2 lg:col-span-6',
|
||||
'px-3 py-2 mt-2'
|
||||
)}>
|
||||
<FontAwesomeIcon icon={faCube} className={'w-3 h-3 mr-2'} />{error}
|
||||
</div>
|
||||
);
|
||||
|
||||
const hasUpdate = data.update_available;
|
||||
const configured = data.configured;
|
||||
|
||||
Reference in New Issue
Block a user