tangled
alpha
login
or
join now
nonbinary.computer
/
weaver
atproto blogging
24
fork
atom
overview
issues
2
pulls
pipelines
bug reporting
Orual
2 months ago
c959a17f
2f2ed57a
+500
-3
8 changed files
expand all
collapse all
unified
split
Cargo.lock
crates
weaver-app
Cargo.toml
assets
styling
editor.css
src
components
editor
log_buffer.rs
mod.rs
report.rs
main.rs
views
editor.rs
+3
Cargo.lock
···
9964
"time",
9965
"tokio",
9966
"tracing",
0
0
0
9967
"wasm-bindgen",
9968
"wasm-bindgen-futures",
9969
"weaver-api",
···
9964
"time",
9965
"tokio",
9966
"tracing",
9967
+
"tracing-subscriber",
9968
+
"tracing-wasm",
9969
+
"urlencoding",
9970
"wasm-bindgen",
9971
"wasm-bindgen-futures",
9972
"weaver-api",
+3
crates/weaver-app/Cargo.toml
···
52
loro = "1.9.1"
53
markdown-weaver-escape = { workspace = true }
54
web-time = "1.1"
0
0
55
56
[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies]
57
webbrowser = "1.0.6"
···
59
60
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
61
reqwest = { version = "0.12", default-features = false, features = ["json"] }
0
62
#sqlite-wasm-rs = { version = "0.4", default-features = false, features = ["precompiled", "relaxed-idb"] }
63
time = { version = "0.3", features = ["wasm-bindgen"] }
64
console_error_panic_hook = "0.1"
···
52
loro = "1.9.1"
53
markdown-weaver-escape = { workspace = true }
54
web-time = "1.1"
55
+
urlencoding = "2.1"
56
+
tracing-subscriber = { version = "0.3", default-features = false, features = ["std", "registry"] }
57
58
[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies]
59
webbrowser = "1.0.6"
···
61
62
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
63
reqwest = { version = "0.12", default-features = false, features = ["json"] }
64
+
tracing-wasm = "0.2"
65
#sqlite-wasm-rs = { version = "0.4", default-features = false, features = ["precompiled", "relaxed-idb"] }
66
time = { version = "0.3", features = ["wasm-bindgen"] }
67
console_error_panic_hook = "0.1"
+144
crates/weaver-app/assets/styling/editor.css
···
116
line-height: 1em;
117
vertical-align: baseline;
118
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
116
line-height: 1em;
117
vertical-align: baseline;
118
}
119
+
120
+
/* Editor page header with report button */
121
+
.editor-header {
122
+
display: flex;
123
+
justify-content: space-between;
124
+
align-items: center;
125
+
padding: 1rem 6rem;
126
+
background: var(--color-base);
127
+
}
128
+
129
+
.editor-header h1 {
130
+
margin: 0;
131
+
color: var(--color-text);
132
+
}
133
+
134
+
/* Bug report button and dialog */
135
+
.report-bug-button {
136
+
padding: 0.5rem 1rem;
137
+
background: var(--color-surface);
138
+
border: 1px solid var(--color-border);
139
+
border-radius: 4px;
140
+
color: var(--color-text);
141
+
cursor: pointer;
142
+
margin-right: 3.5rem;
143
+
font-size: 0.9rem;
144
+
font-family: var(--font-body);
145
+
}
146
+
147
+
.report-bug-button:hover {
148
+
background: var(--color-overlay);
149
+
}
150
+
151
+
.report-dialog-overlay {
152
+
position: fixed;
153
+
top: 0;
154
+
left: 0;
155
+
right: 0;
156
+
bottom: 0;
157
+
background: rgba(0, 0, 0, 0.6);
158
+
display: flex;
159
+
align-items: center;
160
+
justify-content: center;
161
+
z-index: 1000;
162
+
}
163
+
164
+
.report-dialog {
165
+
background: var(--color-surface);
166
+
border: 1px solid var(--color-border);
167
+
border-radius: 8px;
168
+
padding: 1.5rem;
169
+
max-width: 1000px;
170
+
width: 90%;
171
+
max-height: 80vh;
172
+
overflow-y: auto;
173
+
color: var(--color-text);
174
+
}
175
+
176
+
.report-dialog h2 {
177
+
margin: 0 0 1rem 0;
178
+
color: var(--color-emphasis);
179
+
}
180
+
181
+
.report-section {
182
+
margin-bottom: 1rem;
183
+
}
184
+
185
+
.report-section label {
186
+
display: block;
187
+
margin-bottom: 0.5rem;
188
+
font-weight: 500;
189
+
color: var(--color-text);
190
+
}
191
+
192
+
.report-section h4 {
193
+
margin: 0.5rem 0;
194
+
color: var(--color-subtle);
195
+
}
196
+
197
+
.report-section pre {
198
+
background: var(--color-base);
199
+
padding: 1rem;
200
+
border-radius: 4px;
201
+
overflow-x: auto;
202
+
font-size: 0.8rem;
203
+
font-family: var(--font-mono);
204
+
overflow-y: auto;
205
+
color: var(--color-text);
206
+
}
207
+
208
+
.report-comment {
209
+
width: 100%;
210
+
padding: 1rem;
211
+
border: 1px solid var(--color-border);
212
+
border-radius: 4px;
213
+
background: var(--color-base);
214
+
color: var(--color-text);
215
+
font-family: var(--font-body);
216
+
resize: vertical;
217
+
}
218
+
219
+
.report-details {
220
+
margin: 1rem 0;
221
+
}
222
+
223
+
.report-details summary {
224
+
cursor: pointer;
225
+
color: var(--color-muted);
226
+
}
227
+
228
+
.report-actions {
229
+
display: flex;
230
+
gap: 1rem;
231
+
justify-content: flex-end;
232
+
margin-top: 1rem;
233
+
}
234
+
235
+
.report-cancel {
236
+
padding: 0.5rem 1rem;
237
+
background: transparent;
238
+
border: 1px solid var(--color-border);
239
+
border-radius: 4px;
240
+
color: var(--color-text);
241
+
cursor: pointer;
242
+
font-family: var(--font-body);
243
+
}
244
+
245
+
.report-cancel:hover {
246
+
background: var(--color-overlay);
247
+
}
248
+
249
+
.report-submit {
250
+
padding: 0.5rem 1rem;
251
+
background: var(--color-primary);
252
+
border: none;
253
+
border-radius: 4px;
254
+
color: var(--color-base);
255
+
cursor: pointer;
256
+
font-weight: 500;
257
+
font-family: var(--font-body);
258
+
}
259
+
260
+
.report-submit:hover {
261
+
opacity: 0.9;
262
+
}
+110
crates/weaver-app/src/components/editor/log_buffer.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
//! Ring buffer log capture for bug reports.
2
+
//!
3
+
//! Provides a tracing Layer that captures recent log messages to a fixed-size
4
+
//! ring buffer. Logs can be retrieved on-demand for bug reports.
5
+
6
+
use std::cell::RefCell;
7
+
use std::collections::VecDeque;
8
+
use std::fmt::Write as FmtWrite;
9
+
10
+
use tracing::field::{Field, Visit};
11
+
use tracing::{Event, Level, Subscriber};
12
+
use tracing_subscriber::layer::Context;
13
+
use tracing_subscriber::Layer;
14
+
15
+
/// Maximum number of log entries to keep.
16
+
const MAX_ENTRIES: usize = 100;
17
+
18
+
/// Module prefixes to capture in the ring buffer.
19
+
const CAPTURED_PREFIXES: &[&str] = &["weaver_", "markdown_weaver"];
20
+
21
+
thread_local! {
22
+
static LOG_BUFFER: RefCell<VecDeque<String>> = RefCell::new(VecDeque::with_capacity(MAX_ENTRIES));
23
+
}
24
+
25
+
/// Minimum level to buffer from our modules.
26
+
const BUFFER_MIN_LEVEL: Level = Level::DEBUG;
27
+
28
+
/// A tracing Layer that captures log messages to a ring buffer.
29
+
/// Console output is handled by WASMLayer in the subscriber stack.
30
+
pub struct LogCaptureLayer;
31
+
32
+
impl<S: Subscriber> Layer<S> for LogCaptureLayer {
33
+
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
34
+
let metadata = event.metadata();
35
+
let level = metadata.level();
36
+
let target = metadata.target();
37
+
38
+
// Only buffer debug+ logs from our modules
39
+
let is_our_module = CAPTURED_PREFIXES.iter().any(|prefix| target.starts_with(prefix));
40
+
if !is_our_module || *level > BUFFER_MIN_LEVEL {
41
+
return;
42
+
}
43
+
44
+
// Format the log entry
45
+
let mut message = String::new();
46
+
let mut visitor = MessageVisitor(&mut message);
47
+
event.record(&mut visitor);
48
+
49
+
let formatted = format!("[{}] {}: {}", level_str(level), target, message);
50
+
51
+
LOG_BUFFER.with(|buf| {
52
+
let mut buf = buf.borrow_mut();
53
+
if buf.len() >= MAX_ENTRIES {
54
+
buf.pop_front();
55
+
}
56
+
buf.push_back(formatted);
57
+
});
58
+
}
59
+
}
60
+
61
+
/// Visitor that extracts the message field from a tracing event.
62
+
struct MessageVisitor<'a>(&'a mut String);
63
+
64
+
impl Visit for MessageVisitor<'_> {
65
+
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
66
+
if field.name() == "message" {
67
+
let _ = write!(self.0, "{:?}", value);
68
+
} else {
69
+
if !self.0.is_empty() {
70
+
self.0.push_str(", ");
71
+
}
72
+
let _ = write!(self.0, "{}={:?}", field.name(), value);
73
+
}
74
+
}
75
+
76
+
fn record_str(&mut self, field: &Field, value: &str) {
77
+
if field.name() == "message" {
78
+
self.0.push_str(value);
79
+
} else {
80
+
if !self.0.is_empty() {
81
+
self.0.push_str(", ");
82
+
}
83
+
let _ = write!(self.0, "{}={}", field.name(), value);
84
+
}
85
+
}
86
+
}
87
+
88
+
fn level_str(level: &Level) -> &'static str {
89
+
match *level {
90
+
Level::ERROR => "ERROR",
91
+
Level::WARN => "WARN",
92
+
Level::INFO => "INFO",
93
+
Level::DEBUG => "DEBUG",
94
+
Level::TRACE => "TRACE",
95
+
}
96
+
}
97
+
98
+
/// Get all captured log entries as a single string.
99
+
pub fn get_logs() -> String {
100
+
LOG_BUFFER.with(|buf| {
101
+
let buf = buf.borrow();
102
+
buf.iter().cloned().collect::<Vec<_>>().join("\n")
103
+
})
104
+
}
105
+
106
+
/// Clear the log buffer.
107
+
#[allow(dead_code)]
108
+
pub fn clear_logs() {
109
+
LOG_BUFFER.with(|buf| buf.borrow_mut().clear());
110
+
}
+4
crates/weaver-app/src/components/editor/mod.rs
···
7
mod cursor;
8
mod document;
9
mod formatting;
0
10
mod offset_map;
11
mod paragraph;
12
mod platform;
13
mod render;
0
14
mod storage;
15
mod toolbar;
16
mod visibility;
···
28
pub use toolbar::EditorToolbar;
29
pub use visibility::VisibilityState;
30
pub use writer::{SyntaxSpanInfo, SyntaxType, WriterResult};
0
0
31
32
use dioxus::prelude::*;
33
···
7
mod cursor;
8
mod document;
9
mod formatting;
10
+
mod log_buffer;
11
mod offset_map;
12
mod paragraph;
13
mod platform;
14
mod render;
15
+
mod report;
16
mod storage;
17
mod toolbar;
18
mod visibility;
···
30
pub use toolbar::EditorToolbar;
31
pub use visibility::VisibilityState;
32
pub use writer::{SyntaxSpanInfo, SyntaxType, WriterResult};
33
+
pub use report::ReportButton;
34
+
pub use log_buffer::LogCaptureLayer;
35
36
use dioxus::prelude::*;
37
+199
crates/weaver-app/src/components/editor/report.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
//! Bug report dialog for the markdown editor.
2
+
//!
3
+
//! Captures editor state, DOM, and platform info for bug reports.
4
+
//! All capture happens on-demand when the report button is clicked.
5
+
6
+
use dioxus::prelude::*;
7
+
8
+
use super::log_buffer;
9
+
use super::storage::load_from_storage;
10
+
11
+
/// Captured report data.
12
+
#[derive(Clone, Default)]
13
+
struct ReportData {
14
+
editor_text: String,
15
+
dom_html: String,
16
+
platform_info: String,
17
+
recent_logs: String,
18
+
}
19
+
20
+
impl ReportData {
21
+
/// Capture current state from DOM and LocalStorage.
22
+
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
23
+
fn capture(editor_id: &str) -> Self {
24
+
let dom_html = web_sys::window()
25
+
.and_then(|w| w.document())
26
+
.and_then(|d| d.get_element_by_id(editor_id))
27
+
.map(|e| e.outer_html())
28
+
.unwrap_or_default();
29
+
30
+
let editor_text = load_from_storage()
31
+
.map(|snapshot| snapshot.to_string())
32
+
.unwrap_or_default();
33
+
34
+
let platform_info = {
35
+
let plat = super::platform::platform();
36
+
format!(
37
+
"iOS: {}, Android: {}, Safari: {}, Chrome: {}, Firefox: {}, Mobile: {}\n\
38
+
User Agent: {}",
39
+
plat.ios,
40
+
plat.android,
41
+
plat.safari,
42
+
plat.chrome,
43
+
plat.gecko,
44
+
plat.mobile,
45
+
web_sys::window()
46
+
.and_then(|w| w.navigator().user_agent().ok())
47
+
.unwrap_or_default()
48
+
)
49
+
};
50
+
51
+
let recent_logs = log_buffer::get_logs();
52
+
53
+
Self {
54
+
editor_text,
55
+
dom_html,
56
+
platform_info,
57
+
recent_logs,
58
+
}
59
+
}
60
+
61
+
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
62
+
fn capture(_editor_id: &str) -> Self {
63
+
Self::default()
64
+
}
65
+
66
+
/// Generate mailto URL with report data.
67
+
fn to_mailto(&self, email: &str, comment: &str) -> String {
68
+
let subject = "Weaver Editor Bug Report";
69
+
70
+
let body = format!(
71
+
"## Bug Report\n\n\
72
+
### Comment\n{}\n\n\
73
+
### Platform Info\n```\n{}\n```\n\n\
74
+
### Recent Logs\n```\n{}\n```\n\n\
75
+
### Editor Text\n```markdown\n{}\n```\n\n\
76
+
### DOM State\n```html\n{}\n```",
77
+
comment, self.platform_info, self.recent_logs, self.editor_text, self.dom_html
78
+
);
79
+
80
+
let encoded_subject = urlencoding::encode(subject);
81
+
let encoded_body = urlencoding::encode(&body);
82
+
83
+
format!(
84
+
"mailto:{}?subject={}&body={}",
85
+
email, encoded_subject, encoded_body
86
+
)
87
+
}
88
+
}
89
+
90
+
/// Props for the bug report button.
91
+
#[derive(Props, Clone, PartialEq)]
92
+
pub struct ReportButtonProps {
93
+
/// Email address to send reports to.
94
+
pub email: String,
95
+
/// Editor element ID for DOM capture.
96
+
pub editor_id: String,
97
+
}
98
+
99
+
/// Bug report button and dialog.
100
+
#[component]
101
+
pub fn ReportButton(props: ReportButtonProps) -> Element {
102
+
let mut show_dialog = use_signal(|| false);
103
+
let mut comment = use_signal(String::new);
104
+
let mut report_data = use_signal(ReportData::default);
105
+
106
+
let editor_id = props.editor_id.clone();
107
+
let capture_state = move |_| {
108
+
report_data.set(ReportData::capture(&editor_id));
109
+
show_dialog.set(true);
110
+
};
111
+
112
+
let email = props.email.clone();
113
+
let submit_report = move |_| {
114
+
let data = report_data();
115
+
let mailto_url = data.to_mailto(&email, &comment());
116
+
117
+
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
118
+
if let Some(window) = web_sys::window() {
119
+
let _ = window.open_with_url(&mailto_url);
120
+
}
121
+
122
+
show_dialog.set(false);
123
+
comment.set(String::new());
124
+
};
125
+
126
+
let close_dialog = move |_| {
127
+
show_dialog.set(false);
128
+
};
129
+
130
+
rsx! {
131
+
button {
132
+
class: "report-bug-button",
133
+
onclick: capture_state,
134
+
"Report Bug"
135
+
}
136
+
137
+
if show_dialog() {
138
+
div {
139
+
class: "report-dialog-overlay",
140
+
onclick: close_dialog,
141
+
142
+
div {
143
+
class: "report-dialog",
144
+
onclick: move |e| e.stop_propagation(),
145
+
146
+
h2 { "Report a Bug" }
147
+
148
+
div { class: "report-section",
149
+
label { "Describe the issue:" }
150
+
textarea {
151
+
class: "report-comment",
152
+
placeholder: "What happened? What did you expect?",
153
+
value: "{comment}",
154
+
oninput: move |e| comment.set(e.value()),
155
+
rows: "4",
156
+
}
157
+
}
158
+
159
+
details { class: "report-details",
160
+
summary { "Captured Data (click to expand)" }
161
+
162
+
div { class: "report-section",
163
+
h4 { "Platform" }
164
+
pre { "{report_data().platform_info}" }
165
+
}
166
+
167
+
div { class: "report-section",
168
+
h4 { "Recent Logs" }
169
+
pre { "{report_data().recent_logs}" }
170
+
}
171
+
172
+
div { class: "report-section",
173
+
h4 { "Editor Text" }
174
+
pre { "{report_data().editor_text}" }
175
+
}
176
+
177
+
div { class: "report-section",
178
+
h4 { "DOM HTML" }
179
+
pre { "{report_data().dom_html}" }
180
+
}
181
+
}
182
+
183
+
div { class: "report-actions",
184
+
button {
185
+
class: "report-cancel",
186
+
onclick: close_dialog,
187
+
"Cancel"
188
+
}
189
+
button {
190
+
class: "report-submit",
191
+
onclick: submit_report,
192
+
"Open Email"
193
+
}
194
+
}
195
+
}
196
+
}
197
+
}
198
+
}
199
+
}
+28
crates/weaver-app/src/main.rs
···
100
#[cfg(target_arch = "wasm32")]
101
console_error_panic_hook::set_once();
102
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
103
#[cfg(feature = "server")]
104
std::panic::set_hook(Box::new(|panic_info| {
105
tracing::error!("PANIC: {:?}", panic_info);
···
100
#[cfg(target_arch = "wasm32")]
101
console_error_panic_hook::set_once();
102
103
+
// Set up tracing subscriber with both console output and log capture (wasm only)
104
+
// Must happen before dioxus::launch so dioxus skips its own init
105
+
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
106
+
{
107
+
use tracing::Level;
108
+
use tracing::subscriber::set_global_default;
109
+
use tracing_subscriber::layer::SubscriberExt;
110
+
use tracing_subscriber::Registry;
111
+
112
+
let console_level = if cfg!(debug_assertions) {
113
+
Level::DEBUG
114
+
} else {
115
+
Level::INFO
116
+
};
117
+
118
+
let wasm_layer = tracing_wasm::WASMLayer::new(
119
+
tracing_wasm::WASMLayerConfigBuilder::new()
120
+
.set_max_level(console_level)
121
+
.build(),
122
+
);
123
+
124
+
let reg = Registry::default()
125
+
.with(wasm_layer)
126
+
.with(components::editor::LogCaptureLayer);
127
+
128
+
let _ = set_global_default(reg);
129
+
}
130
+
131
#[cfg(feature = "server")]
132
std::panic::set_hook(Box::new(|panic_info| {
133
tracing::error!("PANIC: {:?}", panic_info);
+9
-3
crates/weaver-app/src/views/editor.rs
···
1
//! Editor view - wraps the MarkdownEditor component for the /editor route.
2
3
-
use crate::components::{editor::MarkdownEditor, record_view::CodeView};
4
use dioxus::prelude::*;
5
6
/// Editor page view.
···
12
rsx! {
13
EditorCss {}
14
div { class: "editor-page",
15
-
h1 { style: "margin-left: 6rem;", "Markdown Editor Test" }
0
0
0
0
0
0
16
MarkdownEditor { initial_content: None }
17
-
18
}
19
}
20
}
···
41
_ => rsx! {},
42
}
43
}
0
···
1
//! Editor view - wraps the MarkdownEditor component for the /editor route.
2
3
+
use crate::components::editor::{MarkdownEditor, ReportButton};
4
use dioxus::prelude::*;
5
6
/// Editor page view.
···
12
rsx! {
13
EditorCss {}
14
div { class: "editor-page",
15
+
div { class: "editor-header",
16
+
h1 { "Markdown Editor Test" }
17
+
ReportButton {
18
+
email: "editor-bugs@weaver.sh".to_string(),
19
+
editor_id: "markdown-editor".to_string(),
20
+
}
21
+
}
22
MarkdownEditor { initial_content: None }
0
23
}
24
}
25
}
···
46
_ => rsx! {},
47
}
48
}
49
+