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:
Claude (Chronicler #83 - The Compiler)
2026-04-13 01:31:44 -05:00
parent b20f342bd3
commit 0caddef86d
4 changed files with 218 additions and 15 deletions

View File

@@ -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*

View File

@@ -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*

View File

@@ -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

View File

@@ -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;