Skip to content

Commit d719597

Browse files
committed
v1.3.0 — live DiffPanel, pinned-group preservation, dynamic-group restoration, group restore bug fixes
Major: - Live side-by-side DiffPanel on Restore page (source vs destination, before/after snapshots per node) - Pinned-group preservation with multi-endpoint convert chain - Dynamic-group restoration by filter NAME (backup carries filterName; restore remaps to dest filterId) - Resizable PanedWindow split, numbered rows, tooltips, auto-scroll, mouse-wheel forwarding Bug fixes: - Dynamic groups no longer silently restored as static - PUT /groups/{id} no longer sends 'type' field (S1 rejects it) - Group create with inherits=False fixed (always create with inherits=True; policy step decouples) - Config overrides re-inject data.scope after _clean_for_restore strips it - S1 API error extractor reads title+detail+code (no more 'Unrecognised error') - Per-item failure records keep full error message (was 80-char truncated) Also includes uncommitted v1.2.0 features: Purple AI page, Unified Alerts page, connection pooling, 429/5xx retry, parallel fan-out, GraphQL transport.
1 parent c4f6585 commit d719597

26 files changed

Lines changed: 4455 additions & 217 deletions

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,8 @@ s1-restore-report-*.html
1313
build/
1414
dist/
1515
*.spec
16+
17+
# Internal-only modules (never publish)
18+
jira_page.py
19+
atlas_token.txt
20+
pso_tickets_cache.json

.idea/.gitignore

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app.py

Lines changed: 178 additions & 25 deletions
Large diffs are not rendered by default.

build_macos.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pyinstaller \
4141
--hidden-import openpyxl \
4242
--hidden-import PIL \
4343
--collect-all customtkinter \
44+
--exclude-module jira_page \
4445
--noconfirm \
4546
main.py
4647

build_windows.bat

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pyinstaller ^
3838
--hidden-import openpyxl ^
3939
--hidden-import PIL ^
4040
--collect-all customtkinter ^
41+
--exclude-module jira_page ^
4142
--noconfirm ^
4243
main.py
4344

config.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
from dataclasses import dataclass, asdict
77
from typing import Optional
88

9-
CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".s1-command-center")
10-
CONFIG_FILE = os.path.join(CONFIG_DIR, "contexts.json")
9+
CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".s1-command-center")
10+
CONFIG_FILE = os.path.join(CONFIG_DIR, "contexts.json")
1111

1212

1313
@dataclass
@@ -16,6 +16,7 @@ class Context:
1616
url: str
1717
api_token: str
1818
role: str = "" # "source" or "destination"
19+
ignore_ssl_errors: bool = False
1920

2021
@property
2122
def display_url(self) -> str:
@@ -43,31 +44,48 @@ def save(self):
4344
with open(CONFIG_FILE, "w") as f:
4445
json.dump([asdict(c) for c in self.contexts], f, indent=2)
4546

46-
def upsert(self, name: str, url: str, api_token: str, role: str = "") -> Context:
47+
def upsert(self, name: str, url: str, api_token: str, role: str = "",
48+
ignore_ssl_errors: bool = False) -> Context:
4749
if not url.startswith("http"):
4850
url = f"https://{url}.sentinelone.net"
49-
self.contexts = [c for c in self.contexts if c.name != name]
50-
ctx = Context(name=name, url=url, api_token=api_token, role=role)
51+
# A console's identity is its URL, not its friendly name — ticket
52+
# data often gives the same account name to both source and
53+
# destination consoles, so deduping by name would wipe one of them.
54+
self.contexts = [c for c in self.contexts if c.url != url]
55+
ctx = Context(name=name, url=url, api_token=api_token, role=role,
56+
ignore_ssl_errors=bool(ignore_ssl_errors))
5157
self.contexts.append(ctx)
5258
self.save()
5359
return ctx
5460

55-
def remove(self, name: str):
56-
self.contexts = [c for c in self.contexts if c.name != name]
61+
def remove(self, url_or_name: str):
62+
"""Remove by URL first; fall back to name for legacy callers."""
63+
before = len(self.contexts)
64+
self.contexts = [c for c in self.contexts if c.url != url_or_name]
65+
if len(self.contexts) == before:
66+
self.contexts = [c for c in self.contexts if c.name != url_or_name]
5767
self.save()
5868

5969
def get(self, name: str) -> Optional[Context]:
6070
return next((c for c in self.contexts if c.name == name), None)
6171

72+
def get_by_url(self, url: str) -> Optional[Context]:
73+
return next((c for c in self.contexts if c.url == url), None)
74+
6275
def get_by_role(self, role: str) -> Optional[Context]:
6376
return next((c for c in self.contexts if c.role == role), None)
6477

65-
def set_role(self, name: str, role: str):
78+
def set_role(self, identifier: str, role: str):
79+
"""Promote the context with this URL (or name, for legacy) to the
80+
given role, demoting any other context that currently holds it."""
81+
target = self.get_by_url(identifier) or self.get(identifier)
82+
if not target:
83+
return
6684
for c in self.contexts:
67-
if c.role == role:
68-
c.role = ""
69-
if c.name == name:
85+
if c is target:
7086
c.role = role
87+
elif c.role == role:
88+
c.role = ""
7189
self.save()
7290

7391
def names(self) -> list[str]:

0 commit comments

Comments
 (0)