from __future__ import annotations from PySide6.QtCore import QSize, Qt from PySide6.QtWidgets import ( QAbstractItemView, QCheckBox, QComboBox, QFormLayout, QGroupBox, QHeaderView, QHBoxLayout, QLabel, QLineEdit, QListView, QListWidget, QPlainTextEdit, QProgressBar, QPushButton, QScrollArea, QSpinBox, QTableWidget, QStyle, QToolButton, QVBoxLayout, QWidget, QFrame, ) class UITabsSingBoxLayoutMixin: def _build_tab_singbox(self) -> None: tab = QWidget() layout = QVBoxLayout(tab) metrics_row = QHBoxLayout() layout.addLayout(metrics_row) ( _card_conn, self.lbl_singbox_metric_conn_value, self.lbl_singbox_metric_conn_sub, ) = self._create_singbox_metric_card("Connection") metrics_row.addWidget(_card_conn, stretch=1) ( _card_profile, self.lbl_singbox_metric_profile_value, self.lbl_singbox_metric_profile_sub, ) = self._create_singbox_metric_card("Profile") metrics_row.addWidget(_card_profile, stretch=1) ( _card_proto, self.lbl_singbox_metric_proto_value, self.lbl_singbox_metric_proto_sub, ) = self._create_singbox_metric_card("Protocol / Transport / Security") metrics_row.addWidget(_card_proto, stretch=1) ( _card_policy, self.lbl_singbox_metric_policy_value, self.lbl_singbox_metric_policy_sub, ) = self._create_singbox_metric_card("Routing / DNS / Killswitch") metrics_row.addWidget(_card_policy, stretch=1) profiles_group = QGroupBox("Connection profiles") profiles_layout = QVBoxLayout(profiles_group) profiles_actions = QHBoxLayout() self.btn_singbox_profile_create = QPushButton("Create connection") self.btn_singbox_profile_create.clicked.connect(self.on_singbox_create_connection_click) profiles_actions.addWidget(self.btn_singbox_profile_create) profiles_actions.addStretch(1) profiles_layout.addLayout(profiles_actions) self.lst_singbox_profile_cards = QListWidget() self.lst_singbox_profile_cards.setViewMode(QListView.IconMode) self.lst_singbox_profile_cards.setResizeMode(QListView.Adjust) self.lst_singbox_profile_cards.setMovement(QListView.Static) self.lst_singbox_profile_cards.setWrapping(True) self.lst_singbox_profile_cards.setSpacing(8) self.lst_singbox_profile_cards.setGridSize(QSize(240, 88)) self.lst_singbox_profile_cards.setMinimumHeight(110) self.lst_singbox_profile_cards.setContextMenuPolicy(Qt.CustomContextMenu) self.lst_singbox_profile_cards.customContextMenuRequested.connect( self.on_singbox_profile_card_context_menu ) self.lst_singbox_profile_cards.itemSelectionChanged.connect( self.on_singbox_profile_card_selected ) profiles_layout.addWidget(self.lst_singbox_profile_cards) layout.addWidget(profiles_group) card_group = QGroupBox("Connection card (runtime)") card_layout = QVBoxLayout(card_group) card_row = QHBoxLayout() card_layout.addLayout(card_row) self.lbl_transport_selected_engine = QLabel("Selected profile: —") self.lbl_transport_selected_engine.setStyleSheet("color: gray;") card_row.addWidget(self.lbl_transport_selected_engine, stretch=1) self.cmb_transport_engine = QComboBox() self.cmb_transport_engine.setMaxVisibleItems(10) self.cmb_transport_engine.currentIndexChanged.connect( self.on_transport_engine_selected ) # Hidden selector: internal state source (tiles are the visible selection control). self.cmb_transport_engine.setVisible(False) self.btn_transport_engine_refresh = QToolButton() self.btn_transport_engine_refresh.setAutoRaise(True) self.btn_transport_engine_refresh.setIcon( self.style().standardIcon(QStyle.SP_BrowserReload) ) self.btn_transport_engine_refresh.setToolTip("Refresh engines") self.btn_transport_engine_refresh.clicked.connect( self.on_transport_engine_refresh ) card_row.addWidget(self.btn_transport_engine_refresh) self.btn_transport_engine_provision = QPushButton("Prepare") self.btn_transport_engine_provision.setToolTip( "Optional: pre-provision runtime/config artifacts for selected profile" ) self.btn_transport_engine_provision.clicked.connect( lambda: self.on_transport_engine_action("provision") ) card_row.addWidget(self.btn_transport_engine_provision) self.btn_transport_engine_toggle = QPushButton("Disconnected") self.btn_transport_engine_toggle.setCheckable(True) self.btn_transport_engine_toggle.setToolTip( "Toggle connection for selected profile" ) self.btn_transport_engine_toggle.clicked.connect( self.on_transport_engine_toggle ) card_row.addWidget(self.btn_transport_engine_toggle) self.btn_transport_engine_restart = QPushButton("Restart") self.btn_transport_engine_restart.clicked.connect( lambda: self.on_transport_engine_action("restart") ) card_row.addWidget(self.btn_transport_engine_restart) self.btn_transport_engine_rollback = QPushButton("Rollback policy") self.btn_transport_engine_rollback.clicked.connect( self.on_transport_policy_rollback ) card_row.addWidget(self.btn_transport_engine_rollback) self.btn_transport_netns_toggle = QPushButton("Debug netns: OFF") self.btn_transport_netns_toggle.setToolTip( "Toggle netns for all SingBox engines (debug/testing)" ) self.btn_transport_netns_toggle.clicked.connect( self.on_transport_netns_toggle ) card_row.addWidget(self.btn_transport_netns_toggle) self.lbl_transport_engine_meta = QLabel("Engine: loading...") self.lbl_transport_engine_meta.setStyleSheet("color: gray;") card_layout.addWidget(self.lbl_transport_engine_meta) layout.addWidget(card_group) settings_toggle_row = QHBoxLayout() self.btn_singbox_toggle_profile_settings = QPushButton("Profile settings") self.btn_singbox_toggle_profile_settings.setCheckable(True) self.btn_singbox_toggle_profile_settings.clicked.connect( self.on_toggle_singbox_profile_settings ) settings_toggle_row.addWidget(self.btn_singbox_toggle_profile_settings) self.btn_singbox_toggle_global_defaults = QPushButton("Global defaults") self.btn_singbox_toggle_global_defaults.setCheckable(True) self.btn_singbox_toggle_global_defaults.clicked.connect( self.on_toggle_singbox_global_defaults ) settings_toggle_row.addWidget(self.btn_singbox_toggle_global_defaults) self.btn_singbox_toggle_activity = QPushButton("Activity log") self.btn_singbox_toggle_activity.setCheckable(True) self.btn_singbox_toggle_activity.clicked.connect( self.on_toggle_singbox_activity ) settings_toggle_row.addWidget(self.btn_singbox_toggle_activity) settings_toggle_row.addStretch(1) layout.addLayout(settings_toggle_row) profile_group = QGroupBox("Profile settings (SingBox)") self.grp_singbox_profile_settings = profile_group profile_layout = QVBoxLayout(profile_group) self.lbl_singbox_profile_name = QLabel("Profile: —") self.lbl_singbox_profile_name.setStyleSheet("color: gray;") profile_layout.addWidget(self.lbl_singbox_profile_name) profile_scope_row = QHBoxLayout() self.chk_singbox_profile_use_global_routing = QCheckBox("Use global routing defaults") self.chk_singbox_profile_use_global_routing.stateChanged.connect( self.on_singbox_profile_scope_changed ) profile_scope_row.addWidget(self.chk_singbox_profile_use_global_routing) self.chk_singbox_profile_use_global_dns = QCheckBox("Use global DNS defaults") self.chk_singbox_profile_use_global_dns.stateChanged.connect( self.on_singbox_profile_scope_changed ) profile_scope_row.addWidget(self.chk_singbox_profile_use_global_dns) self.chk_singbox_profile_use_global_killswitch = QCheckBox("Use global kill-switch defaults") self.chk_singbox_profile_use_global_killswitch.stateChanged.connect( self.on_singbox_profile_scope_changed ) profile_scope_row.addWidget(self.chk_singbox_profile_use_global_killswitch) profile_scope_row.addStretch(1) profile_layout.addLayout(profile_scope_row) profile_form = QFormLayout() self.cmb_singbox_profile_routing = QComboBox() self.cmb_singbox_profile_routing.addItem("Global default", "global") self.cmb_singbox_profile_routing.addItem("Selective", "selective") self.cmb_singbox_profile_routing.addItem("Full tunnel", "full") self.cmb_singbox_profile_routing.currentIndexChanged.connect( self.on_singbox_profile_scope_changed ) profile_form.addRow("Routing mode:", self.cmb_singbox_profile_routing) self.cmb_singbox_profile_dns = QComboBox() self.cmb_singbox_profile_dns.addItem("Global default", "global") self.cmb_singbox_profile_dns.addItem("System resolver", "system_resolver") self.cmb_singbox_profile_dns.addItem("SingBox DNS", "singbox_dns") self.cmb_singbox_profile_dns.currentIndexChanged.connect( self.on_singbox_profile_scope_changed ) profile_form.addRow("DNS mode:", self.cmb_singbox_profile_dns) self.cmb_singbox_profile_killswitch = QComboBox() self.cmb_singbox_profile_killswitch.addItem("Global default", "global") self.cmb_singbox_profile_killswitch.addItem("Enabled", "on") self.cmb_singbox_profile_killswitch.addItem("Disabled", "off") self.cmb_singbox_profile_killswitch.currentIndexChanged.connect( self.on_singbox_profile_scope_changed ) profile_form.addRow("Kill-switch:", self.cmb_singbox_profile_killswitch) profile_layout.addLayout(profile_form) profile_actions = QHBoxLayout() self.btn_singbox_profile_preview = QPushButton("Preview render") self.btn_singbox_profile_preview.clicked.connect(self.on_singbox_profile_preview) profile_actions.addWidget(self.btn_singbox_profile_preview) self.btn_singbox_profile_validate = QPushButton("Validate profile") self.btn_singbox_profile_validate.clicked.connect(self.on_singbox_profile_validate) profile_actions.addWidget(self.btn_singbox_profile_validate) self.btn_singbox_profile_apply = QPushButton("Apply profile") self.btn_singbox_profile_apply.clicked.connect(self.on_singbox_profile_apply) profile_actions.addWidget(self.btn_singbox_profile_apply) self.btn_singbox_profile_rollback = QPushButton("Rollback profile") self.btn_singbox_profile_rollback.clicked.connect(self.on_singbox_profile_rollback) profile_actions.addWidget(self.btn_singbox_profile_rollback) self.btn_singbox_profile_history = QPushButton("History") self.btn_singbox_profile_history.clicked.connect(self.on_singbox_profile_history) profile_actions.addWidget(self.btn_singbox_profile_history) self.btn_singbox_profile_save = QPushButton("Save draft") self.btn_singbox_profile_save.clicked.connect(self.on_singbox_profile_save) profile_actions.addWidget(self.btn_singbox_profile_save) profile_actions.addStretch(1) profile_layout.addLayout(profile_actions) self.lbl_singbox_profile_effective = QLabel("Effective: routing=— | dns=— | kill-switch=—") self.lbl_singbox_profile_effective.setStyleSheet("color: gray;") profile_layout.addWidget(self.lbl_singbox_profile_effective) self._build_singbox_vless_editor(profile_layout) self._singbox_editor_default_title = self.grp_singbox_proto_editor.title() self.grp_singbox_proto_editor.setVisible(False) self.lbl_singbox_editor_hint = QLabel("Right-click a profile card and select Edit to open protocol settings.") self.lbl_singbox_editor_hint.setStyleSheet("color: gray;") profile_layout.addWidget(self.lbl_singbox_editor_hint) layout.addWidget(profile_group) profile_group.setVisible(False) global_group = QGroupBox("Global defaults") self.grp_singbox_global_defaults = global_group global_layout = QVBoxLayout(global_group) global_form = QFormLayout() self.cmb_singbox_global_routing = QComboBox() self.cmb_singbox_global_routing.addItem("Selective", "selective") self.cmb_singbox_global_routing.addItem("Full tunnel", "full") self.cmb_singbox_global_routing.currentIndexChanged.connect( self.on_singbox_global_defaults_changed ) global_form.addRow("Default routing mode:", self.cmb_singbox_global_routing) self.cmb_singbox_global_dns = QComboBox() self.cmb_singbox_global_dns.addItem("System resolver", "system_resolver") self.cmb_singbox_global_dns.addItem("SingBox DNS", "singbox_dns") self.cmb_singbox_global_dns.currentIndexChanged.connect( self.on_singbox_global_defaults_changed ) global_form.addRow("Default DNS mode:", self.cmb_singbox_global_dns) self.cmb_singbox_global_killswitch = QComboBox() self.cmb_singbox_global_killswitch.addItem("Enabled", "on") self.cmb_singbox_global_killswitch.addItem("Disabled", "off") self.cmb_singbox_global_killswitch.currentIndexChanged.connect( self.on_singbox_global_defaults_changed ) global_form.addRow("Default kill-switch:", self.cmb_singbox_global_killswitch) global_layout.addLayout(global_form) global_actions = QHBoxLayout() self.btn_singbox_global_save = QPushButton("Save global defaults") self.btn_singbox_global_save.clicked.connect(self.on_singbox_global_save) global_actions.addWidget(self.btn_singbox_global_save) global_actions.addStretch(1) global_layout.addLayout(global_actions) self.lbl_singbox_global_hint = QLabel( "Global defaults are used by profiles with 'Use global ...' enabled." ) self.lbl_singbox_global_hint.setStyleSheet("color: gray;") global_layout.addWidget(self.lbl_singbox_global_hint) layout.addWidget(global_group) global_group.setVisible(False) # During UI construction routes/dns widgets are not fully created yet, # so apply local SingBox control state without touching global save path. self._apply_singbox_profile_controls() # Multi-interface routing tools are placed on a dedicated tab. self.grp_singbox_activity = QGroupBox("Activity log") activity_layout = QVBoxLayout(self.grp_singbox_activity) self.txt_transport = QPlainTextEdit() self.txt_transport.setReadOnly(True) activity_layout.addWidget(self.txt_transport) layout.addWidget(self.grp_singbox_activity, stretch=1) self.grp_singbox_activity.setVisible(False) self._apply_singbox_compact_visibility() self.tabs.addTab(tab, "SingBox") def _build_tab_multiif(self) -> None: tab = QWidget() layout = QVBoxLayout(tab) scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setFrameShape(QFrame.NoFrame) scroll_content = QWidget() scroll_layout = QVBoxLayout(scroll_content) scroll_layout.setContentsMargins(0, 0, 0, 0) scroll_layout.setSpacing(8) self.grp_singbox_owner_locks = self._build_singbox_owner_locks_group() scroll_layout.addWidget(self.grp_singbox_owner_locks) scroll_layout.addStretch(1) scroll.setWidget(scroll_content) layout.addWidget(scroll, stretch=1) self.tabs.addTab(tab, "MultiIF") def _build_singbox_owner_locks_group(self) -> QGroupBox: group = QGroupBox("Routing policy & ownership locks") owner_locks_layout = QVBoxLayout(group) owner_actions = QHBoxLayout() self.btn_singbox_owner_locks_refresh = QPushButton("Refresh locks") self.btn_singbox_owner_locks_refresh.clicked.connect( self.on_singbox_owner_locks_refresh ) owner_actions.addWidget(self.btn_singbox_owner_locks_refresh) self.btn_singbox_owner_locks_clear = QPushButton("Clear locks...") self.btn_singbox_owner_locks_clear.clicked.connect( self.on_singbox_owner_locks_clear ) owner_actions.addWidget(self.btn_singbox_owner_locks_clear) owner_actions.addWidget(QLabel("Engine:")) self.cmb_singbox_owner_engine_scope = QComboBox() self.cmb_singbox_owner_engine_scope.addItem("All", "all") self.cmb_singbox_owner_engine_scope.addItem("Transport", "transport") self.cmb_singbox_owner_engine_scope.addItem("AdGuard VPN", "adguardvpn") self.cmb_singbox_owner_engine_scope.currentIndexChanged.connect( self.on_singbox_owner_engine_scope_changed ) owner_actions.addWidget(self.cmb_singbox_owner_engine_scope) owner_actions.addStretch(1) owner_locks_layout.addLayout(owner_actions) self.lbl_singbox_owner_locks_summary = QLabel("Ownership: — | Locks: —") self.lbl_singbox_owner_locks_summary.setStyleSheet("color: gray;") owner_locks_layout.addWidget(self.lbl_singbox_owner_locks_summary) self.lbl_singbox_interfaces_hint = QLabel("Interfaces (read-only)") self.lbl_singbox_interfaces_hint.setStyleSheet("color: #666;") owner_locks_layout.addWidget(self.lbl_singbox_interfaces_hint) self.tbl_singbox_interfaces = QTableWidget(0, 7) self.tbl_singbox_interfaces.setHorizontalHeaderLabels( ["Iface ID", "Mode", "Runtime iface", "NetNS", "Routing table", "Clients UP/Total", "Updated"] ) self.tbl_singbox_interfaces.setSelectionBehavior(QAbstractItemView.SelectRows) self.tbl_singbox_interfaces.setSelectionMode(QAbstractItemView.SingleSelection) self.tbl_singbox_interfaces.setEditTriggers(QAbstractItemView.NoEditTriggers) self.tbl_singbox_interfaces.verticalHeader().setVisible(False) self.tbl_singbox_interfaces.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents) self.tbl_singbox_interfaces.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.tbl_singbox_interfaces.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents) self.tbl_singbox_interfaces.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents) self.tbl_singbox_interfaces.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeToContents) self.tbl_singbox_interfaces.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeToContents) self.tbl_singbox_interfaces.horizontalHeader().setSectionResizeMode(6, QHeaderView.Stretch) self.tbl_singbox_interfaces.setMinimumHeight(120) owner_locks_layout.addWidget(self.tbl_singbox_interfaces) self.lbl_singbox_policy_quick_help = QLabel( "Policy flow: 1) Add demo/fill intent -> 2) Validate policy -> 3) Validate & apply." ) self.lbl_singbox_policy_quick_help.setStyleSheet("color: #1f6b2f;") owner_locks_layout.addWidget(self.lbl_singbox_policy_quick_help) policy_group = QGroupBox("Policy intents") policy_layout = QVBoxLayout(policy_group) self.lbl_singbox_policy_input_help = QLabel( "Intent fields: selector type | selector value | client | mode | priority" ) self.lbl_singbox_policy_input_help.setStyleSheet("color: #666;") policy_layout.addWidget(self.lbl_singbox_policy_input_help) policy_template_row = QHBoxLayout() self.cmb_singbox_policy_template = QComboBox() self.cmb_singbox_policy_template.addItem("Quick template...", "") self.cmb_singbox_policy_template.addItem( "Domain -> active client (strict)", { "selector_type": "domain", "selector_value": "example.com", "mode": "strict", "priority": 100, }, ) self.cmb_singbox_policy_template.addItem( "Wildcard domain (fallback)", { "selector_type": "domain", "selector_value": "*.example.com", "mode": "fallback", "priority": 200, }, ) self.cmb_singbox_policy_template.addItem( "CIDR subnet (strict)", { "selector_type": "cidr", "selector_value": "1.2.3.0/24", "mode": "strict", "priority": 100, }, ) self.cmb_singbox_policy_template.addItem( "IP host (strict)", { "selector_type": "cidr", "selector_value": "1.2.3.4", "mode": "strict", "priority": 100, }, ) self.cmb_singbox_policy_template.addItem( "App key (strict)", { "selector_type": "app_key", "selector_value": "steam", "mode": "strict", "priority": 100, }, ) self.cmb_singbox_policy_template.addItem( "UID (strict)", { "selector_type": "uid", "selector_value": "1000", "mode": "strict", "priority": 100, }, ) self.cmb_singbox_policy_template.setToolTip( "Prefill intent fields from a template. It does not add to draft automatically." ) policy_template_row.addWidget(self.cmb_singbox_policy_template, stretch=2) self.btn_singbox_policy_use_template = QPushButton("Use template") self.btn_singbox_policy_use_template.clicked.connect(self.on_singbox_policy_use_template) policy_template_row.addWidget(self.btn_singbox_policy_use_template) self.btn_singbox_policy_add_demo = QPushButton("Add demo intent") self.btn_singbox_policy_add_demo.setToolTip( "Create one test intent (domain -> selected client) and add it to draft." ) self.btn_singbox_policy_add_demo.clicked.connect(self.on_singbox_policy_add_demo_intent) policy_template_row.addWidget(self.btn_singbox_policy_add_demo) policy_template_row.addStretch(1) policy_layout.addLayout(policy_template_row) policy_input_row = QHBoxLayout() self.cmb_singbox_policy_selector_type = QComboBox() self.cmb_singbox_policy_selector_type.addItem("domain", "domain") self.cmb_singbox_policy_selector_type.addItem("cidr", "cidr") self.cmb_singbox_policy_selector_type.addItem("app_key", "app_key") self.cmb_singbox_policy_selector_type.addItem("cgroup", "cgroup") self.cmb_singbox_policy_selector_type.addItem("uid", "uid") self.cmb_singbox_policy_selector_type.currentIndexChanged.connect( self.on_singbox_policy_selector_type_changed ) policy_input_row.addWidget(self.cmb_singbox_policy_selector_type) self.ent_singbox_policy_selector_value = QLineEdit() self.ent_singbox_policy_selector_value.setPlaceholderText("example.com") self.ent_singbox_policy_selector_value.setToolTip( "Examples: domain=example.com, cidr=1.2.3.0/24, app_key=steam, cgroup=user.slice/..., uid=1000. Press Enter to add intent." ) self.ent_singbox_policy_selector_value.returnPressed.connect(self.on_singbox_policy_add_intent) policy_input_row.addWidget(self.ent_singbox_policy_selector_value, stretch=2) self.cmb_singbox_policy_client_id = QComboBox() self.cmb_singbox_policy_client_id.setMinimumWidth(180) policy_input_row.addWidget(self.cmb_singbox_policy_client_id, stretch=1) self.cmb_singbox_policy_mode = QComboBox() self.cmb_singbox_policy_mode.addItem("strict", "strict") self.cmb_singbox_policy_mode.addItem("fallback", "fallback") policy_input_row.addWidget(self.cmb_singbox_policy_mode) self.spn_singbox_policy_priority = QSpinBox() self.spn_singbox_policy_priority.setRange(1, 10000) self.spn_singbox_policy_priority.setValue(100) self.spn_singbox_policy_priority.setToolTip("Intent priority") policy_input_row.addWidget(self.spn_singbox_policy_priority) self.btn_singbox_policy_add = QPushButton("Add intent") self.btn_singbox_policy_add.clicked.connect(self.on_singbox_policy_add_intent) policy_input_row.addWidget(self.btn_singbox_policy_add) self.btn_singbox_policy_load_selected = QPushButton("Load selected") self.btn_singbox_policy_load_selected.clicked.connect(self.on_singbox_policy_load_selected_intent) policy_input_row.addWidget(self.btn_singbox_policy_load_selected) self.btn_singbox_policy_update_selected = QPushButton("Update selected") self.btn_singbox_policy_update_selected.clicked.connect(self.on_singbox_policy_update_selected_intent) policy_input_row.addWidget(self.btn_singbox_policy_update_selected) self.btn_singbox_policy_remove = QPushButton("Remove selected") self.btn_singbox_policy_remove.clicked.connect(self.on_singbox_policy_remove_selected) policy_input_row.addWidget(self.btn_singbox_policy_remove) policy_layout.addLayout(policy_input_row) policy_actions_row = QHBoxLayout() self.btn_singbox_policy_reload = QPushButton("Reload policy") self.btn_singbox_policy_reload.clicked.connect(self.on_singbox_policy_reload) policy_actions_row.addWidget(self.btn_singbox_policy_reload) self.btn_singbox_policy_validate = QPushButton("Validate policy") self.btn_singbox_policy_validate.clicked.connect(self.on_singbox_policy_validate) policy_actions_row.addWidget(self.btn_singbox_policy_validate) self.btn_singbox_policy_apply = QPushButton("Validate & apply") self.btn_singbox_policy_apply.clicked.connect(self.on_singbox_policy_apply) policy_actions_row.addWidget(self.btn_singbox_policy_apply) self.btn_singbox_policy_rollback = QPushButton("Rollback policy") self.btn_singbox_policy_rollback.clicked.connect(self.on_singbox_policy_rollback_explicit) policy_actions_row.addWidget(self.btn_singbox_policy_rollback) policy_actions_row.addStretch(1) policy_layout.addLayout(policy_actions_row) self.lbl_singbox_policy_state = QLabel("Policy editor: loading...") self.lbl_singbox_policy_state.setStyleSheet("color: gray;") policy_layout.addWidget(self.lbl_singbox_policy_state) self.lbl_singbox_policy_conflicts_hint = QLabel( "Validation conflicts (last validate/apply, read-only)" ) self.lbl_singbox_policy_conflicts_hint.setStyleSheet("color: #666;") policy_layout.addWidget(self.lbl_singbox_policy_conflicts_hint) self.tbl_singbox_policy_conflicts = QTableWidget(0, 5) self.tbl_singbox_policy_conflicts.setHorizontalHeaderLabels( ["Type", "Severity", "Owners", "Reason", "Suggested resolution"] ) self.tbl_singbox_policy_conflicts.setSelectionBehavior(QAbstractItemView.SelectRows) self.tbl_singbox_policy_conflicts.setSelectionMode(QAbstractItemView.SingleSelection) self.tbl_singbox_policy_conflicts.setEditTriggers(QAbstractItemView.NoEditTriggers) self.tbl_singbox_policy_conflicts.verticalHeader().setVisible(False) self.tbl_singbox_policy_conflicts.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents) self.tbl_singbox_policy_conflicts.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.tbl_singbox_policy_conflicts.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents) self.tbl_singbox_policy_conflicts.horizontalHeader().setSectionResizeMode(3, QHeaderView.Stretch) self.tbl_singbox_policy_conflicts.horizontalHeader().setSectionResizeMode(4, QHeaderView.Stretch) self.tbl_singbox_policy_conflicts.setMinimumHeight(100) policy_layout.addWidget(self.tbl_singbox_policy_conflicts) self.tbl_singbox_policy_intents = QTableWidget(0, 5) self.tbl_singbox_policy_intents.setHorizontalHeaderLabels( ["Selector type", "Selector value", "Client ID", "Mode", "Priority"] ) self.tbl_singbox_policy_intents.setSelectionBehavior(QAbstractItemView.SelectRows) self.tbl_singbox_policy_intents.setSelectionMode(QAbstractItemView.ExtendedSelection) self.tbl_singbox_policy_intents.setEditTriggers(QAbstractItemView.NoEditTriggers) self.tbl_singbox_policy_intents.verticalHeader().setVisible(False) self.tbl_singbox_policy_intents.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents) self.tbl_singbox_policy_intents.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) self.tbl_singbox_policy_intents.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents) self.tbl_singbox_policy_intents.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents) self.tbl_singbox_policy_intents.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeToContents) self.tbl_singbox_policy_intents.setMinimumHeight(130) self.tbl_singbox_policy_intents.itemDoubleClicked.connect( self.on_singbox_policy_intent_double_clicked ) policy_layout.addWidget(self.tbl_singbox_policy_intents) self.lbl_singbox_policy_applied_hint = QLabel( "Applied intents (read-only, current backend policy)" ) self.lbl_singbox_policy_applied_hint.setStyleSheet("color: #666;") policy_layout.addWidget(self.lbl_singbox_policy_applied_hint) self.tbl_singbox_policy_applied = QTableWidget(0, 5) self.tbl_singbox_policy_applied.setHorizontalHeaderLabels( ["Selector type", "Selector value", "Client ID", "Mode", "Priority"] ) self.tbl_singbox_policy_applied.setSelectionBehavior(QAbstractItemView.SelectRows) self.tbl_singbox_policy_applied.setSelectionMode(QAbstractItemView.SingleSelection) self.tbl_singbox_policy_applied.setEditTriggers(QAbstractItemView.NoEditTriggers) self.tbl_singbox_policy_applied.verticalHeader().setVisible(False) self.tbl_singbox_policy_applied.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents) self.tbl_singbox_policy_applied.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) self.tbl_singbox_policy_applied.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents) self.tbl_singbox_policy_applied.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents) self.tbl_singbox_policy_applied.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeToContents) self.tbl_singbox_policy_applied.setMinimumHeight(110) policy_layout.addWidget(self.tbl_singbox_policy_applied) owner_locks_layout.addWidget(policy_group) self.lbl_singbox_ownership_hint = QLabel( "Ownership (read-only, populated after policy apply)" ) self.lbl_singbox_ownership_hint.setStyleSheet("color: #666;") owner_locks_layout.addWidget(self.lbl_singbox_ownership_hint) self.tbl_singbox_ownership = QTableWidget(0, 6) self.tbl_singbox_ownership.setHorizontalHeaderLabels( ["Selector", "Owner", "Owner scope", "Iface / table", "Status", "Lock"] ) self.tbl_singbox_ownership.setSelectionBehavior(QAbstractItemView.SelectRows) self.tbl_singbox_ownership.setSelectionMode(QAbstractItemView.SingleSelection) self.tbl_singbox_ownership.setEditTriggers(QAbstractItemView.NoEditTriggers) self.tbl_singbox_ownership.verticalHeader().setVisible(False) self.tbl_singbox_ownership.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) self.tbl_singbox_ownership.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.tbl_singbox_ownership.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents) self.tbl_singbox_ownership.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents) self.tbl_singbox_ownership.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeToContents) self.tbl_singbox_ownership.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeToContents) self.tbl_singbox_ownership.setMinimumHeight(130) owner_locks_layout.addWidget(self.tbl_singbox_ownership) filters_row = QHBoxLayout() self.ent_singbox_owner_lock_client = QLineEdit() self.ent_singbox_owner_lock_client.setPlaceholderText("Filter client_id (optional)") filters_row.addWidget(self.ent_singbox_owner_lock_client, stretch=1) self.ent_singbox_owner_lock_destination = QLineEdit() self.ent_singbox_owner_lock_destination.setPlaceholderText( "Destination IP or CSV list (optional)" ) filters_row.addWidget(self.ent_singbox_owner_lock_destination, stretch=2) owner_locks_layout.addLayout(filters_row) self.lbl_singbox_locks_hint = QLabel( "Destination locks (read-only, conntrack sticky state)" ) self.lbl_singbox_locks_hint.setStyleSheet("color: #666;") owner_locks_layout.addWidget(self.lbl_singbox_locks_hint) self.tbl_singbox_owner_locks = QTableWidget(0, 6) self.tbl_singbox_owner_locks.setHorizontalHeaderLabels( ["Destination", "Owner", "Kind", "Iface", "Mark/Proto", "Updated"] ) self.tbl_singbox_owner_locks.setSelectionBehavior(QAbstractItemView.SelectRows) self.tbl_singbox_owner_locks.setSelectionMode(QAbstractItemView.ExtendedSelection) self.tbl_singbox_owner_locks.setEditTriggers(QAbstractItemView.NoEditTriggers) self.tbl_singbox_owner_locks.verticalHeader().setVisible(False) self.tbl_singbox_owner_locks.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents) self.tbl_singbox_owner_locks.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.tbl_singbox_owner_locks.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents) self.tbl_singbox_owner_locks.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents) self.tbl_singbox_owner_locks.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeToContents) self.tbl_singbox_owner_locks.horizontalHeader().setSectionResizeMode(5, QHeaderView.Stretch) self.tbl_singbox_owner_locks.setMinimumHeight(170) owner_locks_layout.addWidget(self.tbl_singbox_owner_locks) self.lbl_singbox_owner_locks_hint = QLabel( "Clear flow is two-step confirm. Empty filter uses selected destination rows." ) self.lbl_singbox_owner_locks_hint.setStyleSheet("color: gray;") owner_locks_layout.addWidget(self.lbl_singbox_owner_locks_hint) return group def _create_singbox_metric_card(self, title: str) -> tuple[QFrame, QLabel, QLabel]: frame = QFrame() frame.setFrameShape(QFrame.StyledPanel) frame.setObjectName("singboxMetricCard") frame.setStyleSheet( """ QFrame#singboxMetricCard { border: 1px solid #c9c9c9; border-radius: 6px; background: #f7f7f7; } """ ) lay = QVBoxLayout(frame) lay.setContentsMargins(10, 8, 10, 8) lay.setSpacing(2) lbl_title = QLabel(title) lbl_title.setStyleSheet("color: #555; font-size: 11px;") lay.addWidget(lbl_title) lbl_value = QLabel("—") lbl_value.setStyleSheet("font-weight: 600;") lay.addWidget(lbl_value) lbl_sub = QLabel("—") lbl_sub.setStyleSheet("color: #666; font-size: 11px;") lay.addWidget(lbl_sub) return frame, lbl_value, lbl_sub