Skip to content

Commit 499c46a

Browse files
ndbroadbentclaude
andcommitted
Add debug panel with custom host URL and headers support
- Hidden debug panel activated by 5 clicks on logo (each within 1s of previous) - Custom API host URL input with dev/staging/prod shortcut links - Custom headers UI with +/- buttons for adding name/value pairs - Settings saved to localStorage and applied to Rust API client - Error message sanitization to remove HTML and escaped bytes from API errors - Tauri dev config workaround for v2 bug #9629 (devUrl not used) - tippy.js tooltips for permission badges - Extracted debug panel code to src/debug.ts to keep main.ts under 500 lines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 49e3fad commit 499c46a

File tree

10 files changed

+627
-29
lines changed

10 files changed

+627
-29
lines changed

Taskfile.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@ tasks:
77
- task --list
88

99
# === Development ===
10+
# Note: --config tauri.dev.conf.json works around Tauri v2 bug where dev mode
11+
# uses frontendDist instead of devUrl. See: https://github.com/tauri-apps/tauri/issues/9629
1012
dev:
1113
desc: Start Tauri dev mode (uses production API)
1214
cmds:
13-
- cargo tauri dev
15+
- cargo tauri dev --config src-tauri/tauri.dev.conf.json
1416

1517
dev:local:
1618
desc: Start Tauri dev mode (uses localhost:5173 API)
1719
cmds:
18-
- cargo tauri dev --features desktop,dev-server
20+
- cargo tauri dev --config src-tauri/tauri.dev.conf.json --features desktop,dev-server
1921

2022
build:
2123
desc: Build for production (points to chattomap.com)

