2026-07-22 14:51:11 +08:00
<!doctype html>
< html lang = "zh-CN" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > Chub 繚 瘚讛� � 典� 靘讠恣� � � � � </ title >
< style >
: root {
color-scheme : light ;
--bg : #f5f7fb ;
--surface : #ffffff ;
--surface-2 : #f8f9fc ;
--surface-3 : #eef2f8 ;
--text : #18202b ;
--muted : #617083 ;
--line : #dce2eb ;
--line-strong : #c7d0dd ;
--accent : #2563eb ;
--accent-strong : #1d4ed8 ;
--accent-soft : #e7efff ;
--success : #087443 ;
--success-soft : #e4f6ed ;
--warning : #9a5b00 ;
--warning-soft : #fff3d8 ;
--danger : #b42318 ;
--danger-soft : #ffebe8 ;
--shadow : 0 18 px 48 px rgba ( 26 , 43 , 73 , .14 ), 0 2 px 7 px rgba ( 26 , 43 , 73 , .08 );
--radius-control : 8 px ;
--radius-card : 14 px ;
--radius-dialog : 16 px ;
--focus : 0 0 0 3 px rgba ( 37 , 99 , 235 , .25 );
}
[ data-theme = "dark" ] {
color-scheme : dark ;
--bg : #11151c ;
--surface : #1b222d ;
--surface-2 : #202936 ;
--surface-3 : #293544 ;
--text : #f1f5fb ;
--muted : #aab7c8 ;
--line : #364354 ;
--line-strong : #4a5a6c ;
--accent : #83aaff ;
--accent-strong : #a9c2ff ;
--accent-soft : #263b66 ;
--success : #65d49a ;
--success-soft : #173c2b ;
--warning : #f6c15c ;
--warning-soft : #4a3612 ;
--danger : #ff9c91 ;
--danger-soft : #4b2421 ;
--shadow : 0 18 px 48 px rgba ( 0 , 0 , 0 , .35 );
}
* { box-sizing : border-box ; }
html , body { margin : 0 ; min-height : 100 % ; font-family : "Segoe UI Variable" , "Segoe UI" , system-ui , sans-serif ; color : var ( -- text ); background : var ( -- bg ); }
body { font-size : 14 px ; }
button , input , select { font : inherit ; }
button { border : 0 ; cursor : pointer ; }
button : focus-visible , input : focus-visible , select : focus-visible { outline : none ; box-shadow : var ( -- focus ); }
. prototype-ribbon { position : fixed ; z-index : 20 ; top : 0 ; left : 50 % ; transform : translateX ( -50 % ); background : #173b72 ; color : #fff ; padding : 5 px 14 px ; border-radius : 0 0 8 px 8 px ; font-size : 11 px ; letter-spacing : .04 em ; }
. app { display : grid ; grid-template-columns : 232 px minmax ( 0 , 1 fr ); min-height : 100 vh ; }
. sidebar { background : var ( -- surface ); border-right : 1 px solid var ( -- line ); padding : 34 px 14 px 18 px ; display : flex ; flex-direction : column ; gap : 24 px ; }
. brand { display : flex ; align-items : center ; gap : 10 px ; padding : 0 10 px ; }
. brand-mark { width : 32 px ; height : 32 px ; border-radius : 10 px ; background : linear-gradient ( 135 deg , #2563eb , #66a1ff ); color : #fff ; display : grid ; place-items : center ; font-weight : 800 ; letter-spacing : -.04 em ; }
. brand strong { display : block ; font-size : 17 px ; letter-spacing : -.02 em ; }
. brand span { color : var ( -- muted ); font-size : 11 px ; }
. nav { display : grid ; gap : 4 px ; }
. nav button { min-height : 40 px ; padding : 0 12 px ; text-align : left ; color : var ( -- muted ); background : transparent ; border-radius : var ( -- radius - control ); display : flex ; align-items : center ; gap : 11 px ; }
. nav button : hover { background : var ( -- surface -2 ); color : var ( -- text ); }
. nav button [ aria-current = "page" ] { background : var ( -- accent - soft ); color : var ( -- accent - strong ); font-weight : 650 ; }
. nav-icon { width : 18 px ; text-align : center ; font-size : 16 px ; }
. sidebar-footer { margin-top : auto ; border-top : 1 px solid var ( -- line ); padding : 16 px 10 px 0 ; color : var ( -- muted ); font-size : 12 px ; line-height : 1.5 ; }
. main { min-width : 0 ; padding : 34 px clamp ( 18 px , 3 vw , 42 px ) 38 px ; }
. topbar { display : flex ; align-items : flex-start ; justify-content : space-between ; gap : 18 px ; max-width : 1420 px ; margin : 0 auto 24 px ; }
. eyebrow { color : var ( -- accent - strong ); font-size : 12 px ; font-weight : 700 ; letter-spacing : .08 em ; text-transform : uppercase ; }
h1 { margin : 5 px 0 6 px ; font-size : clamp ( 24 px , 3 vw , 32 px ); letter-spacing : -.035 em ; }
. subtitle { margin : 0 ; color : var ( -- muted ); }
2026-07-25 16:21:38 +08:00
. top-actions { display : flex ; gap : 8 px ; align-items : center ; width : min ( 468 px , 50 vw ); }
. instance-commands { display : grid ; grid-template-columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 8 px ; flex : 1 ; }
. instance-commands button { width : 100 % ; }
2026-07-22 14:51:11 +08:00
. icon-button , . secondary , . primary , . danger-button { min-height : 36 px ; padding : 0 14 px ; border-radius : var ( -- radius - control ); font-weight : 650 ; white-space : nowrap ; }
. icon-button , . secondary { color : var ( -- text ); background : var ( -- surface ); border : 1 px solid var ( -- line - strong ); }
. icon-button { width : 38 px ; padding : 0 ; }
. secondary : hover , . icon-button : hover { background : var ( -- surface -2 ); }
. primary { color : #fff ; background : var ( -- accent ); }
. primary : hover { background : var ( -- accent - strong ); }
. danger-button { color : #fff ; background : var ( -- danger ); }
. danger-button : hover { filter : brightness ( .94 ); }
. content { max-width : 1420 px ; margin : auto ; display : grid ; gap : 18 px ; }
. banner { display : flex ; gap : 12 px ; align-items : flex-start ; background : var ( -- warning - soft ); border : 1 px solid color-mix ( in srgb , var ( -- warning ) 30 % , transparent ); border-radius : var ( -- radius - card ); padding : 13 px 16 px ; color : var ( -- text ); }
. banner b { color : var ( -- warning ); }
. banner p { margin : 3 px 0 0 ; color : var ( -- muted ); font-size : 13 px ; }
. summary-grid { display : grid ; grid-template-columns : repeat ( 4 , minmax ( 0 , 1 fr )); gap : 12 px ; }
. summary { background : var ( -- surface ); border : 1 px solid var ( -- line ); border-radius : var ( -- radius - card ); padding : 16 px ; }
. summary-label { color : var ( -- muted ); font-size : 12 px ; }
. summary-value { margin-top : 8 px ; display : flex ; align-items : baseline ; gap : 8 px ; font-size : 25 px ; font-weight : 750 ; letter-spacing : -.03 em ; }
. summary-note { color : var ( -- muted ); font-size : 12 px ; }
. workspace { background : var ( -- surface ); border : 1 px solid var ( -- line ); border-radius : var ( -- radius - card ); overflow : hidden ; }
. toolbar { padding : 14 px 16 px ; border-bottom : 1 px solid var ( -- line ); display : flex ; flex-wrap : wrap ; align-items : center ; gap : 9 px ; }
. search { min-width : 220 px ; flex : 1 ; position : relative ; }
. search input { width : 100 % ; height : 36 px ; border : 1 px solid var ( -- line - strong ); border-radius : var ( -- radius - control ); background : var ( -- surface -2 ); padding : 0 12 px 0 35 px ; color : var ( -- text ); }
. search span { position : absolute ; left : 12 px ; top : 8 px ; color : var ( -- muted ); }
select { height : 36 px ; color : var ( -- text ); background : var ( -- surface ); border : 1 px solid var ( -- line - strong ); border-radius : var ( -- radius - control ); padding : 0 30 px 0 11 px ; }
. toolbar-spacer { flex : 1 ; }
. table-wrap { overflow-x : auto ; }
table { width : 100 % ; border-collapse : collapse ; min-width : 760 px ; }
th { height : 42 px ; padding : 0 16 px ; text-align : left ; color : var ( -- muted ); font-size : 12 px ; font-weight : 650 ; background : var ( -- surface -2 ); border-bottom : 1 px solid var ( -- line ); }
td { padding : 14 px 16 px ; border-bottom : 1 px solid var ( -- line ); vertical-align : middle ; }
tbody tr { cursor : pointer ; }
tbody tr : hover { background : var ( -- surface -2 ); }
tbody tr . selected { background : var ( -- accent - soft ); }
. instance-name { display : flex ; align-items : center ; gap : 10 px ; min-width : 190 px ; }
. browser-logo { width : 28 px ; height : 28 px ; display : grid ; place-items : center ; border-radius : 8 px ; color : #fff ; font-size : 12 px ; font-weight : 800 ; }
. chrome { background : conic-gradient ( #e84c3d 0 33 % , #f5c746 0 66 % , #36a269 0 ); }
. edge { background : linear-gradient ( 140 deg , #0c9b9e , #1768d2 ); }
. instance-name strong { display : block ; font-size : 14 px ; }
. instance-name small , . mono { color : var ( -- muted ); font-size : 12 px ; font-family : "Cascadia Mono" , Consolas , monospace ; }
. status { display : inline-flex ; align-items : center ; gap : 7 px ; border-radius : 999 px ; padding : 5 px 9 px ; font-size : 12 px ; font-weight : 700 ; white-space : nowrap ; }
. status :: before { content : "" ; width : 7 px ; height : 7 px ; border-radius : 50 % ; background : currentColor ; }
. status . running { color : var ( -- success ); background : var ( -- success - soft ); }
. status . starting { color : var ( -- accent - strong ); background : var ( -- accent - soft ); }
2026-07-25 15:36:23 +08:00
. status . associated { color : var ( -- warning ); background : var ( -- warning - soft ); }
2026-07-22 14:51:11 +08:00
. status . occupied { color : var ( -- warning ); background : var ( -- warning - soft ); }
. status . exited { color : var ( -- muted ); background : var ( -- surface -3 ); }
2026-07-25 17:47:02 +08:00
. row-actions { display : flex ; justify-content : flex-end ; gap : 10 px ; opacity : .7 ; }
2026-07-22 14:51:11 +08:00
tr : hover . row-actions , tr . selected . row-actions { opacity : 1 ; }
2026-07-25 17:47:02 +08:00
. row-actions button { background : transparent ; color : var ( -- muted ); padding : 8 px 10 px ; border-radius : 6 px ; }
2026-07-22 14:51:11 +08:00
. row-actions button : hover { color : var ( -- accent - strong ); background : var ( -- accent - soft ); }
. table-footer { display : flex ; align-items : center ; justify-content : space-between ; padding : 12 px 16 px ; color : var ( -- muted ); font-size : 12 px ; }
. drawer-backdrop { position : fixed ; inset : 0 ; background : rgba ( 16 , 24 , 40 , .28 ); z-index : 10 ; opacity : 0 ; pointer-events : none ; transition : opacity .16 s ease ; }
. drawer-backdrop . open { opacity : 1 ; pointer-events : auto ; }
. drawer { position : fixed ; z-index : 11 ; top : 0 ; right : 0 ; bottom : 0 ; width : min ( 480 px , 100 vw ); background : var ( -- surface ); box-shadow : var ( -- shadow ); transform : translateX ( 100 % ); transition : transform .2 s ease ; overflow : auto ; }
. drawer . open { transform : translateX ( 0 ); }
. drawer-head { display : flex ; align-items : flex-start ; justify-content : space-between ; gap : 12 px ; padding : 28 px 26 px 20 px ; border-bottom : 1 px solid var ( -- line ); }
. drawer-head h2 { margin : 0 0 5 px ; font-size : 21 px ; letter-spacing : -.02 em ; }
. drawer-head p { margin : 0 ; color : var ( -- muted ); font-size : 13 px ; }
. drawer-body { padding : 22 px 26 px ; display : grid ; gap : 22 px ; }
2026-07-27 09:40:37 +08:00
. drawer-body . instance-form-workspace { margin : 14 px ; padding : 14 px ; gap : 14 px ; border : 1 px solid var ( -- line - strong ); border-radius : calc ( var ( -- radius - card ) + 2 px ); background : var ( -- surface -2 ); }
. instance-form-workspace . form-section { padding : 13 px ; border : 1 px solid var ( -- line ); border-radius : 10 px ; background : var ( -- surface ); }
. instance-form-workspace . field { padding : 10 px ; border : 1 px solid var ( -- line ); border-radius : 9 px ; background : var ( -- surface -2 ); }
. instance-form-workspace . field : focus-within { border-color : var ( -- accent ); box-shadow : var ( -- focus ); }
. instance-form-workspace . drawer-actions { padding : 12 px ; border : 1 px solid var ( -- line ); border-radius : 10 px ; background : var ( -- surface ); }
2026-07-22 14:51:11 +08:00
. form-section { display : grid ; gap : 13 px ; }
. form-section h3 { margin : 0 ; font-size : 13 px ; color : var ( -- muted ); text-transform : uppercase ; letter-spacing : .06 em ; }
. field { display : grid ; gap : 6 px ; }
. field label { font-weight : 650 ; }
. field label span { color : var ( -- danger ); }
. field input , . field select { width : 100 % ; height : 38 px ; border : 1 px solid var ( -- line - strong ); background : var ( -- surface -2 ); color : var ( -- text ); border-radius : var ( -- radius - control ); padding : 0 11 px ; }
. field small , . help { color : var ( -- muted ); font-size : 12 px ; line-height : 1.45 ; }
. field . error input { border-color : var ( -- danger ); box-shadow : 0 0 0 2 px color-mix ( in srgb , var ( -- danger ) 18 % , transparent ); }
. field-error { color : var ( -- danger ); font-size : 12 px ; }
. check-line { display : flex ; gap : 9 px ; align-items : flex-start ; color : var ( -- text ); }
. check-line input { width : 16 px ; height : 16 px ; accent-color : var ( -- accent ); margin-top : 2 px ; }
. drawer-actions { display : flex ; justify-content : flex-end ; gap : 8 px ; border-top : 1 px solid var ( -- line ); padding-top : 18 px ; }
. detail-card { padding : 16 px ; background : var ( -- surface -2 ); border : 1 px solid var ( -- line ); border-radius : 12 px ; display : grid ; gap : 12 px ; }
. detail-line { display : flex ; justify-content : space-between ; gap : 18 px ; align-items : baseline ; }
. detail-line span : first-child { color : var ( -- muted ); }
. detail-line span : last-child { text-align : right ; max-width : 68 % ; overflow-wrap : anywhere ; }
. activity { display : grid ; gap : 9 px ; }
. activity-item { display : grid ; grid-template-columns : 9 px 1 fr ; gap : 9 px ; font-size : 12 px ; color : var ( -- muted ); }
. activity-item i { width : 8 px ; height : 8 px ; background : var ( -- accent ); border-radius : 50 % ; margin-top : 4 px ; }
dialog { border : 0 ; border-radius : var ( -- radius - dialog ); padding : 0 ; width : min ( 430 px , calc ( 100 vw - 32 px )); color : var ( -- text ); background : var ( -- surface ); box-shadow : var ( -- shadow ); }
2026-07-27 10:01:07 +08:00
dialog . edit-dialog , dialog . tabs-dialog { width : min ( 620 px , calc ( 100 vw - 32 px )); padding : 10 px ; border : 1 px solid var ( -- line - strong ); background : var ( -- surface -2 ); }
dialog . tabs-dialog { width : min ( 720 px , calc ( 100 vw - 32 px )); }
2026-07-22 14:51:11 +08:00
dialog :: backdrop { background : rgba ( 16 , 24 , 40 , .42 ); }
. dialog-inner { padding : 24 px ; display : grid ; gap : 17 px ; }
. dialog-inner h2 { margin : 0 ; font-size : 19 px ; }
. dialog-inner p { margin : 0 ; color : var ( -- muted ); line-height : 1.55 ; }
. dialog-warning { padding : 11 px 12 px ; border-radius : 9 px ; background : var ( -- danger - soft ); color : var ( -- danger ); font-size : 12 px ; line-height : 1.45 ; }
2026-07-25 18:07:57 +08:00
. dialog-list { margin : -3 px 0 0 ; padding-left : 20 px ; color : var ( -- text ); display : grid ; gap : 5 px ; }
. dialog-list li { line-height : 1.45 ; }
2026-07-22 14:51:11 +08:00
. dialog-actions { display : flex ; justify-content : flex-end ; gap : 8 px ; }
2026-07-27 09:40:37 +08:00
. edit-form { display : grid ; gap : 12 px ; border : 1 px solid var ( -- line ); border-radius : var ( -- radius - card ); background : var ( -- surface ); }
. edit-form > . field { padding : 10 px ; border : 1 px solid var ( -- line ); border-radius : 9 px ; background : var ( -- surface -2 ); }
. edit-form > . field : focus-within { border-color : var ( -- accent ); box-shadow : var ( -- focus ); }
. edit-form . form-note { border : 1 px solid color-mix ( in srgb , var ( -- warning ) 35 % , var ( -- line )); }
. edit-form . dialog-actions { padding : 12 px ; border : 1 px solid var ( -- line ); border-radius : 10 px ; background : var ( -- surface -2 ); }
2026-07-25 16:21:38 +08:00
. edit-form . path-field { grid-template-columns : minmax ( 0 , 1 fr ) auto ; }
2026-07-27 10:01:07 +08:00
. tabs-meta { padding : 10 px 12 px ; border : 1 px solid var ( -- line ); border-radius : 9 px ; background : var ( -- surface -2 ); color : var ( -- muted ); font-size : 12 px ; line-height : 1.5 ; }
. tabs-state { min-height : 42 px ; padding : 11 px 12 px ; border : 1 px solid color-mix ( in srgb , var ( -- accent ) 30 % , var ( -- line )); border-radius : 9 px ; background : var ( -- accent - soft ); color : var ( -- accent - strong ); font-size : 12 px ; line-height : 1.45 ; }
. tabs-target-list { max-height : min ( 300 px , 42 vh ); overflow : auto ; display : grid ; gap : 7 px ; }
. tabs-target { padding : 10 px 12 px ; border : 1 px solid var ( -- line ); border-radius : 9 px ; background : var ( -- surface -2 ); display : grid ; gap : 3 px ; }
. tabs-target small { color : var ( -- muted ); }
. tabs-target strong , . tabs-target code { overflow-wrap : anywhere ; }
2026-07-25 16:21:38 +08:00
. readonly-value { background : var ( -- surface -3 ) !important ; color : var ( -- muted ) !important ; cursor : default ; }
. form-note { padding : 10 px 12 px ; border-radius : 9 px ; background : var ( -- warning - soft ); color : var ( -- warning ); font-size : 12 px ; line-height : 1.5 ; }
2026-07-22 14:51:11 +08:00
. empty-state { display : none ; text-align : center ; padding : 48 px 20 px ; color : var ( -- muted ); }
. empty-state strong { display : block ; color : var ( -- text ); font-size : 16 px ; margin-bottom : 7 px ; }
. sr-only { position : absolute ; width : 1 px ; height : 1 px ; padding : 0 ; margin : -1 px ; overflow : hidden ; clip : rect ( 0 , 0 , 0 , 0 ); white-space : nowrap ; border : 0 ; }
. toast { position : fixed ; right : 22 px ; bottom : 22 px ; z-index : 30 ; max-width : min ( 420 px , calc ( 100 vw - 44 px )); padding : 12 px 15 px ; border : 1 px solid var ( -- line ); border-left : 4 px solid var ( -- accent ); border-radius : 10 px ; background : var ( -- surface ); box-shadow : var ( -- shadow ); color : var ( -- text ); opacity : 0 ; transform : translateY ( 8 px ); pointer-events : none ; transition : opacity .16 s ease , transform .16 s ease ; }
. toast . show { opacity : 1 ; transform : translateY ( 0 ); }
. page-hidden { display : none !important ; }
2026-07-26 17:42:40 +08:00
. settings-workspace { padding : 16 px ; border : 1 px solid var ( -- line ); border-radius : calc ( var ( -- radius - card ) + 2 px ); background : var ( -- surface -2 ); }
. settings-grid { display : grid ; grid-template-columns : minmax ( 0 , 1.2 fr ) minmax ( 260 px , .8 fr ); gap : 16 px ; }
. settings-card { background : var ( -- surface ); border : 1 px solid var ( -- line - strong ); border-radius : var ( -- radius - card ); padding : 20 px ; display : grid ; gap : 16 px ; }
2026-07-22 14:51:11 +08:00
. settings-card h2 { margin : 0 ; font-size : 18 px ; letter-spacing : -.02 em ; }
2026-07-26 17:42:40 +08:00
. settings-card > p { margin : -10 px 0 0 ; color : var ( -- muted ); line-height : 1.5 ; }
. settings-card . form-section , . settings-card . setting-list { padding : 14 px ; border : 1 px solid var ( -- line ); border-radius : 10 px ; background : var ( -- surface -2 ); }
. settings-card . form-section { gap : 12 px ; }
. settings-card . field { padding : 12 px ; border : 1 px solid var ( -- line ); border-radius : 9 px ; background : var ( -- surface ); }
. settings-card . field : focus-within { border-color : var ( -- accent ); box-shadow : var ( -- focus ); }
. settings-card . check-line { padding : 12 px ; border : 1 px solid var ( -- line ); border-radius : 9 px ; background : var ( -- surface ); }
2026-07-22 15:57:02 +08:00
. path-field { display : grid ; grid-template-columns : minmax ( 0 , 1 fr ) auto auto ; gap : 8 px ; }
. radio-row { display : flex ; flex-wrap : wrap ; gap : 16 px ; align-items : center ; min-height : 38 px ; }
. radio-row label { font-weight : 500 ; display : inline-flex ; gap : 7 px ; align-items : center ; }
2026-07-26 17:42:40 +08:00
. settings-status { min-height : 42 px ; padding : 11 px 12 px ; border : 1 px solid color-mix ( in srgb , var ( -- success ) 35 % , var ( -- line )); border-radius : 9 px ; background : var ( -- success - soft ); color : var ( -- success ); font-size : 12 px ; line-height : 1.45 ; }
2026-07-22 14:51:11 +08:00
. settings-status . warning { background : var ( -- warning - soft ); color : var ( -- warning ); }
2026-07-26 17:42:40 +08:00
. settings-actions { display : flex ; justify-content : flex-end ; gap : 8 px ; border : 1 px solid var ( -- line ); border-radius : 10 px ; background : var ( -- surface -2 ); padding : 12 px ; }
2026-07-22 14:51:11 +08:00
. setting-list { display : grid ; gap : 12 px ; }
. setting-list . detail-line { padding-bottom : 12 px ; border-bottom : 1 px solid var ( -- line ); }
. setting-list . detail-line : last-child { border-bottom : 0 ; padding-bottom : 0 ; }
@ media ( max-width : 800px ) { . settings-grid { grid-template-columns : 1 fr ; } }
@ media ( max-width : 1007px ) {
. app { grid-template-columns : 70 px minmax ( 0 , 1 fr ); }
. sidebar { padding-inline : 10 px ; }
. brand { justify-content : center ; padding : 0 ; }
. brand > div : last-child , . nav button span : not ( . nav-icon ), . sidebar-footer { display : none ; }
. nav button { justify-content : center ; padding : 0 ; }
. summary-grid { grid-template-columns : repeat ( 2 , minmax ( 0 , 1 fr )); }
}
@ media ( max-width : 640px ) {
. app { display : block ; }
. sidebar { height : 58 px ; padding : 10 px 14 px ; border-right : 0 ; border-bottom : 1 px solid var ( -- line ); flex-direction : row ; align-items : center ; }
. brand { margin-right : auto ; }
. brand > div : last-child { display : block ; }
. nav { display : flex ; }
. nav button { width : 38 px ; }
. nav button span : not ( . nav-icon ) { display : none ; }
. main { padding : 28 px 12 px ; }
. topbar { display : block ; }
2026-07-25 16:21:38 +08:00
. top-actions { margin-top : 16 px ; width : 100 % ; }
. instance-commands { grid-template-columns : 1 fr ; }
2026-07-22 14:51:11 +08:00
. summary-grid { gap : 8 px ; }
. summary { padding : 12 px ; }
. summary-value { font-size : 21 px ; }
. toolbar { align-items : stretch ; }
. search { min-width : 100 % ; }
. toolbar-spacer { display : none ; }
. toolbar select { flex : 1 ; }
}
@ media ( prefers-reduced-motion : reduce ) { *, * :: before , * :: after { transition-duration : .01 ms !important ; animation-duration : .01 ms !important ; } }
</ style >
</ head >
< body >
2026-07-25 18:07:57 +08:00
< div class = "prototype-ribbon" > PROTOTYPE 繚 隞� � 鈭支� 蝖株恕嚗屸� � 煺漣摰䂿緵 繚 Ctrl+Shift+E 璅⊥� � 㛖恣瘚讛� � 券� � � � </ div >
2026-07-22 14:51:11 +08:00
< div class = "app" id = "app" >
< aside class = "sidebar" aria-label = "銝餃紡� � " >
< div class = "brand" >< div class = "brand-mark" aria-hidden = "true" > C</ div >< div >< strong > Chub</ strong >< span > Browser Hub</ span ></ div ></ div >
< nav class = "nav" >
< button aria-current = "page" data-nav = "instances" >< span class = "nav-icon" aria-hidden = "true" > � � </ span >< span > 摰硺� </ span ></ button >
< button data-nav = "settings" >< span class = "nav-icon" aria-hidden = "true" > � � </ span >< span > 霈曄蔭</ span ></ button >
</ nav >
< div class = "sidebar-footer" > � 砍𧑐餈鞱� < br > 銝滢� 摮䀹� 閫� 膥� 剛� </ div >
</ aside >
< main class = "main" >
< header class = "topbar" >
< div >< div class = "eyebrow" id = "pageEyebrow" > Browser instances</ div >< h1 id = "pageTitle" > 瘚讛� � 典� 靘� </ h1 >< p class = "subtitle" id = "pageSubtitle" > � 臬𢆡� � � 撖笔僎摰匧� 蝞∠� Chrome 銝� Edge � 臬� </ p ></ div >
2026-07-25 16:21:38 +08:00
< div class = "top-actions" >< button class = "icon-button" id = "themeToggle" aria-label = "� � 揢瘛梯𠧧銝駁� " title = "� � 揢瘛梯𠧧銝駁� " > � � </ button >< div class = "instance-commands" id = "instanceCommands" >< button class = "primary" id = "newInstance" > 嚗� � 啣遣摰硺� </ button >< button class = "secondary" id = "refresh" title = "� 瑟鰵摰硺� � 嗆� � (F5)" > � � � 瑟鰵� 嗆� � </ button ></ div ></ div >
2026-07-22 14:51:11 +08:00
</ header >
< section class = "content" id = "instancesPage" aria-label = "摰硺� 撌乩� � � " >
< div class = "banner" role = "status" >< div aria-hidden = "true" > � � </ div >< div >< b > � 砍𧑐摰匧� 璅∪� </ b >< p > Chub � 芰恣� � 歇瘜典� 銝磰澈隞賣嵗撉屸� 朞� � � � 靘卝� � � � 冽� 閫� 膥銝滢� 鋡怨䌊� 典� � 准� � </ p ></ div ></ div >
< section class = "summary-grid" aria-label = "摰硺� 璁� � " >
< article class = "summary" >< div class = "summary-label" > � 券� 摰硺� </ div >< div class = "summary-value" > 4 < span class = "summary-note" > 撌脤� 蝵� </ span ></ div ></ article >
< article class = "summary" >< div class = "summary-label" > 餈鞱� 銝� </ div >< div class = "summary-value" style = "color:var(--success)" > 1 < span class = "summary-note" > 蝔喳� </ span ></ div ></ article >
< article class = "summary" >< div class = "summary-label" > � 臬𢆡銝� </ div >< div class = "summary-value" style = "color:var(--accent-strong)" > 1 < span class = "summary-note" > 霂瑞� � � </ span ></ div ></ article >
< article class = "summary" >< div class = "summary-label" > � � 憭� � </ div >< div class = "summary-value" style = "color:var(--warning)" > 1 < span class = "summary-note" > profile � 删鍂</ span ></ div ></ article >
</ section >
< section class = "workspace" aria-labelledby = "listTitle" >
2026-07-25 16:21:38 +08:00
< div class = "toolbar" >< div class = "search" >< span aria-hidden = "true" > � � </ span >< input id = "search" type = "search" placeholder = "� 𦦵揣摰硺� � � 楝敺� � � ID � 𣇉垢� � " aria-label = "� 𦦵揣摰硺� � � 楝敺� � � ID � 𣇉垢� � " ></ div >< select id = "browserFilter" aria-label = "蝑偦� 㗇� 閫� 膥" >< option value = "all" > � 券� 瘚讛� � � </ option >< option value = "chrome" > Chrome</ option >< option value = "edge" > Edge</ option ></ select >< select id = "statusFilter" aria-label = "蝑偦� 厩𠶖� � " >< option value = "all" > � 券� � 嗆� � </ option >< option value = "running" > 餈鞱� 銝� </ option >< option value = "starting" > � 臬𢆡銝� </ option >< option value = "associated" > 憭㚚� 撌脣� � � </ option >< option value = "occupied" > 鋡怠� � � </ option >< option value = "exited" > 撌脤� � � � </ option ></ select ></ div >
2026-07-25 15:36:23 +08:00
< div class = "table-wrap" >< table >< caption id = "listTitle" class = "sr-only" > 瘚讛� � 典� 靘见� 銵� </ caption >< thead >< tr >< th scope = "col" > 摰硺� � 滨妍</ th >< th scope = "col" > 瘚讛� � 函掩� � </ th >< th scope = "col" > � 冽� � 唳旿� 桀� </ th >< th scope = "col" > 靚� � 蝡臬藁</ th >< th scope = "col" > � 嗆� � </ th >< th scope = "col" > � 滢� </ th ></ tr ></ thead >< tbody id = "instanceRows" ></ tbody ></ table >< div class = "empty-state" id = "emptyState" >< strong > 瘝⊥� � 寥� � � � 靘� </ strong >< span > 撠肽� 皜� 膄蝑偦� 㚁� � 𡝗鰵撱箔� 銝芣� 閫� 膥摰硺� � � </ span ></ div ></ div >
2026-07-22 14:51:11 +08:00
< div class = "table-footer" >< span id = "resultCount" > � 曄內 4 銝芸� 靘� </ span >< span > � � � 擧� � 伐� � 𡁜� </ span ></ div >
</ section >
</ section >
< section class = "content page-hidden" id = "settingsPage" aria-label = "摨𠉛鍂霈曄蔭" >
2026-07-26 17:42:40 +08:00
< div class = "settings-workspace" >< div class = "settings-grid" >
2026-07-22 14:51:11 +08:00
< form class = "settings-card" id = "settingsForm" >
< div >< h2 > 瘚讛� � 其� � 桀� </ h2 >< p > 餈嗘� 霈曄蔭� 其� � 啣遣摰硺� � � � 霈文� 潘� 撌脫� 摰硺� � � � 蝵桐� 隡朞◤� 䠷� 靽格㺿� � </ p ></ div >
< div class = "form-section" >< h3 > 瘚讛� � 刻楝敺� </ h3 >
2026-07-22 15:57:02 +08:00
< div class = "field" >< label for = "chromePath" > Chrome � 舀� 銵峕� 隞� </ label >< div class = "path-field" >< input id = "chromePath" value = "� 芸𢆡� 𤑳緵嚗䬙hrome.exe" >< button type = "button" class = "secondary browse-setting" data-target = "chromePath" > � 㗇𥋘� � 辣</ button >< button type = "button" class = "secondary search-setting" data-target = "chromePath" > � 𦦵揣</ button ></ div >< small > � 嗵征� 㚚� 㗇𥋘� � 辣� 𠬍� Chub � 臬𢆡� 嗡� � ⊿� 頝臬� � 航挪� 柴� � </ small ></ div >
< div class = "field" >< label for = "edgePath" > Edge � 舀� 銵峕� 隞� </ label >< div class = "path-field" >< input id = "edgePath" value = "� 芸𢆡� 𤑳緵嚗饢sedge.exe" >< button type = "button" class = "secondary browse-setting" data-target = "edgePath" > � 㗇𥋘� � 辣</ button >< button type = "button" class = "secondary search-setting" data-target = "edgePath" > � 𦦵揣</ button ></ div >< small > Chrome 銝� Edge � 臬� � 怨挽蝵殷� 銋笔虾隞亦誧蝏凋蝙� 刻䌊� 典� � 啜� � </ small ></ div >
2026-07-22 14:51:11 +08:00
</ div >
< div class = "form-section" >< h3 > 暺䁅恕� 桀� </ h3 >
2026-07-27 23:50:24 +08:00
< div class = "field" >< label for = "defaultDataDir" > 暺䁅恕 User Data Dir � 寧𤌍敶� </ label >< div class = "path-field" >< input id = "defaultDataDir" value = "D:\\Tools\\chub\\user_data_dirs" >< button type = "button" class = "secondary browse-setting" data-target = "defaultDataDir" > � 㗇𥋘� 桀� </ button >< button type = "button" class = "secondary restore-portable-setting" data-target = "defaultDataDir" > � W � 蝔见� � 桀� 暺䁅恕</ button >< button type = "button" class = "secondary search-setting" data-target = "defaultDataDir" > � 𦦵揣</ button ></ div >< small > 暺䁅恕頝罸� 蝔见� � 桀� 嚗Ếhub.exe � � � 函𤌍敶𤏪� � � < span class = "mono" > user_data_dirs</ span > 嚗𥕦蘨敶勗� 銋见� � 啣遣� � � 靘页� 銝滨宏� 典歇� � profile� � </ small ></ div >
< div class = "field" >< label for = "logDir" > � 亙� � 桀� </ label >< div class = "path-field" >< input id = "logDir" value = "D:\\Tools\\chub\\logs" >< button type = "button" class = "secondary browse-setting" data-target = "logDir" > � 㗇𥋘� 桀� </ button >< button type = "button" class = "secondary restore-portable-setting" data-target = "logDir" > � W � 蝔见� � 桀� 暺䁅恕</ button >< button type = "button" class = "secondary search-setting" data-target = "logDir" > � 𦦵揣</ button ></ div >< small > 暺䁅恕頝罸� 蝔见� � 桀� � � < span class = "mono" > logs</ span > 嚗𥟇𠯫敹𦯀� 靽嘥� 敹� � 霂𦠜鱏靽⊥� 嚗� 僎餈� 誘� 𤩺� � � 㺭� � </ small ></ div >
2026-07-22 14:51:11 +08:00
</ div >
2026-07-25 17:47:02 +08:00
< div class = "form-section" >< h3 > 隞� � </ h3 >< p class = "help" > 隞� � 摮䀹� 霈方� 隞� � � � � � 㗇� � � � 靘见� 撱箏� 蝻𤥁� � � � 蝷箏� � 游𧑐� � � � </ p >< div class = "field" >< label for = "proxySelect" > 撌脖� 摮䀝誨� � </ label >< select id = "proxySelect" >< option value = "" > � 㗇𥋘隞� � 嚗� 鰵撱綽� </ option ></ select >< small > � 㗇𥋘� 𦒘� � 𧼮‵� 啣� 嚗偦� 㗇𥋘� 𨀣鰵撱算� 苷� 皜� 征颲枏� 獢� � � </ small ></ div >< div class = "field" >< label for = "proxyServer" > 隞� � � 啣� </ label >< div class = "path-field" >< input id = "proxyServer" placeholder = "靘见� 嚗冴ttp://127.0.0.1:8080" >< button type = "button" class = "primary" id = "proxySave" > 靽嘥� 隞� � </ button >< button type = "button" class = "danger-button" id = "proxyDelete" disabled > � 𣳇膄隞� � </ button ></ div >< small > � 舀� http� � ttps� � ocks4� � ocks5 � � < span class = "mono" > scheme://host:port</ span > 嚗𥕢� � � 捂� 冽� � 溻� � � � � � 頝臬� � � </ small ></ div >< div class = "settings-status" id = "proxyStatus" > � 啣遣隞� � 嚗𡁜‵� 坔𧑐� � � 𦒘� 摮塩� � </ div ></ div >
2026-07-22 14:51:11 +08:00
< div class = "settings-actions" >< button type = "button" class = "secondary" id = "discardSettings" > � 𡝗� </ button >< button type = "submit" class = "primary" > 靽嘥� 霈曄蔭</ button ></ div >
</ form >
2026-07-25 15:36:23 +08:00
< aside class = "settings-card" >< div >< h2 > 餈鞱� 銵䔶蛹</ h2 >< p > 敶勗� � 𡒊賒� 臬𢆡� � � � 典� � 剜𧒄� � � 霈方� 銝箝� � </ p ></ div >< div class = "setting-list" >< label class = "check-line" >< input type = "checkbox" id = "closeOnExit" >< span >< b > � � � � Chub � 嗅� � 剜𧋦撌亙� � 臬𢆡� � � 靘� </ b >< br >< small class = "help" > 暺䁅恕� 喲𡡒� � � 隡𡁜蔣� 滚� � 冽� 閫� 膥� � </ small ></ span ></ label >< div class = "field" >< label for = "remoteDebugPort" > 餈𦦵� 靚� � 韏瑕� 蝡臬藁</ label >< input id = "remoteDebugPort" type = "number" min = "1024" max = "65535" value = "9666" inputmode = "numeric" >< small > � 嗵征� 嗡蝙� � 9666� � 鍳� 典� 靘衤� 蝏穃� 127.0.0.1嚗𤤿垢� � ◤� 删鍂� 園◇摨誩� 霂蓥� 銝� 銝芸虾� 函垢� � � � </ small ></ div >< div class = "settings-status" id = "settingsStatus" > 霈曄蔭撌脖� 摮� 繚 � � � 擧凒� 堆� � 𡁜� </ div ></ div ></ aside >
2026-07-26 17:42:40 +08:00
</ div ></ div >
2026-07-22 14:51:11 +08:00
</ section >
</ main >
</ div >
< div class = "drawer-backdrop" id = "backdrop" ></ div >
< aside class = "drawer" id = "drawer" aria-label = "摰硺� 霂行� � 屸� 蝵� " aria-hidden = "true" ></ aside >
< dialog id = "confirmDialog" aria-labelledby = "dialogTitle" >< div class = "dialog-inner" >< h2 id = "dialogTitle" > � 喲𡡒瘚讛� � 典� 靘页� </ h2 >< p id = "dialogText" > 撠� 窈瘙� � 閫� 膥甇� 虜� � � 箝� � 𧊋靽嘥� � � � 憿萄� 摰寧眏瘚讛� � 刻䌊銵� � � � � � </ p >< div class = "dialog-warning" id = "dialogWarning" hidden > 撘箏� 蝏� 迫� 航� � 䭾� profile � 唳旿� 笔� � � � � 冽� 閫� 膥� 惩� 摨娍𧒄雿輻鍂� � </ div >< div class = "dialog-actions" >< button class = "secondary" id = "dialogCancel" > � 𡝗� </ button >< button class = "primary" id = "dialogConfirm" > 隡㗛� � 喲𡡒</ button ></ div ></ div ></ dialog >
2026-07-27 22:59:10 +08:00
< dialog id = "instanceDeleteDialog" aria-labelledby = "instanceDeleteTitle" aria-describedby = "instanceDeleteText" >< div class = "dialog-inner" >< h2 id = "instanceDeleteTitle" > � 𣳇膄摰硺� 嚗� </ h2 >< p id = "instanceDeleteText" ></ p >< label class = "check-line" >< input type = "checkbox" id = "deleteInstanceData" >< span >< b > � 峕𧒄� 𣳇膄 User Data Dir 銝剔� � 唳旿</ b >< br >< small class = "help" > 暺䁅恕銝滚� � 扎� � 𤌍敶蓥葉� � � 閫� 膥� 唳旿� 𣳇膄� 擧� 瘜閙� 憭溻� � </ small ></ span ></ label >< div class = "form-note" id = "instanceDeleteUnavailable" hidden ></ div >< div class = "dialog-warning" id = "instanceDeleteWarning" hidden ></ div >< div class = "dialog-actions" >< button class = "secondary" id = "instanceDeleteCancel" > � 𡝗� </ button >< button class = "danger-button" id = "instanceDeleteConfirm" > � 𣳇膄摰硺� </ button ></ div ></ div ></ dialog >
2026-07-27 10:01:07 +08:00
< dialog class = "edit-dialog" id = "editDialog" aria-labelledby = "editTitle" >< form class = "dialog-inner edit-form" id = "editForm" >< div >< h2 id = "editTitle" > 蝻𤥁� 摰硺� </ h2 >< p id = "editSubtitle" > 靽格㺿銝𧢲活� 臬𢆡雿輻鍂� � � 摮㗛� 蝵柴� � </ p ></ div >< div class = "field" >< label for = "editName" > 摰硺� � 滨妍 < span > *</ span ></ label >< input id = "editName" required >< small > � 其� � 典� 靘见� 銵其葉霂� � 甇斗� 閫� 膥� 臬� � � </ small ></ div >< div class = "field" >< span > 瘚讛� � 函掩� � </ span >< div class = "radio-row" role = "radiogroup" aria-label = "瘚讛� � 函掩� � " >< label >< input type = "radio" name = "editKind" value = "chrome" id = "editChrome" > Chrome</ label >< label >< input type = "radio" name = "editKind" value = "edge" id = "editEdge" > Edge</ label ></ div ></ div >< div class = "field" >< label for = "editDataDir" > User Data Dir < span > *</ span ></ label >< div class = "path-field" >< input id = "editDataDir" required >< button type = "button" class = "secondary" id = "chooseEditDir" > � 㗇𥋘� 桀� </ button ></ div >< small > 敹� ◆� 舐� 撖寡楝敺� � � 䔶� � 桀� 銝滩� 鋡怠� 銝芸� 靘见� � 嗡蝙� 具� � </ small ></ div >< div class = "field" >< label for = "editURL" > � 臬𢆡 URL嚗� 虾� 㚁� </ label >< input id = "editURL" type = "url" placeholder = "https://example.com" >< small > 隞� 𣈲� � http/https嚗𤤿� 蝛箸𧒄� 臬𢆡瘚讛� � 券� 霈日△� � </ small ></ div >< div class = "field" >< label for = "editPreferredPort" > 擐㚚� 㕑� 霂閧垢� � </ label >< input id = "editPreferredPort" type = "number" min = "1024" max = "65535" inputmode = "numeric" >< small > 銝𧢲活� 臬𢆡隡睃� 雿輻鍂嚗𥕢� � 嗡� Chub 摰硺� � 滚� � 嗡� � 鞟內靽格㺿� � </ small ></ div >< div class = "field" >< label for = "editProxy" > 隞� � </ label >< select id = "editProxy" >< option value = "" > � 牐誨� � </ option ></ select >< small > � 㗇𥋘撌脖� 摮条� � 㰘恕霂� 誨� � � 靽格㺿� 𦒘� 甈∪鍳� 函� � � � � </ small ></ div >< div class = "field" >< label for = "editPort" > 摰鮋� 靚� � 蝡臬藁</ label >< input class = "readonly-value" id = "editPort" readonly >< small > 隞� 銁撌脤� 霂� � 餈鞱� � 硋� � 典� � 𠉛𠶖� � 遬蝷綽� 甇文� � 芾粉� � </ small ></ div >< div class = "form-note" id = "editReadOnlyNote" hidden > 霂亙� 靘𧢲迤� 刻� 銵峕� 銝箏� � 典� � 𢛵� � 蛹� 踹� � 滨蔭銝舘� 銵諹澈隞賭� 銝� � 湛� 瘚讛� � 函掩� 卝� � ser Data Dir � 屸� � 厩垢� � 歇� � � � � </ div >< div class = "dialog-actions" >< button type = "button" class = "secondary" id = "editTabs" > � 亦� � � 倌憿� </ button >< button type = "button" class = "secondary" id = "editCancel" > � 𡝗� </ button >< button type = "submit" class = "primary" id = "editSave" > 靽嘥� � 湔㺿</ button ></ div ></ form ></ dialog >
< dialog class = "tabs-dialog" id = "tabsDialog" aria-labelledby = "tabsTitle" aria-describedby = "tabsMeta" >< div class = "dialog-inner" >< div >< h2 id = "tabsTitle" > � � 倌憿蛛� CDP 憿菟𢒰� 格� 嚗� </ h2 >< p id = "tabsSubtitle" > � 芾粉� 亦� 蝏誯� 霂� � � 祆㦤 CDP 憿菟𢒰� 格� � � </ p ></ div >< div class = "tabs-meta" id = "tabsMeta" > 銝滢� � 曄內 WebSocket � 啣� 嚗䔶� 銝滢� 撖潸⏛� � � � 啜� � � � 剝△� X� � 扯� � 𡁏𧋦� � </ div >< div class = "tabs-state" id = "tabsState" role = "status" aria-live = "polite" > 甇� 銁霂餃� 撌脤� 霂� � � 砍𧑐 CDP 憿菟𢒰� 格� � � </ div >< div class = "tabs-target-list" id = "tabsTargetList" hidden ></ div >< div class = "dialog-actions" >< button class = "primary" id = "tabsClose" > � 喲𡡒</ button ></ div ></ div ></ dialog >
2026-07-25 16:21:38 +08:00
< dialog id = "discardDialog" aria-labelledby = "discardTitle" >< div class = "dialog-inner" >< h2 id = "discardTitle" > � 曉� � 芯� 摮条� � 湔㺿嚗� </ h2 >< p > � 臭誑� � � 摮㗛� 蝵殷� � 𡝗𦆮撘� 𧋦甈∠� 颲穃僎� 喲𡡒撖寡� 獢� � � </ p >< div class = "dialog-actions" >< button class = "secondary" id = "keepEditing" > 蝏抒賒蝻𤥁� </ button >< button class = "secondary" id = "discardChanges" > 銝滢� 摮� </ button >< button class = "primary" id = "saveChanges" > 靽嘥� </ button ></ div ></ div ></ dialog >
2026-07-25 17:47:02 +08:00
< dialog id = "proxyDeleteDialog" aria-labelledby = "proxyDeleteTitle" >< div class = "dialog-inner" >< h2 id = "proxyDeleteTitle" > � 𣳇膄隞� � 嚗� </ h2 >< p id = "proxyDeleteText" ></ p >< div class = "dialog-actions" >< button class = "secondary" id = "proxyDeleteCancel" > � 𡝗� </ button >< button class = "danger-button" id = "proxyDeleteConfirm" > � 𣳇膄隞� � </ button ></ div ></ div ></ dialog >
2026-07-25 18:07:57 +08:00
< dialog id = "unexpectedExitDialog" aria-labelledby = "unexpectedExitTitle" aria-describedby = "unexpectedExitText" >< div class = "dialog-inner" >< h2 id = "unexpectedExitTitle" > 瘚讛� � 典歇� � � � </ h2 >< p id = "unexpectedExitText" ></ p >< ul class = "dialog-list" id = "unexpectedExitList" ></ ul >< div class = "form-note" > 摰硺� � 滨蔭� � User Data Dir � 芾◤� 𣳇膄� � � � 剜迨� 鞟內� 𠬍� � 臭誑隞𤾸� 靘见� 銵券� � 啣鍳� 冽� 閫� 膥� � </ div >< div class = "dialog-actions" >< button class = "primary" id = "unexpectedExitAcknowledge" > � 仿� 鈭� </ button ></ div ></ div ></ dialog >
2026-07-22 14:51:11 +08:00
< div class = "toast" id = "toast" role = "status" aria-live = "polite" ></ div >
< script >
2026-07-25 16:53:44 +08:00
const proxies = [
{ id : 'proxy-sg' , name : '� 啣� � ∪枂� � ' , server : 'http://127.0.0.1:8080' },
{ id : 'proxy-test' , name : '瘚贝� SOCKS5' , server : 'socks5://127.0.0.1:1080' }
];
2026-07-22 14:51:11 +08:00
const instances = [
2026-07-27 09:29:10 +08:00
{ id : 'inst-chrome-shop' , name : 'Shopee 繚 SG 銝餃� ' , kind : 'chrome' , browser : 'Chrome' , status : 'running' , statusText : '餈鞱� 銝哨� � 条恣嚗� ' , pid : '18420' , port : 9666 , preferredPort : 9666 , dir : 'D:\\Chub\\profiles\\shopee-sg' , started : '隞𠰴予 09:42' , url : 'https://seller.shopee.sg/' , proxyId : 'proxy-sg' , executable : 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' },
{ id : 'inst-edge-review' , name : '蝝䭾� 摰⊥瓲 繚 Edge' , kind : 'edge' , browser : 'Edge' , status : 'starting' , statusText : '� 臬𢆡銝� ' , pid : '� � ' , port : 0 , preferredPort : 9667 , dir : 'D:\\Chub\\profiles\\asset-review' , started : '甇� 銁� 臬𢆡' , url : 'https://example.com/review' , executable : 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe' },
{ id : 'inst-chrome-ph' , name : 'Shopee 繚 PH 憭� 鍂' , kind : 'chrome' , browser : 'Chrome' , status : 'associated' , statusText : '憭㚚� 撌脣� � � ' , pid : '16108' , port : 9668 , preferredPort : 9668 , dir : 'D:\\Chub\\profiles\\shopee-ph' , started : '� 典予 18:16' , url : 'https://seller.shopee.ph/' , executable : 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' },
{ id : 'inst-edge-test' , name : '瘚贝� � 臬� 繚 Edge' , kind : 'edge' , browser : 'Edge' , status : 'exited' , statusText : '撌脤� � � � ' , pid : '� � ' , port : 0 , preferredPort : 9669 , dir : 'D:\\Chub\\profiles\\edge-test' , started : '� 典予 16:04' , url : 'https://localhost:3000' , executable : 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe' , exit : '� � � 箇� 0' }
2026-07-22 14:51:11 +08:00
];
let selectedId = 'inst-chrome-shop' ;
let modalAction = 'stop' ;
2026-07-25 16:21:38 +08:00
let editingID = '' ;
let editSnapshot = null ;
2026-07-25 17:47:02 +08:00
let selectedProxyID = '' ;
2026-07-25 18:07:57 +08:00
let unexpectedExitIDs = [];
let unexpectedExitFocusID = '' ;
2026-07-27 10:01:07 +08:00
let tabsRequest = 0 ;
2026-07-27 22:59:10 +08:00
let pendingInstanceDeleteID = '' ;
let instanceDeleteRunning = false ;
2026-07-22 14:51:11 +08:00
const $ = ( id ) => document . getElementById ( id );
const statusClass = ( s ) => s ;
2026-07-25 17:47:02 +08:00
const proxyLabel = ( id ) => proxies . find ( p => p . id === id ) ? . server || '� 牐誨� � ' ;
2026-07-27 09:29:10 +08:00
const portLabel = ( x ) => x . status === 'associated' ? ( x . port ? `憭㚚� ${ x . port } ` : '� � ' ) : x . status === 'starting' ? ( x . preferredPort ? `� � � ${ x . preferredPort } ` : '� � ' ) : x . port ? `摰鮋� ${ x . port } ` : x . status === 'exited' ? ( x . preferredPort ? `撱箄悅 ${ x . preferredPort } ` : '� � ' ) : '� � ' ;
2026-07-25 17:47:02 +08:00
function renderProxyOptions ( select , selected = '' ) { select . innerHTML = '<option value="">� 牐誨� � </option>' + proxies . map ( p => `<option value=" ${ p . id } "> ${ p . server } </option>` ). join ( '' ); select . value = selected ; }
function renderProxies () { const select = $ ( 'proxySelect' ), input = $ ( 'proxyServer' ), status = $ ( 'proxyStatus' ), remove = $ ( 'proxyDelete' ); select . innerHTML = '<option value="">� 㗇𥋘隞� � 嚗� 鰵撱綽� </option>' + proxies . map ( p => `<option value=" ${ p . id } "> ${ p . server } </option>` ). join ( '' ); if ( ! proxies . some ( p => p . id === selectedProxyID )) selectedProxyID = '' ; select . value = selectedProxyID ; const selected = proxies . find ( p => p . id === selectedProxyID ); input . value = selected ? . server || '' ; remove . disabled = ! selected ; status . textContent = selected ? `甇� 銁蝻𤥁� 撌脖� 摮䀝誨� � � 撌脰◤ ${ instances . filter ( i => i . proxyId === selected . id ). length } 銝芸� 靘衤蝙� 具� � : '� 啣遣隞� � 嚗𡁜‵� 坔𧑐� � � 𦒘� 摮塩� � '; }
2026-07-22 14:51:11 +08:00
function filtered() {
const q = $('search').value.trim().toLowerCase(), browser = $('browserFilter').value, status = $('statusFilter').value;
2026-07-27 09:29:10 +08:00
return instances.filter(x => (!q || [x.name, x.browser, x.pid, portLabel(x), x.dir].join(' ').toLowerCase().includes(q)) && (browser === 'all' || x.kind === browser) && (status === 'all' || x.status === status));
2026-07-22 14:51:11 +08:00
}
2026-07-25 16:21:38 +08:00
function selectRow(id, body) {
selectedId = id;
body.querySelectorAll('tr').forEach(row => row.classList.toggle('selected', row.dataset.id === id));
}
2026-07-22 14:51:11 +08:00
function renderRows() {
const rows = filtered(), body = $('instanceRows'); body.innerHTML = '';
2026-07-25 16:21:38 +08:00
rows.forEach(x => { const tr = document.createElement('tr'); tr.className = x.id === selectedId ? 'selected' : ''; tr.tabIndex = 0; tr.dataset.id = x.id; tr.title = '� � 稬� 𡝗� Enter 蝻𤥁� 摰硺� '; tr.innerHTML = `
2026-07-22 16:22:59 +08:00
< td >< div class = "instance-name" >< span class = "browser-logo ${x.kind}" aria - hidden = "true" > $ { x . kind === 'chrome' ? 'C' : 'E' } < /span><div><strong>${x.name}</strong><small>${x.id}</small></div></div></td>
2026-07-27 09:29:10 +08:00
< td > $ { x . browser } < /td><td class="mono">${x.dir}</td><td class="mono">${portLabel(x)}</td><td><span class="status ${statusClass(x.status)}">${x.statusText}</span></td>
2026-07-25 17:47:02 +08:00
< td >< div class = "row-actions" >< button data - action = "start" aria - label = "${x.status === 'associated' ? '� 齿鰵璉� 瘚� ' : x.status === 'running' ? '� 𨀣迫' : '� 臬𢆡'} ${x.name}" > $ { x . status === 'associated' ? '� 齿鰵璉� 瘚� ' : x . status === 'running' ? '� � � 𨀣迫' : '� � � 臬𢆡' } < /button><button data-action="edit" aria-label="蝻𤥁� ${x.name}">蝻𤥁� </button><button data-action="delete" aria-label="� 𣳇膄 ${x.name}">� 𣳇膄</button></div></td>`;
2026-07-25 16:21:38 +08:00
tr . addEventListener ( 'click' , e => { if ( ! e . target . closest ( 'button' )) selectRow ( x . id , body ); });
tr . addEventListener ( 'dblclick' , e => { if ( ! e . target . closest ( 'button' )) openEdit ( x . id ); });
tr . addEventListener ( 'keydown' , e => { if ( e . key === 'Enter' ) { e . preventDefault (); openEdit ( x . id ); } else if ( e . key === ' ' ) { e . preventDefault (); selectRow ( x . id , body ); } });
2026-07-27 22:59:10 +08:00
tr . querySelectorAll ( 'button' ). forEach ( b => b . addEventListener ( 'click' , e => { e . stopPropagation (); selectRow ( x . id , body ); if ( b . dataset . action === 'edit' ) openEdit ( x . id ); else if ( b . dataset . action === 'delete' ) openInstanceDelete ( x . id ); else if ( x . status === 'running' ) { x . status = 'stopping' ; x . statusText = '� 𨀣迫銝� ' ; renderRows (); setTimeout (() => { x . status = 'exited' ; x . statusText = '撌脤� � � � ' ; x . pid = '� � ' ; x . port = 0 ; renderRows (); showToast ( `� � ${ x . name } � 嘥歇隡㗛� � � � 箝� � ); }, 700); } else openConfirm(x.id, 'start'); }));
2026-07-22 14:51:11 +08:00
body.appendChild(tr);
});
$('emptyState').style.display = rows.length ? 'none' : 'block'; $('resultCount').textContent = ` � 曄內 $ { rows . length } 銝芸 � 靘㞗 ;
}
2026-07-27 09:29:10 +08:00
function openDetails ( id ) { selectedId = id ; const x = instances . find ( i => i . id === id ), externallyAssociated = x . status === 'associated' ; $ ( 'drawer' ). innerHTML = `<div class="drawer-head"><div><h2> ${ x . name } </h2><p> ${ x . browser } 繚 ${ x . id } </p></div><button class="icon-button" id="closeDrawer" aria-label="� 喲𡡒霂行� ">� </button></div><div class="drawer-body"><div><span class="status ${ x . status } "> ${ x . statusText } </span></div><div class="detail-card"><div class="detail-line"><span>餈𤤿� PID</span><span class="mono"> ${ x . pid } </span></div><div class="detail-line"><span>靚� � 蝡臬藁</span><span class="mono"> ${ portLabel ( x ) } </span></div><div class="detail-line"><span>瘚讛� � � </span><span> ${ x . browser } </span></div><div class="detail-line"><span>user data dir</span><span class="mono"> ${ x . dir } </span></div><div class="detail-line"><span>� 臬𢆡� 啣� </span><span class="mono"> ${ x . url } </span></div><div class="detail-line"><span>� 舀� 銵峕� 隞� </span><span class="mono"> ${ x . executable } </span></div> ${ x . exit ? `<div class="detail-line"><span>� � � 箇� � � </span><span> ${ x . exit } </span></div>` : '' } </div><div class="form-section"><h3>摰硺� � 滢� </h3><div class="drawer-actions" style="border-top:0;padding-top:0;justify-content:flex-start"><button class="secondary" id="restartBtn" ${ x . status === 'starting' || externallyAssociated ? 'disabled' : '' } >� � � 滚鍳</button><button class="danger-button" id="stopBtn" ${ x . status === 'exited' || externallyAssociated ? 'disabled' : '' } >� 喲𡡒摰硺� </button></div> ${ externallyAssociated ? '<small class="help">憭㚚� � 唾� 摰硺� 隞� � 閫� � 嚗龦hub 銝滢� � 亦恣� 嗥� � 賢𪂹� 麄� � </small>' : '' } </div><div class="form-section"><h3>� � 餈烐暑� � </h3><div class="activity"><div class="activity-item"><i></i><span>� 嗆� � 歇� 峕郊 繚 ${ x . started } </span></div><div class="activity-item"><i></i><span> ${ externallyAssociated ? '撌脤� 霂� 𧋦� � CDP 蝡舐� 嚗𥟇𧊋� 亦恣憭㚚� 瘚讛� � 具� � ' : x . status === 'occupied' ? '璉� 瘚见� 憭㚚� 餈𤤿� � 删鍂霂� user data dir' : '摰硺� � 滨蔭撌脤� 朞� 頨思遢� ⊿� ' } </span></div></div></div></div>` ; showDrawer (); $ ( 'closeDrawer' ). onclick = hideDrawer ; $ ( 'stopBtn' ). onclick = () => openConfirm ( x . id , false ); $ ( 'restartBtn' ). onclick = () => openConfirm ( x . id , 'restart' ); }
2026-07-25 16:21:38 +08:00
function editIsLocked ( x ) { return [ 'running' , 'starting' , 'associated' ]. includes ( x . status ); }
function editIsDirty () {
if ( ! editingID || ! editSnapshot ) return false ;
2026-07-27 09:29:10 +08:00
return $ ( 'editName' ). value !== editSnapshot . name || $ ( 'editDataDir' ). value !== editSnapshot . dir || $ ( 'editURL' ). value !== editSnapshot . url || $ ( 'editPreferredPort' ). value !== String ( editSnapshot . preferredPort ) || $ ( 'editProxy' ). value !== editSnapshot . proxyId || document . querySelector ( 'input[name="editKind"]:checked' ) ? . value !== editSnapshot . kind ;
2026-07-25 16:21:38 +08:00
}
function focusEditedRow ( id ) { requestAnimationFrame (() => document . querySelector ( `tr[data-id=" ${ id } "]` ) ? . focus ()); }
2026-07-27 10:01:07 +08:00
function finishEdit () { if ( $ ( 'tabsDialog' ). open ) $ ( 'tabsDialog' ). close (); const id = editingID ; $ ( 'editDialog' ). close (); editingID = '' ; editSnapshot = null ; focusEditedRow ( id ); }
2026-07-25 16:21:38 +08:00
function openEdit ( id ) {
const x = instances . find ( i => i . id === id ); if ( ! x ) return ;
2026-07-27 09:29:10 +08:00
selectedId = id ; editingID = id ; editSnapshot = { name : x . name , dir : x . dir , url : x . url || '' , kind : x . kind , preferredPort : x . preferredPort || '' , proxyId : x . proxyId || '' };
const locked = editIsLocked ( x ); $ ( 'editName' ). value = x . name ; $ ( 'editDataDir' ). value = x . dir ; $ ( 'editURL' ). value = x . url || '' ; $ ( 'editPreferredPort' ). value = x . preferredPort || '' ; renderProxyOptions ( $ ( 'editProxy' ), x . proxyId || '' ); $ ( 'editPort' ). value = x . port || '� � ' ; $ ( 'editChrome' ). checked = x . kind === 'chrome' ; $ ( 'editEdge' ). checked = x . kind === 'edge' ;
2026-07-25 16:21:38 +08:00
$ ( 'editSubtitle' ). textContent = locked ? '敶枏� 餈鞱� � 嗆� � � � 舀䰻� 钅� 蝵殷� 雿� 澈隞賜漲� 笔� 畾萎� � 臭耨� 嫘� � ' : '靽格㺿銝𧢲活� 臬𢆡雿輻鍂� � � 摮㗛� 蝵柴� � ' ;
2026-07-27 10:01:07 +08:00
const canInspectTabs = [ 'running' , 'associated' ]. includes ( x . status ) && Number ( x . port ) > 0 ;
$ ( 'editChrome' ). disabled = locked ; $ ( 'editEdge' ). disabled = locked ; $ ( 'editDataDir' ). disabled = locked ; $ ( 'chooseEditDir' ). disabled = locked ; $ ( 'editPreferredPort' ). disabled = locked ; $ ( 'editTabs' ). disabled = ! canInspectTabs ; $ ( 'editTabs' ). title = canInspectTabs ? '� 芾粉� 亦� 撌脤� 霂� � CDP 憿菟𢒰� 格� ' : '� 亦� � � 倌憿菟� 閬� 歇撉諹� � � � 銵峕� 憭㚚� � 唾� � 嗆� � � 摰鮋� 靚� � 蝡臬藁' ; $ ( 'editReadOnlyNote' ). hidden = ! locked ;
2026-07-25 16:21:38 +08:00
$ ( 'editDialog' ). showModal (); setTimeout (() => $ ( 'editName' ). focus (), 0 );
}
function saveEdit () {
const x = instances . find ( i => i . id === editingID ); if ( ! x ) return finishEdit ();
2026-07-27 09:29:10 +08:00
const name = $ ( 'editName' ). value . trim (), dir = $ ( 'editDataDir' ). value . trim (), url = $ ( 'editURL' ). value . trim (), preferredPort = Number ( $ ( 'editPreferredPort' ). value );
2026-07-25 16:21:38 +08:00
if ( ! name || ! dir ) { showToast ( '霂瑕‵� 坔� 靘见� 蝘啣� User Data Dir� � ' ); ( ! name ? $ ( 'editName' ) : $ ( 'editDataDir' )). focus (); return ; }
2026-07-27 23:50:24 +08:00
if ( instances . some ( item => item . id !== x . id && item . name . trim (). toLocaleLowerCase () === name . toLocaleLowerCase ())) { showToast ( '摰硺� � 滨妍撌脣� � 剁� 霂瑟凒� U� � ' ); $ ( 'editName' ). focus (); return ; }
2026-07-25 16:53:44 +08:00
const locked = editIsLocked ( x ); x . name = name ; x . url = url ; x . proxyId = $ ( 'editProxy' ). value ;
2026-07-27 09:29:10 +08:00
if ( ! locked ) { if ( ! Number . isInteger ( preferredPort ) || preferredPort < 1024 || preferredPort > 65535 ) { showToast ( '霂瑕‵� 蹱� � � � 擐㚚� 㕑� 霂閧垢� � � � ' ); $ ( 'editPreferredPort' ). focus (); return ; } x . dir = dir ; x . kind = document . querySelector ( 'input[name="editKind"]:checked' ) ? . value || x . kind ; x . browser = x . kind === 'edge' ? 'Edge' : 'Chrome' ; x . preferredPort = preferredPort ; }
2026-07-25 16:21:38 +08:00
if ( $ ( 'discardDialog' ). open ) $ ( 'discardDialog' ). close (); renderRows (); const savedName = x . name ; finishEdit (); showToast ( `撌脖� 摮覀� � ${ savedName } � 萘� 摰硺� � 滨蔭� � );
}
function requestEditClose() { if (editIsDirty()) { $('editDialog').close(); $('discardDialog').showModal(); return; } finishEdit(); }
2026-07-27 10:01:07 +08:00
function closeTabs() { tabsRequest++; if ($('tabsDialog').open) $('tabsDialog').close(); if ($('editDialog').open) setTimeout(() => $('editTabs').focus(), 0); }
function openTabs() {
const x = instances.find(i => i.id === editingID);
if (!x || !['running', 'associated'].includes(x.status) || !Number(x.port)) { showToast('� 亦� � � 倌憿菟� 閬� � 鈭𤾸歇撉諹� � � � 銵峕� 憭㚚� � 唾� � 嗆� � � 撟嗅� � 匧� � � � 霂閧垢� � � � '); return; }
const request = ++tabsRequest;
$('tabsTitle').textContent = '� � 倌憿蛛� CDP 憿菟𢒰� 格� 嚗� ';
$('tabsSubtitle').textContent = ` $ { x . name } 繚 摰鮋 � 蝡臬藁 $ { x . port } 繚 � 芾粉 � 亦 � `;
$('tabsState').hidden = false; $('tabsState').textContent = '甇� 銁霂餃� 撌脤� 霂� � � 砍𧑐 CDP 憿菟𢒰� 格� � � '; $('tabsTargetList').hidden = true; $('tabsTargetList').innerHTML = '';
$('tabsDialog').showModal(); setTimeout(() => $('tabsClose').focus(), 0);
setTimeout(() => {
if (request !== tabsRequest || !$('tabsDialog').open || editingID !== x.id) return;
const targets = x.kind === 'edge'
? [{ type: 'page', title: '蝝䭾� 摰⊥瓲', url: 'https://example.com/review' }, { type: 'service_worker', title: '嚗� � � � � 嚗� ', url: 'chrome-extension://example/worker.js' }]
: [{ type: 'page', title: '餈鞱𨯫� 𤾸蝱', url: 'https://seller.shopee.sg/' }, { type: 'page', title: '霈W� 銝剖� ', url: 'https://seller.shopee.sg/orders' }];
$('tabsState').textContent = targets.length ? '撌脰粉� 硋蘨霂� CDP 憿菟𢒰� 格� � � RL 撌脩宏� 斤鍂� 瑚縑� 胯� � 䰻霂W� � 啣� � � 挾� � ' : '� 芸� � 啣虾� 曄內� � CDP 憿菟𢒰� 格� � � ';
$('tabsTargetList').innerHTML = targets.map(target => ` < article class = "tabs-target" >< small > $ { target . type } < /small><strong>${target.title}</strong><code>${target.url}</code></article>`).join('');
$ ( 'tabsTargetList' ). hidden = ! targets . length ;
}, 380 );
}
2026-07-27 23:50:24 +08:00
function openLaunch () { $ ( 'drawer' ). innerHTML = `<div class="drawer-head"><div><h2>� 啣遣瘚讛� � 典� 靘� </h2><p>� 滨蔭摰峕� � 𠬍� Chub 隡𡁜銁� 祆㦤� 臬𢆡銝� 銝芷� 蝳餌㴓憓� � � </p></div><button class="icon-button" id="closeDrawer" aria-label="� 喲𡡒� 啣遣摰硺� ">� </button></div><form class="drawer-body instance-form-workspace" id="launchForm"><div class="form-section"><h3>瘚讛� � 券� 蝵� </h3><div class="field"><label for="instanceName">摰硺� � 滨妍 <span>*</span></label><input id="instanceName" placeholder="靘见� 嚗𡁜恣� � Edge" required><small>� 滨妍敹� ◆� 臭� 嚗𤤿𤌍敶蓥� 雿輻鍂摰匧� � 硋� 蝘啣� 蝔喳� ID嚗䔶� � 擧㺿� 滢� 隡𡁶宏� 典歇� 㗇㺭� 柴� � </small></div><div class="field"><span>瘚讛� � 函掩� � <span>*</span></span><div class="radio-row" role="radiogroup" aria-label="瘚讛� � 函掩� � "><label><input type="radio" name="kind" value="chrome" checked> Chrome</label><label><input type="radio" name="kind" value="edge"> Edge</label></div></div><div class="field"><label for="exe">� 舀� 銵峕� 隞� </label><input id="exe" value="� 芸𢆡� 𤑳緵" aria-describedby="exeHelp"><small id="exeHelp">� 嗵征� 嗆� 瘚讛� � 函掩� 𧢲� 蝝X� � � � 鋆� 楝敺� � � </small></div></div><div class="form-section"><h3>餈鞱� � 臬� </h3><div class="field"><label for="dataDir">User Data Dir <span>*</span></label><div class="path-field"><input id="dataDir" value="D:\\Tools\\chub\\user_data_dirs\\instance-摰⊥瓲-7f3a21" required><button type="button" class="secondary" id="chooseInstanceDir">� 㗇𥋘� 桀� </button></div><small>暺䁅恕頝罸� 蝔见� � 桀� 嚗𥕦鍳� 典� 隡𡁻� 霂� 𤌍敶訫虾� 辷� � 䔶� � 桀� 銝滩� 鋡怠� 銝芸� 靘见� � 嗡蝙� 具� � </small></div><div class="field"><label for="profile">profile-directory</label><input id="profile" value="Default"></div><div class="field"><label for="url">� 臬𢆡 URL嚗� 虾� 㚁� </label><input id="url" type="url" placeholder="https://example.com"><small>隞� 𣈲� � http/https嚗𤤿� 蝛箸𧒄� 臬𢆡瘚讛� � 券� 霈日△� � </small></div><div class="field"><label for="launchPreferredPort">擐㚚� 㕑� 霂閧垢� � </label><input id="launchPreferredPort" type="number" min="1024" max="65535" value="9670" inputmode="numeric"><small>蝟餌� 隞擧迨� 澆� 憪见遣霈格𧊋鋡怠� 隞� Chub 摰硺� 憸� � � � 垢� � � 摰鮋� 蝡臬藁隡𡁜銁� 臬𢆡� 園� 霂� � � </small></div><div class="field"><label for="launchProxy">隞� � </label><select id="launchProxy"><option value="">� 牐誨� � </option></select><small>� 㗇𥋘霈曄蔭銝凋� 摮条� � 㰘恕霂� 誨� � � 蝡舐� 隡𡁜銁� 臬𢆡� 嗅� � 其� � 鉝� � </small></div></div><div class="form-section"><h3>� 臬𢆡� 厰★</h3><label class="check-line"><input type="checkbox" id="headless"> <span><b>� 惩仍璅∪� </b><br><small class="help">� 臬𢆡� 𤾸蝱瘚讛� � 剁� 銝齿遬蝷箇� � � � � </small></span></label><label class="check-line"><input type="checkbox" id="remember" checked> <span><b>靽嘥� 甇日� 蝵� </b><br><small class="help">� 芯� 摮䁅楝敺� � � 墧� � 笔� � 啜� � </small></span></label></div><div class="drawer-actions"><button type="button" class="secondary" id="cancelLaunch">� 𡝗� </button><button type="submit" class="primary">� 臬𢆡摰硺� </button></div></form>` ; showDrawer (); renderProxyOptions ( $ ( 'launchProxy' )); $ ( 'closeDrawer' ). onclick = hideDrawer ; $ ( 'cancelLaunch' ). onclick = hideDrawer ; $ ( 'chooseInstanceDir' ). onclick = () => showToast ( '� 笔� 瞍𠉛內嚗𡁜� � 枏� � 桀� � 㗇𥋘� 具� � ' ); $ ( 'launchForm' ). onsubmit = e => { e . preventDefault (); const name = $ ( 'instanceName' ), data = $ ( 'dataDir' ), port = Number ( $ ( 'launchPreferredPort' ). value ); if ( ! name . value . trim ()) { name . focus (); showToast ( '霂瑁� � 亙� 靘见� 蝘啜� � ' ); return ; } if ( instances . some ( item => item . name . trim (). toLocaleLowerCase () === name . value . trim (). toLocaleLowerCase ())) { name . focus (); showToast ( '摰硺� � 滨妍撌脣� � 剁� 霂瑟凒� U� � ' ); return ; } if ( ! data . value . trim ()) { data . focus (); data . parentElement . classList . add ( 'error' ); const msg = document . createElement ( 'div' ); msg . className = 'field-error' ; msg . textContent = '霂瑕‵� � User Data Dir� � ' ; data . parentElement . appendChild ( msg ); return ; } if ( ! Number . isInteger ( port ) || port < 1024 || port > 65535 ) { $ ( 'launchPreferredPort' ). focus (); showToast ( '霂瑕
2026-07-22 14:51:11 +08:00
function showDrawer() { $(' drawer ').classList.add(' open '); $(' backdrop ').classList.add(' open '); $(' drawer ').setAttribute(' aria - hidden ', ' false '); setTimeout(() => $(' drawer ').querySelector(' button , input , select ')?.focus(), 60); }
function hideDrawer() { $(' drawer ').classList.remove(' open '); $(' backdrop ').classList.remove(' open '); $(' drawer ').setAttribute(' aria - hidden ', ' true '); }
function openConfirm(id, action) { const x = instances.find(i => i.id === id); modalAction = action; $(' dialogTitle ').textContent = action === ' restart ' ? ' � 滚鍳瘚讛 � � 典 � 靘页 � ' : action === ' start ' ? ' � 臬𢆡瘚讛 � � 典 � 靘页 � ' : ' � 喲𡡒瘚讛 � � 典 � 靘页 � '; $(' dialogText ').textContent = action === ' restart ' ? `撠� � 霂瑟� � � ${x.name}� 脲迤撣賊� � � 綽� � 滢蝙� 函㮾� 屸� 蝵桅� � 啣鍳� 具� � : action === ' start ' ? `撠� 蝙� 典歇靽嘥� � 滨蔭� 臬𢆡� � ${x.name}� 腈� � : `撠� 窈瘙� � � ${x.name}� 脲迤撣賊� � � 箝� � 𧊋靽嘥� � � � 憿萄� 摰寧眏瘚讛� � 刻䌊銵� � � � � � ; $(' dialogWarning ').hidden = action !== false; $(' dialogConfirm ').textContent = action === false ? ' 隡㗛 � � 喲𡡒 ' : action === ' restart ' ? ' 蝖株恕 � 滚鍳 ' : ' � 臬𢆡摰硺 � '; $(' dialogConfirm ').className = action === false ? ' danger - button ' : ' primary '; $(' confirmDialog ').showModal(); }
2026-07-27 22:59:10 +08:00
function canDeleteInstanceData(x) { return [' exited ', ' failed '].includes(x.status); }
function updateInstanceDeleteChoice() { const x = instances.find(i => i.id === pendingInstanceDeleteID), checked = $(' deleteInstanceData ').checked && !$(' deleteInstanceData ').disabled; $(' instanceDeleteWarning ').hidden = !checked; $(' instanceDeleteWarning ').textContent = checked && x ? `撠� 偶銋� � � 歹� ${x.dir}� � 迨� 滢� � 䭾� � 日� 嚗𡱰hub 隞滢� � 冽� 銵� � � 齿鰵璉� � 交� 閫� 膥� 删鍂� � : ''; $(' instanceDeleteConfirm ').textContent = checked ? ' � 𣳇膄摰硺 � � 𦠜㺭 � � ' : ' � 𣳇膄摰硺 � '; }
function openInstanceDelete(id) { const x = instances.find(i => i.id === id); if (!x) return; pendingInstanceDeleteID = id; instanceDeleteRunning = false; const allowed = canDeleteInstanceData(x); $(' instanceDeleteText ').textContent = `蝖桀� � 𣳇膄� � ${x.name}� 萘� Chub 摰硺� � 滨蔭� 梹� 暺䁅恕隡帋� � � User Data Dir� � ; $(' deleteInstanceData ').checked = false; $(' deleteInstanceData ').disabled = !allowed; $(' instanceDeleteUnavailable ').hidden = allowed; $(' instanceDeleteUnavailable ').textContent = allowed ? '' : ' 璉 � 瘚见 � 瘚讛 � � 冽迤 � 刻 � 銵䎚 � � 鍳 � � / � 𨀣迫 � 硋 � � 剁 � 霂瑕 � � 𨀣迫撟嗅 � � 啁𠶖 � � � � 滚 � � 斗㺭 � 柴 � � � � 滢 � � 臬蘨 � 𣳇膄摰硺 � � 滨蔭 � � '; $(' instanceDeleteCancel ').disabled = false; $(' instanceDeleteConfirm ').disabled = false; updateInstanceDeleteChoice(); $(' instanceDeleteDialog ').showModal(); setTimeout(() => $(' instanceDeleteCancel ').focus(), 0); }
function closeInstanceDelete() { if (instanceDeleteRunning) return; $(' instanceDeleteDialog ').close(); pendingInstanceDeleteID = ''; }
$(' deleteInstanceData ').onchange = updateInstanceDeleteChoice;
$(' instanceDeleteCancel ').onclick = closeInstanceDelete;
$(' instanceDeleteDialog ').addEventListener(' cancel ', e => { e.preventDefault(); closeInstanceDelete(); });
$(' instanceDeleteConfirm ').onclick = () => { const x = instances.find(i => i.id === pendingInstanceDeleteID), withData = $(' deleteInstanceData ').checked && !$(' deleteInstanceData ').disabled; if (!x || instanceDeleteRunning) return; if (!withData) { instances.splice(instances.indexOf(x), 1); $(' instanceDeleteDialog ').close(); pendingInstanceDeleteID = ''; renderRows(); showToast(`撌脣� � 手� � ${x.name}� 萘� 摰硺� � 滨蔭嚗𢥧ser Data Dir � 芾◤� 𣳇膄� � ); return; } instanceDeleteRunning = true; $(' instanceDeleteCancel ').disabled = true; $(' instanceDeleteConfirm ').disabled = true; $(' instanceDeleteConfirm ').textContent = ' 甇 � 銁 � 𣳇膄 � 唳旿 � � '; setTimeout(() => { const current = instances.find(i => i.id === pendingInstanceDeleteID); if (current) instances.splice(instances.indexOf(current), 1); $(' instanceDeleteDialog ').close(); pendingInstanceDeleteID = ''; instanceDeleteRunning = false; renderRows(); showToast(`� 笔� 瞍𠉛內嚗𡁜歇� 𣳇膄� � ${x.name}� 萘� 摰硺� � 滨蔭� � User Data Dir � 唳旿� � ); }, 700); };
2026-07-22 14:51:11 +08:00
function showToast(message) { const toast = $(' toast '); toast.textContent = message; toast.classList.add(' show '); clearTimeout(showToast.timer); showToast.timer = setTimeout(() => toast.classList.remove(' show '), 3600); }
2026-07-25 18:07:57 +08:00
function hasOtherModal() { return [...document.querySelectorAll(' dialog [ open ] ')].some(dialog => dialog.id !== ' unexpectedExitDialog '); }
function queueUnexpectedExit(id) {
const instance = instances.find(x => x.id === id);
if (!instance || instance.status !== ' running ') return;
2026-07-27 09:29:10 +08:00
instance.status = ' exited '; instance.statusText = ' 撌脤 � � � � '; instance.pid = ' � � '; instance.port = 0; instance.exit = ' 撌脫 � 瘚见 � 餈𤤿 � � � � � ';
2026-07-25 18:07:57 +08:00
if (!unexpectedExitIDs.includes(id)) unexpectedExitIDs.push(id);
selectedId = id;
renderRows();
presentUnexpectedExitNotice();
}
function presentUnexpectedExitNotice() {
const dialog = $(' unexpectedExitDialog ');
const exited = unexpectedExitIDs.map(id => instances.find(x => x.id === id)).filter(Boolean);
if (!exited.length || hasOtherModal()) return;
$(' unexpectedExitTitle ').textContent = exited.length === 1 ? ' 瘚讛 � � 典歇 � � � � ' : `${exited.length} 銝芣� 閫� 膥撌脤� � � 槁;
$(' unexpectedExitText ').textContent = exited.length === 1 ? `璉� 瘚见� � � ${exited[0].name}� 萘� ${exited[0].browser} 撌脤� � � 箝� � : ' 璉 � 瘚见 � 隞乩 � � � Chub � 臬𢆡 � � � 閫 � 膥撌脤 � � � 箝 � � ';
$(' unexpectedExitList ').innerHTML = exited.map(x => `<li><strong>${x.name}</strong> 繚 ${x.browser}</li>`).join('');
unexpectedExitFocusID = exited[0].id;
if (!dialog.open) { dialog.showModal(); setTimeout(() => $(' unexpectedExitAcknowledge ').focus(), 0); }
}
function dismissUnexpectedExitNotice() {
const focusID = unexpectedExitFocusID;
unexpectedExitIDs = [];
$(' unexpectedExitDialog ').close();
requestAnimationFrame(() => {
const start = document.querySelector(`tr[data-id="${focusID}"] button[data-action="start"]`);
(start || document.querySelector(`tr[data-id="${focusID}"]`))?.focus();
});
}
2026-07-22 14:51:11 +08:00
$(' dialogCancel ').onclick = () => $(' confirmDialog ').close(); $(' confirmDialog ').addEventListener(' cancel ', e => { e.preventDefault(); $(' confirmDialog ').close(); }); $(' dialogConfirm ').onclick = () => { $(' confirmDialog ').close(); const message = modalAction === false ? ' � 喲𡡒霂瑟 � 撌脫 � 鈭歹 � 摰硺 � 撠 � � � � STOPPING � � ' : modalAction === ' restart ' ? ' � 滚鍳霂瑟 � 撌脫 � 鈭歹 � 甇 � 銁蝑匧 � � 批 � 靘钅 � � � 箝 � � ' : ' � 臬𢆡霂瑟 � 撌脫 � 鈭歹 � 摰硺 � 撠 � � � � STARTING � � '; setTimeout(() => showToast(message), 80); };
2026-07-25 18:07:57 +08:00
$(' unexpectedExitAcknowledge ').onclick = dismissUnexpectedExitNotice; $(' unexpectedExitDialog ').addEventListener(' cancel ', e => { e.preventDefault(); dismissUnexpectedExitNotice(); }); document.querySelectorAll(' dialog ').forEach(dialog => dialog.addEventListener(' close ', () => setTimeout(presentUnexpectedExitNotice, 0)));
2026-07-25 16:21:38 +08:00
function refreshInstances() { const button = $(' refresh '); if (button.disabled) return; button.disabled = true; button.textContent = ' � � � 瑟鰵銝凌 � � '; setTimeout(() => { const starting = instances.find(x => x.status === ' starting '); if (starting) { starting.status = ' running '; starting.statusText = ' 餈鞱 � 銝哨 � � 条恣嚗 � '; starting.pid = ' 18468 '; starting.port = ' 9667 '; } renderRows(); button.disabled = false; button.textContent = ' � � � 瑟鰵 � 嗆 � � '; document.querySelector(' . table - footer span : last - child ').textContent = ' � � � 擧 � � 伐 � � 𡁜 � '; showToast(' 撌脫 � � 乩 � 摮条 � 摰硺 � � 嗆 � � � � 芣醌 � 誩 � 隞 � Chrome � � Edge � � '); }, 900); }
function setPage(page) { const settings = page === ' settings '; $(' instancesPage ').classList.toggle(' page - hidden ', settings); $(' settingsPage ').classList.toggle(' page - hidden ', !settings); $(' pageEyebrow ').textContent = settings ? ' Application settings ' : ' Browser instances '; $(' pageTitle ').textContent = settings ? ' 霈曄蔭 ' : ' 瘚讛 � � 典 � 靘 � '; $(' pageSubtitle ').textContent = settings ? ' 蝞 ∠� 瘚讛 � � 刻楝敺 � � � � 霈斤𤌍敶訫 � � 砍𧑐餈鞱 � 銵䔶蛹 ' : ' � 臬𢆡 � � � 撖笔僎摰匧 � 蝞 ∠� Chrome 銝 � Edge � 臬 � '; $(' instanceCommands ').classList.toggle(' page - hidden ', settings); if (settings) $(' chromePath ').focus(); }
$(' newInstance ').onclick = openLaunch; $(' backdrop ').onclick = hideDrawer; $(' search ').oninput = renderRows; $(' browserFilter ').onchange = renderRows; $(' statusFilter ').onchange = renderRows; $(' refresh ').onclick = refreshInstances; $(' themeToggle ').onclick = () => { const dark = document.documentElement.dataset.theme === ' dark '; document.documentElement.dataset.theme = dark ? '' : ' dark '; $(' themeToggle ').textContent = dark ? ' � � ' : ' � � '; $(' themeToggle ').setAttribute(' aria - label ', dark ? ' � � 揢瘛梯𠧧銝駁 � ' : ' � � 揢瘚 � 𠧧銝駁 � '); };
2026-07-27 10:01:07 +08:00
$(' editCancel ').onclick = requestEditClose; $(' editTabs ').onclick = openTabs; $(' tabsClose ').onclick = closeTabs; $(' tabsDialog ').addEventListener(' cancel ', e => { e.preventDefault(); closeTabs(); }); $(' tabsDialog ').addEventListener(' close ', () => { if ($(' editDialog ').open) setTimeout(() => $(' editTabs ').focus(), 0); }); $(' chooseEditDir ').onclick = () => showToast(' � 笔 � 瞍𠉛內嚗𡁜 � � 枏 � � 桀 � � 㗇𥋘 � 典僎 � 𧼮 ‵ User Data Dir � � '); $(' editForm ').addEventListener(' submit ', e => { e.preventDefault(); saveEdit(); }); $(' editDialog ').addEventListener(' cancel ', e => { e.preventDefault(); requestEditClose(); }); $(' keepEditing ').onclick = () => { $(' discardDialog ').close(); $(' editDialog ').showModal(); setTimeout(() => $(' editName ').focus(), 0); }; $(' discardChanges ').onclick = () => { $(' discardDialog ').close(); finishEdit(); }; $(' saveChanges ').onclick = saveEdit;
document.addEventListener(' keydown ', e => { if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === ' e ') { e.preventDefault(); queueUnexpectedExit(' inst - chrome - shop '); return; } if (e.key === ' F5 ' && !$(' editDialog ').open && !$(' tabsDialog ').open && !$(' discardDialog ').open) { e.preventDefault(); refreshInstances(); } });
2026-07-22 14:51:11 +08:00
document.querySelectorAll(' [ data - nav ] ').forEach(b => b.onclick = () => { document.querySelectorAll(' [ data - nav ] ').forEach(n => n.removeAttribute(' aria - current ')); b.setAttribute(' aria - current ', ' page '); setPage(b.dataset.nav); });
document.querySelectorAll(' . browse - setting ').forEach(b => b.onclick = () => showToast(`� 笔� 瞍𠉛內嚗𡁜� � 枏� ${b.dataset.target.includes(' Path ') ? ' � � 辣 ' : ' � 桀 � '}� 㗇𥋘� 具� � ));
2026-07-22 15:57:02 +08:00
document.querySelectorAll(' . search - setting ').forEach(b => b.onclick = () => { if (b.dataset.timer) { clearTimeout(Number(b.dataset.timer)); delete b.dataset.timer; b.textContent = ' � 𦦵揣 '; showToast(' 撌脣 � 瘨 � � � 齿 � 蝝U � � '); return; } b.textContent = ' � 𡝗 � '; showToast(' 甇 � 銁 � 𦦵揣 � 舐鍂頝臬 � � � '); b.dataset.timer = String(setTimeout(() => { delete b.dataset.timer; b.textContent = ' � 𦦵揣 '; showToast(' � 𦦵揣摰峕 � 嚗𡁜歇 � 湔鰵敶枏 � 摮埈挾 � � '); }, 1600)); });
2026-07-25 17:47:02 +08:00
$(' proxySelect ').onchange = () => { selectedProxyID = $(' proxySelect ').value; renderProxies(); $(' proxyServer ').focus(); };
$(' proxySave ').onclick = () => { const server = $(' proxyServer ').value.trim().toLowerCase(); if (!/^(https?|socks[45]):\/\/[^\/@?#:]+(?::\d+)$/.test(server)) { $(' proxyStatus ').textContent = ' 霂瑕 ‵ � 蹱 � 霈方 � � � scheme : //host:port 隞� � � 啣� � � '; return; } if (proxies.some(p => p.server === server && p.id !== selectedProxyID)) { $('proxyStatus').textContent = '霂乩誨� � 𧑐� � 撌脣� � 剁� 霂瑚� 銝𧢲� 獢� � 㗇𥋘摰� � � '; return; } if (selectedProxyID) { const p = proxies.find(item => item.id === selectedProxyID); p.server = server; p.name = server; } else { selectedProxyID = `proxy-${Date.now()}`; proxies.push({ id: selectedProxyID, name: server, server }); } renderProxies(); showToast('隞� � 撌脖� 摮矋� � 𡒊賒� 臬𢆡撠� 蝙� 冽� � 啁垢� 嫘� � '); };
2026-07-27 23:50:24 +08:00
document . querySelectorAll ( '.restore-portable-setting' ). forEach ( button => button . onclick = () => { const target = $ ( button . dataset . target ); target . value = button . dataset . target === 'logDir' ? 'D:\\Tools\\chub\\logs' : 'D:\\Tools\\chub\\user_data_dirs' ; showToast ( '撌脫� 憭滢蛹蝔见� � 桀� 暺䁅恕� 潦� � ' ); });
2026-07-25 17:47:02 +08:00
$ ( 'proxyDelete' ). onclick = () => { const proxy = proxies . find ( p => p . id === selectedProxyID ), uses = instances . filter ( i => i . proxyId === selectedProxyID ). length ; if ( ! proxy ) return ; if ( uses ) { $ ( 'proxyStatus' ). textContent = `霂乩誨� � � 鋡� ${ uses } 銝芸� 靘衤蝙� 剁� 銝滩� � 𣳇膄� � ; return; } $('proxyDeleteText').textContent = ` 蝖桀 � � 𣳇膄隞 � � � � $ { proxy . server } � 嘅 � 甇斗 � 雿靝 � 隡帋耨 � 嫣遙雿訫 � 靘卝 � � ; $ ( 'proxyDeleteDialog' ). showModal (); setTimeout (() => $ ( 'proxyDeleteCancel' ). focus (), 0 ); };
$ ( 'proxyDeleteCancel' ). onclick = () => $ ( 'proxyDeleteDialog' ). close (); $ ( 'proxyDeleteDialog' ). addEventListener ( 'cancel' , e => { e . preventDefault (); $ ( 'proxyDeleteDialog' ). close (); }); $ ( 'proxyDeleteConfirm' ). onclick = () => { const proxy = proxies . find ( p => p . id === selectedProxyID ); if ( proxy ) proxies . splice ( proxies . indexOf ( proxy ), 1 ); selectedProxyID = '' ; $ ( 'proxyDeleteDialog' ). close (); renderProxies (); $ ( 'proxySelect' ). focus (); showToast ( '隞� � 撌脣� � 扎� � ' ); };
2026-07-22 14:51:11 +08:00
$ ( 'settingsForm' ). addEventListener ( 'submit' , e => { e . preventDefault (); $ ( 'settingsStatus' ). textContent = '霈曄蔭撌脖� 摮� 繚 � � � 擧凒� 堆� � 𡁜� ' ; $ ( 'settingsStatus' ). classList . remove ( 'warning' ); showToast ( '霈曄蔭撌脖� 摮矋� � 𡒊賒� 啣遣摰硺� 撠� 蝙� 冽鰵� � � 霈文� 潦� � ' ); });
$ ( 'discardSettings' ). onclick = () => { $ ( 'settingsForm' ). reset (); $ ( 'settingsStatus' ). textContent = '撌脣� 瘨� 𧋦甈∩耨� 對� � W� � 唬� 甈∩� 摮睃� 潦� � ' ; $ ( 'settingsStatus' ). classList . add ( 'warning' ); showToast ( '撌脣� 瘨� 挽蝵桐耨� 嫘� � ' ); };
2026-07-25 16:53:44 +08:00
renderRows (); renderProxies ();
2026-07-22 14:51:11 +08:00
</ script >
</ body >
</ html >