@@ -40,9 +40,9 @@ pub struct Cli {
4040 #[ arg( long, global = true , conflicts_with = "bucket" ) ]
4141 local_dir : Option < PathBuf > ,
4242
43- /// Directory containing UI static files (when running without a subcommand)
44- #[ arg( long, default_value = "ui" , global = true ) ]
45- ui_dir : PathBuf ,
43+ /// Dev mode: serve UI files from disk for faster iteration
44+ #[ arg( long, global = true ) ]
45+ dev : bool ,
4646}
4747
4848#[ derive( Subcommand , Debug ) ]
@@ -99,18 +99,35 @@ async fn main() -> anyhow::Result<()> {
9999 } ,
100100 } ,
101101 Some ( Commands :: Serve { } ) | None => {
102- return serve ( cli. port , cli. bucket , cli. prefix , cli. local_dir , cli. ui_dir ) . await ;
102+ return serve ( cli. port , cli. bucket , cli. prefix , cli. local_dir , cli. dev ) . await ;
103103 }
104104 }
105105 Ok ( ( ) )
106106}
107107
108+ async fn detect_bucket_region ( bucket : & str ) -> Option < String > {
109+ let config = aws_config:: load_defaults ( aws_config:: BehaviorVersion :: latest ( ) ) . await ;
110+ let client = aws_sdk_s3:: Client :: new ( & config) ;
111+ match client. head_bucket ( ) . bucket ( bucket) . send ( ) . await {
112+ Ok ( resp) => resp. bucket_region ( ) . map ( |r| r. to_string ( ) ) ,
113+ Err ( err) => {
114+ // HeadBucket errors include the x-amz-bucket-region header
115+ let raw = err. raw_response ( ) ;
116+ raw. and_then ( |r| {
117+ r. headers ( )
118+ . get ( "x-amz-bucket-region" )
119+ . map ( |v| v. to_string ( ) )
120+ } )
121+ }
122+ }
123+ }
124+
108125async fn serve (
109126 port : u16 ,
110127 bucket : Option < String > ,
111128 prefix : Option < String > ,
112129 local_dir : Option < PathBuf > ,
113- ui_dir : PathBuf ,
130+ dev : bool ,
114131) -> anyhow:: Result < ( ) > {
115132 tracing_subscriber:: fmt ( )
116133 . with_env_filter (
@@ -119,42 +136,89 @@ async fn serve(
119136 )
120137 . init ( ) ;
121138
122- let ui_dir = if ui_dir. exists ( ) {
123- ui_dir
124- } else if let Ok ( exe) = std:: env:: current_exe ( ) {
125- let candidate = exe. parent ( ) . unwrap_or ( exe. as_ref ( ) ) . join ( & ui_dir) ;
126- if candidate. exists ( ) {
127- candidate
128- } else {
129- ui_dir
139+ let dev_ui_dir = if dev {
140+ // In dev mode, find the ui/ directory relative to the manifest or CWD
141+ let candidates = [ PathBuf :: from ( "ui" ) , PathBuf :: from ( "dial9-viewer/ui" ) ] ;
142+ let dir = candidates. into_iter ( ) . find ( |p| p. exists ( ) ) ;
143+ match dir {
144+ Some ( d) => {
145+ tracing:: info!( path = %d. display( ) , "dev mode: serving UI from disk" ) ;
146+ Some ( d)
147+ }
148+ None => {
149+ anyhow:: bail!(
150+ "--dev: could not find ui/ directory. Run from the dial9-viewer/ or repo root directory."
151+ ) ;
152+ }
130153 }
131154 } else {
132- ui_dir
155+ None
133156 } ;
134157
135158 let app_state = if let Some ( dir) = & local_dir {
136159 let dir = std:: fs:: canonicalize ( dir) ?;
137160 tracing:: info!( path = %dir. display( ) , "serving traces from local directory" ) ;
138161 let backend = dial9_viewer:: storage:: LocalBackend :: new ( & dir) ;
139- // Use a sentinel bucket so routes that require one don't fail.
140- dial9_viewer:: server:: AppState :: new (
162+ let mut state = dial9_viewer:: server:: AppState :: new (
141163 std:: sync:: Arc :: new ( backend) ,
142164 Some ( "local" . into ( ) ) ,
143165 prefix. clone ( ) ,
144- )
166+ ) ;
167+ if let Some ( d) = dev_ui_dir {
168+ state = state. with_dev_ui_dir ( d) ;
169+ }
170+ state
145171 } else {
146- let backend = dial9_viewer:: storage:: S3Backend :: from_env ( ) . await ;
147- dial9_viewer:: server:: AppState :: new (
148- std:: sync:: Arc :: new ( backend) ,
149- bucket. clone ( ) ,
150- prefix. clone ( ) ,
151- )
172+ // Detect bucket region if a bucket is provided
173+ if let Some ( bucket_name) = & bucket {
174+ if let Some ( region) = detect_bucket_region ( bucket_name) . await {
175+ tracing:: info!( %region, bucket = %bucket_name, "detected bucket region" ) ;
176+ let config = aws_config:: defaults ( aws_config:: BehaviorVersion :: latest ( ) )
177+ . region ( aws_sdk_s3:: config:: Region :: new ( region) )
178+ . load ( )
179+ . await ;
180+ let client = aws_sdk_s3:: Client :: new ( & config) ;
181+ let backend = dial9_viewer:: storage:: S3Backend :: from_client ( client) ;
182+ let mut state = dial9_viewer:: server:: AppState :: new (
183+ std:: sync:: Arc :: new ( backend) ,
184+ bucket. clone ( ) ,
185+ prefix. clone ( ) ,
186+ ) ;
187+ if let Some ( d) = dev_ui_dir {
188+ state = state. with_dev_ui_dir ( d) ;
189+ }
190+ state
191+ } else {
192+ tracing:: warn!( bucket = %bucket_name, "could not detect bucket region, using default" ) ;
193+ let backend = dial9_viewer:: storage:: S3Backend :: from_env ( ) . await ;
194+ let mut state = dial9_viewer:: server:: AppState :: new (
195+ std:: sync:: Arc :: new ( backend) ,
196+ bucket. clone ( ) ,
197+ prefix. clone ( ) ,
198+ ) ;
199+ if let Some ( d) = dev_ui_dir {
200+ state = state. with_dev_ui_dir ( d) ;
201+ }
202+ state
203+ }
204+ } else {
205+ let backend = dial9_viewer:: storage:: S3Backend :: from_env ( ) . await ;
206+ let mut state = dial9_viewer:: server:: AppState :: new (
207+ std:: sync:: Arc :: new ( backend) ,
208+ bucket. clone ( ) ,
209+ prefix. clone ( ) ,
210+ ) ;
211+ if let Some ( d) = dev_ui_dir {
212+ state = state. with_dev_ui_dir ( d) ;
213+ }
214+ state
215+ }
152216 } ;
153217
154- let app = dial9_viewer:: server:: router ( app_state, & ui_dir ) ;
218+ let app = dial9_viewer:: server:: router ( app_state) ;
155219
156220 let listener = tokio:: net:: TcpListener :: bind ( ( "0.0.0.0" , port) ) . await ?;
157- tracing:: info!( port, ui_dir = %ui_dir . display ( ) , "dial9-viewer listening" ) ;
221+ tracing:: info!( port, dev , "dial9-viewer listening" ) ;
158222 println ! ( "\n → http://localhost:{}\n " , port) ;
159223 if let Some ( dir) = & local_dir {
160224 tracing:: info!( path = %dir. display( ) , "local directory mode" ) ;
0 commit comments