bun.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"@tauri-apps/api": "^2.9.1",
88
"@tauri-apps/plugin-dialog": "^2.4.2",
99
"@tauri-apps/plugin-shell": "^2.3.3",
10+
"tippy.js": "^6.3.7",
1011
},
1112
"devDependencies": {
1213
"@biomejs/biome": "^1.9.4",
@@ -116,6 +117,8 @@
116117

117118
"@nodelib/fs.walk": ["@nodelib/[email protected]", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
118119

120+
"@popperjs/core": ["@popperjs/[email protected]", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="],
121+
119122
"@rollup/rollup-android-arm-eabi": ["@rollup/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ=="],
120123

121124
"@rollup/rollup-android-arm64": ["@rollup/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw=="],
@@ -488,6 +491,8 @@
488491

489492
"tinyspy": ["[email protected]", "", {}, "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q=="],
490493

494+
"tippy.js": ["[email protected]", "", { "dependencies": { "@popperjs/core": "^2.9.0" } }, "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ=="],
495+
491496
"to-regex-range": ["[email protected]", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
492497

493498
"token-stream": ["[email protected]", "", {}, "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg=="],

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"dependencies": {
2727
"@tauri-apps/api": "^2.9.1",
2828
"@tauri-apps/plugin-dialog": "^2.4.2",
29-
"@tauri-apps/plugin-shell": "^2.3.3"
29+
"@tauri-apps/plugin-shell": "^2.3.3",
30+
"tippy.js": "^6.3.7"
3031
}
3132
}

src-tauri/src/main.rs

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,13 @@ struct Args {
3939
output_dir: PathBuf,
4040
}
4141

42-
/// App state for screenshot configuration
42+
/// App state for screenshot configuration and debug settings
4343
struct AppState {
4444
screenshot_config: Mutex<ScreenshotConfig>,
45+
/// Override server host URL (for debugging)
46+
server_host_override: Mutex<Option<String>>,
47+
/// Custom headers to send with API requests (for debugging)
48+
custom_headers: Mutex<std::collections::HashMap<String, String>>,
4549
}
4650

4751
/// Export result returned to the frontend
@@ -81,8 +85,13 @@ fn validate_chat_db(path: String) -> bool {
8185
async fn export_and_upload(
8286
chat_ids: Vec<i32>,
8387
custom_db_path: Option<String>,
88+
state: tauri::State<'_, AppState>,
8489
window: tauri::Window,
8590
) -> Result<ExportResult, String> {
91+
// Get the host override if set
92+
let host_override = state.server_host_override.lock().unwrap().clone();
93+
// Get custom headers if set
94+
let custom_headers = state.custom_headers.lock().unwrap().clone();
8695
// Helper to emit progress
8796
let emit = |stage: &str, percent: u8, message: &str| {
8897
let _ = window.emit(
@@ -123,7 +132,7 @@ async fn export_and_upload(
123132
// Stage 2: Get pre-signed URL (50-55%)
124133
emit("Uploading", 50, "Preparing upload...");
125134

126-
let presign_response = get_presigned_url()
135+
let presign_response = get_presigned_url(host_override.as_deref(), &custom_headers)
127136
.await
128137
.map_err(|e| format!("Failed to get upload URL: {e}"))?;
129138

@@ -155,12 +164,16 @@ async fn export_and_upload(
155164
// Stage 4: Complete upload and start processing (90-95%)
156165
emit("Processing", 90, "Starting processing...");
157166

158-
let job_response = complete_upload(&presign_response.job_id)
159-
.await
160-
.map_err(|e| format!("Failed to start processing: {e}"))?;
167+
let job_response = complete_upload(
168+
&presign_response.job_id,
169+
host_override.as_deref(),
170+
&custom_headers,
171+
)
172+
.await
173+
.map_err(|e| format!("Failed to start processing: {e}"))?;
161174

162175
// Stage 5: Complete (95-100%)
163-
let results_url = get_results_url(&job_response.job_id);
176+
let results_url = get_results_url(&job_response.job_id, host_override.as_deref());
164177
emit("Complete", 100, "Export complete!");
165178

166179
// Open browser to results page
@@ -328,6 +341,35 @@ fn open_licenses() -> Result<(), String> {
328341
.map_err(|e| format!("Failed to open URL: {e}"))
329342
}
330343

344+
/// Set the server host URL override (for debugging)
345+
#[tauri::command]
346+
fn set_server_host(state: tauri::State<AppState>, host: Option<String>) {
347+
let mut override_host = state.server_host_override.lock().unwrap();
348+
eprintln!("[set_server_host] Setting host override to: {:?}", host);
349+
*override_host = host;
350+
}
351+
352+
/// Get the current server host URL (with override applied)
353+
#[tauri::command]
354+
fn get_server_host(state: tauri::State<AppState>) -> String {
355+
let override_host = state.server_host_override.lock().unwrap();
356+
match override_host.as_ref() {
357+
Some(host) if !host.is_empty() => host.clone(),
358+
_ => chat_to_map_desktop::upload::SERVER_BASE_URL.to_string(),
359+
}
360+
}
361+
362+
/// Set custom headers for API requests (for debugging)
363+
#[tauri::command]
364+
fn set_custom_headers(
365+
state: tauri::State<AppState>,
366+
headers: std::collections::HashMap<String, String>,
367+
) {
368+
let mut custom_headers = state.custom_headers.lock().unwrap();
369+
eprintln!("[set_custom_headers] Setting {} headers", headers.len());
370+
*custom_headers = headers;
371+
}
372+
331373
/// Take a screenshot and save it to the specified filename
332374
#[tauri::command]
333375
fn take_screenshot(state: tauri::State<AppState>, filename: String) -> Result<String, String> {
@@ -363,6 +405,8 @@ fn main() {
363405

364406
let app_state = AppState {
365407
screenshot_config: Mutex::new(screenshot_config),
408+
server_host_override: Mutex::new(None),
409+
custom_headers: Mutex::new(std::collections::HashMap::new()),
366410
};
367411

368412
tauri::Builder::default()
@@ -403,6 +447,9 @@ fn main() {
403447
get_screenshot_config,
404448
take_screenshot,
405449
open_licenses,
450+
set_server_host,
451+
get_server_host,
452+
set_custom_headers,
406453
])
407454
.run(tauri::generate_context!())
408455
.expect("error while running tauri application");

src-tauri/src/upload.rs

Lines changed: 107 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Uses auto-generated API client from OpenAPI spec for type safety.
55
*/
66

7-
use std::{fs::File, io::Read, path::Path};
7+
use std::{collections::HashMap, fs::File, io::Read, path::Path};
88

99
use crate::api::{types, Client, ClientUploadExt};
1010

@@ -44,20 +44,46 @@ pub const SERVER_BASE_URL: &str = "https://chattomap.com";
4444
// Upload Implementation
4545
// =============================================================================
4646

47-
/// Create API client
48-
fn create_client() -> Client {
49-
Client::new(SERVER_BASE_URL)
47+
/// Create API client with optional host override and custom headers
48+
fn create_client(host_override: Option<&str>, custom_headers: &HashMap<String, String>) -> Client {
49+
let base_url = host_override.unwrap_or(SERVER_BASE_URL);
50+
51+
if custom_headers.is_empty() {
52+
return Client::new(base_url);
53+
}
54+
55+
// Build a reqwest client with custom headers
56+
let mut headers = reqwest::header::HeaderMap::new();
57+
for (name, value) in custom_headers {
58+
if let (Ok(header_name), Ok(header_value)) = (
59+
reqwest::header::HeaderName::from_bytes(name.as_bytes()),
60+
reqwest::header::HeaderValue::from_str(value),
61+
) {
62+
headers.insert(header_name, header_value);
63+
}
64+
}
65+
66+
let http_client = reqwest::Client::builder()
67+
.default_headers(headers)
68+
.build()
69+
.unwrap_or_default();
70+
71+
Client::new_with_client(base_url, http_client)
5072
}
5173

5274
/// Request a pre-signed upload URL from the server
53-
pub async fn get_presigned_url() -> Result<PresignResponse, String> {
54-
let client = create_client();
55-
56-
let response = client
57-
.upload_presign()
58-
.send()
59-
.await
60-
.map_err(|e| format!("Failed to request presigned URL: {e}"))?;
75+
pub async fn get_presigned_url(
76+
host_override: Option<&str>,
77+
custom_headers: &HashMap<String, String>,
78+
) -> Result<PresignResponse, String> {
79+
let client = create_client(host_override, custom_headers);
80+
81+
let response = client.upload_presign().send().await.map_err(|e| {
82+
format!(
83+
"Failed to request presigned URL: {}",
84+
sanitize_api_error(&e)
85+
)
86+
})?;
6187

6288
let body = response.into_inner();
6389

@@ -121,8 +147,12 @@ pub async fn upload_file(
121147
}
122148

123149
/// Notify server that upload is complete and start processing
124-
pub async fn complete_upload(job_id: &str) -> Result<CreateJobResponse, String> {
125-
let client = create_client();
150+
pub async fn complete_upload(
151+
job_id: &str,
152+
host_override: Option<&str>,
153+
custom_headers: &HashMap<String, String>,
154+
) -> Result<CreateJobResponse, String> {
155+
let client = create_client(host_override, custom_headers);
126156

127157
let response = client
128158
.upload_complete()
@@ -131,7 +161,7 @@ pub async fn complete_upload(job_id: &str) -> Result<CreateJobResponse, String>
131161
})
132162
.send()
133163
.await
134-
.map_err(|e| format!("Failed to complete upload: {e}"))?;
164+
.map_err(|e| format!("Failed to complete upload: {}", sanitize_api_error(&e)))?;
135165

136166
let body = response.into_inner();
137167

@@ -146,14 +176,67 @@ pub async fn complete_upload(job_id: &str) -> Result<CreateJobResponse, String>
146176
}
147177

148178
/// Get the results page URL for a job
149-
pub fn get_results_url(job_id: &str) -> String {
150-
format!("{}/processing/{}", SERVER_BASE_URL, job_id)
179+
pub fn get_results_url(job_id: &str, host_override: Option<&str>) -> String {
180+
let base_url = host_override.unwrap_or(SERVER_BASE_URL);
181+
format!("{}/processing/{}", base_url, job_id)
151182
}
152183

153184
// =============================================================================
154185
// Helper Functions
155186
// =============================================================================
156187

188+
/// Sanitize API client errors (from progenitor)
189+
/// Extracts meaningful message from potentially HTML-heavy error responses
190+
fn sanitize_api_error<E: std::fmt::Display>(error: &E) -> String {
191+
let error_str = error.to_string();
192+
193+
// Check if error contains HTML
194+
if error_str.contains("<!DOCTYPE")
195+
|| error_str.contains("<!doctype")
196+
|| error_str.contains("<html")
197+
{
198+
// Try to extract a title from the HTML
199+
if let Some(title) = extract_html_title(&error_str) {
200+
return clean_error_text(&title);
201+
}
202+
return "Server returned an HTML error page (authentication required?)".to_string();
203+
}
204+
205+
// For shorter errors, return as-is
206+
if error_str.len() <= 200 {
207+
return error_str;
208+
}
209+
210+
// Truncate long errors
211+
format!("{}...", &error_str[..200])
212+
}
213+
214+
/// Clean up error text - remove escaped bytes and non-ASCII characters
215+
fn clean_error_text(text: &str) -> String {
216+
// Remove escaped byte sequences like \xe3\x83\xbb
217+
let mut result = text.to_string();
218+
219+
// Remove \xNN patterns
220+
while let Some(pos) = result.find("\\x") {
221+
if pos + 4 <= result.len() {
222+
result = format!("{}{}", &result[..pos], &result[pos + 4..]);
223+
} else {
224+
break;
225+
}
226+
}
227+
228+
// Remove any remaining non-ASCII and normalize whitespace
229+
result
230+
.chars()
231+
.filter(|c| {
232+
c.is_ascii_alphanumeric() || c.is_ascii_whitespace() || c.is_ascii_punctuation()
233+
})
234+
.collect::<String>()
235+
.split_whitespace()
236+
.collect::<Vec<_>>()
237+
.join(" ")
238+
}
239+
157240
/// Sanitize an error response body for display
158241
///
159242
/// If the body looks like HTML, extract a meaningful message or return a generic error.
@@ -247,11 +330,17 @@ mod tests {
247330

248331
#[test]
249332
fn test_get_results_url() {
250-
let url = get_results_url("abc123");
333+
let url = get_results_url("abc123", None);
251334
assert!(url.contains("abc123"));
252335
assert!(url.contains("/processing/"));
253336
}
254337

338+
#[test]
339+
fn test_get_results_url_with_override() {
340+
let url = get_results_url("abc123", Some("http://localhost:5173"));
341+
assert_eq!(url, "http://localhost:5173/processing/abc123");
342+
}
343+
255344
#[test]
256345
fn test_sanitize_error_body_empty() {
257346
assert_eq!(sanitize_error_body(""), "(empty response)");

src-tauri/tauri.dev.conf.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"$schema": "https://schema.tauri.app/config/2",
3+
"build": {
4+
"frontendDist": "http://localhost:1420"
5+
}
6+
}

0 commit comments

Comments
 (0)