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