Editor/EndpointErrorWindow.cs
using System;
using System.IO;
using System.Text.Json;
using Sandbox;
using Editor;
/// <summary>
/// Modal window that displays endpoint validation errors with full response JSON.
/// Shows when network-storage endpoints fail validation.
/// </summary>
public class EndpointErrorWindow : DockWindow
{
private readonly string _slug;
private readonly string _code;
private readonly string _message;
private readonly string _technicalCode;
private readonly string _rawJson;
private Vector2 _mousePos;
private Rect _copyRect;
private Rect _closeRect;
private float _scrollY;
private float _contentHeight;
private bool _copied;
private float _copiedTimer;
private EndpointErrorWindow( string slug, string code, string message, JsonElement raw )
{
_slug = slug ?? "unknown";
_technicalCode = code ?? "UNKNOWN";
_code = FriendlyCode( _technicalCode, message, raw );
_message = FriendlyMessage( _technicalCode, message, raw );
_rawJson = FormatJson( raw );
Title = $"Endpoint Error: {_slug}";
Size = new Vector2( 620, 480 );
MinimumSize = new Vector2( 500, 300 );
}
/// <summary>Show the error window with endpoint failure details.</summary>
public static void Show( string slug, string code, string message, JsonElement raw )
{
var window = new EndpointErrorWindow( slug, code, message, raw );
window.Show();
}
/// <summary>Show the error window from just slug and raw JSON (extracts code/message).</summary>
public static void Show( string slug, JsonElement raw )
{
var code = "UNKNOWN";
var message = "";
if ( raw.TryGetProperty( "error", out var err ) )
{
if ( err.ValueKind == JsonValueKind.Object )
{
code = err.TryGetProperty( "code", out var c ) && c.ValueKind == JsonValueKind.String
? c.GetString() ?? "UNKNOWN"
: "UNKNOWN";
message = err.TryGetProperty( "message", out var m ) && m.ValueKind == JsonValueKind.String
? m.GetString() ?? ""
: "";
}
else if ( err.ValueKind == JsonValueKind.String )
{
code = err.GetString() ?? "UNKNOWN";
}
}
if ( string.IsNullOrEmpty( message ) && raw.TryGetProperty( "message", out var topMsg ) && topMsg.ValueKind == JsonValueKind.String )
message = topMsg.GetString() ?? "";
Show( slug, code, message, raw );
}
private static string FriendlyCode( string code, string message, JsonElement raw )
{
var text = $"{code} {message} {raw}";
if ( text.Contains( "LEGACY_FLATTEN_FAILED", StringComparison.OrdinalIgnoreCase )
|| (text.Contains( "steps[", StringComparison.OrdinalIgnoreCase ) && text.Contains( ".id", StringComparison.OrdinalIgnoreCase )) )
return "MISSING_STEP_IDS";
if ( text.Contains( "404", StringComparison.OrdinalIgnoreCase ) || text.Contains( "not found", StringComparison.OrdinalIgnoreCase ) )
return "BACKEND_ROUTE_UNAVAILABLE";
return string.IsNullOrWhiteSpace( code ) ? "SYNC_ERROR" : code;
}
private static string FriendlyMessage( string code, string message, JsonElement raw )
{
var text = $"{code} {message} {raw}";
if ( text.Contains( "LEGACY_FLATTEN_FAILED", StringComparison.OrdinalIgnoreCase )
|| (text.Contains( "steps[", StringComparison.OrdinalIgnoreCase ) && text.Contains( ".id", StringComparison.OrdinalIgnoreCase )) )
return "Some endpoint steps are missing IDs. Use Auto-add step IDs, review the preview, then push again.";
if ( text.Contains( "404", StringComparison.OrdinalIgnoreCase ) || text.Contains( "not found", StringComparison.OrdinalIgnoreCase ) )
return "Backend route missing or unavailable. Update the backend or retry against the correct target.";
return message ?? "";
}
private static string FormatJson( JsonElement raw )
{
if ( raw.ValueKind == JsonValueKind.Undefined )
return "(no response data)";
try
{
return JsonSerializer.Serialize( raw, new JsonSerializerOptions { WriteIndented = true } );
}
catch
{
return raw.GetRawText();
}
}
protected override void OnPaint()
{
base.OnPaint();
var pad = 20f;
var w = Width - pad * 2;
var y = 20f;
// Title
Paint.SetDefaultFont( size: 14, weight: 700 );
Paint.SetPen( new Color( 1f, 0.3f, 0.3f ) );
Paint.DrawText( new Rect( pad, y, w, 24 ), "⚠ Endpoint Error", TextFlag.LeftCenter );
y += 32;
// Endpoint info
Paint.SetDefaultFont( size: 11, weight: 600 );
Paint.SetPen( Color.White.WithAlpha( 0.5f ) );
Paint.DrawText( new Rect( pad, y, 80, 18 ), "Endpoint:", TextFlag.LeftCenter );
Paint.SetPen( Color.White.WithAlpha( 0.9f ) );
Paint.DrawText( new Rect( pad + 80, y, w - 80, 18 ), _slug, TextFlag.LeftCenter );
y += 22;
Paint.SetPen( Color.White.WithAlpha( 0.5f ) );
Paint.DrawText( new Rect( pad, y, 80, 18 ), "Status:", TextFlag.LeftCenter );
Paint.SetPen( new Color( 1f, 0.5f, 0.3f ) );
Paint.DrawText( new Rect( pad + 80, y, w - 80, 18 ), _code, TextFlag.LeftCenter );
y += 22;
if ( !string.IsNullOrWhiteSpace( _message ) )
{
Paint.SetPen( Color.White.WithAlpha( 0.5f ) );
Paint.DrawText( new Rect( pad, y, 80, 18 ), "Message:", TextFlag.LeftCenter );
Paint.SetPen( Color.White.WithAlpha( 0.85f ) );
var msgLines = WrapText( _message, w - 80, 10 );
foreach ( var line in msgLines )
{
Paint.DrawText( new Rect( pad + 80, y, w - 80, 18 ), line, TextFlag.LeftCenter );
y += 16;
}
y += 4;
}
y += 8;
// Response header
Paint.SetDefaultFont( size: 11, weight: 700 );
Paint.SetPen( Color.White.WithAlpha( 0.6f ) );
Paint.DrawText( new Rect( pad, y, w, 18 ), "Technical details:", TextFlag.LeftCenter );
y += 22;
// JSON box
var jsonBoxTop = y;
var jsonBoxHeight = Height - y - 70;
var jsonBoxRect = new Rect( pad, jsonBoxTop, w, jsonBoxHeight );
Paint.SetBrush( Color.Black.WithAlpha( 0.3f ) );
Paint.SetPen( Color.White.WithAlpha( 0.1f ) );
Paint.DrawRect( jsonBoxRect, 4 );
// Clip and draw JSON
Paint.SetDefaultFont( size: 9 );
Paint.SetPen( new Color( 1f, 0.6f, 0.3f ).WithAlpha( 0.9f ) );
var jsonY = jsonBoxTop + 8 - _scrollY;
var lineHeight = 14f;
var lines = _rawJson.Split( '\n' );
foreach ( var line in lines )
{
if ( jsonY >= jsonBoxTop - lineHeight && jsonY < jsonBoxTop + jsonBoxHeight )
{
Paint.DrawText( new Rect( pad + 10, jsonY, w - 20, lineHeight ), line, TextFlag.LeftCenter );
}
jsonY += lineHeight;
}
_contentHeight = lines.Length * lineHeight + 16;
// Buttons
y = Height - pad - 32;
var btnW = 100f;
var btnH = 32f;
// Copy button
_copyRect = new Rect( pad, y, btnW, btnH );
var copyHovered = _copyRect.IsInside( _mousePos );
Paint.SetBrush( Color.White.WithAlpha( copyHovered ? 0.12f : 0.06f ) );
Paint.SetPen( Color.White.WithAlpha( copyHovered ? 0.35f : 0.2f ) );
Paint.DrawRect( _copyRect, 4 );
Paint.SetDefaultFont( size: 11, weight: 600 );
Paint.SetPen( Color.White.WithAlpha( copyHovered ? 0.95f : 0.75f ) );
Paint.DrawText( _copyRect, _copied ? "Copied!" : "📋 Copy", TextFlag.Center );
// Close button
_closeRect = new Rect( Width - pad - btnW, y, btnW, btnH );
var closeHovered = _closeRect.IsInside( _mousePos );
Paint.SetBrush( Color.White.WithAlpha( closeHovered ? 0.12f : 0.06f ) );
Paint.SetPen( Color.White.WithAlpha( closeHovered ? 0.35f : 0.2f ) );
Paint.DrawRect( _closeRect, 4 );
Paint.SetPen( Color.White.WithAlpha( closeHovered ? 0.95f : 0.75f ) );
Paint.DrawText( _closeRect, "Dismiss", TextFlag.Center );
// Reset copied state after a bit
if ( _copied )
{
_copiedTimer += 0.016f;
if ( _copiedTimer > 2f )
{
_copied = false;
_copiedTimer = 0f;
}
}
}
private static string[] WrapText( string text, float width, int fontSize )
{
var result = new System.Collections.Generic.List<string>();
var charWidth = fontSize * 0.6f;
var maxChars = (int)(width / charWidth);
foreach ( var paragraph in text.Split( '\n' ) )
{
if ( paragraph.Length <= maxChars )
{
result.Add( paragraph );
continue;
}
var remaining = paragraph;
while ( remaining.Length > maxChars )
{
var breakAt = remaining.LastIndexOf( ' ', maxChars );
if ( breakAt <= 0 ) breakAt = maxChars;
result.Add( remaining[..breakAt] );
remaining = remaining[breakAt..].TrimStart();
}
if ( remaining.Length > 0 )
result.Add( remaining );
}
return result.ToArray();
}
protected override void OnMousePress( MouseEvent e )
{
base.OnMousePress( e );
if ( _copyRect.IsInside( e.LocalPosition ) )
{
var copyText = $"[{_slug}] {_code}: {_message}\n\n{_rawJson}";
var tmpPath = Path.Combine( Path.GetTempPath(), "ns_endpoint_error.json" );
File.WriteAllText( tmpPath, copyText );
EditorUtility.OpenFile( tmpPath );
_copied = true;
_copiedTimer = 0f;
Update();
}
if ( _closeRect.IsInside( e.LocalPosition ) )
Close();
}
protected override void OnMouseMove( MouseEvent e )
{
base.OnMouseMove( e );
_mousePos = e.LocalPosition;
Update();
}
protected override void OnMouseWheel( WheelEvent e )
{
base.OnMouseWheel( e );
_scrollY = Math.Clamp( _scrollY - e.Delta * 30, 0, Math.Max( 0, _contentHeight - 200 ) );
Update();
}
}