portacode 1.4.15.dev23__py3-none-any.whl → 1.4.16.dev5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- portacode/_version.py +2 -2
- portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +37 -16
- portacode/connection/handlers/project_state/manager.py +25 -3
- portacode/connection/handlers/proxmox_infra.py +289 -50
- portacode/connection/terminal.py +80 -0
- {portacode-1.4.15.dev23.dist-info → portacode-1.4.16.dev5.dist-info}/METADATA +1 -1
- {portacode-1.4.15.dev23.dist-info → portacode-1.4.16.dev5.dist-info}/RECORD +11 -11
- {portacode-1.4.15.dev23.dist-info → portacode-1.4.16.dev5.dist-info}/WHEEL +0 -0
- {portacode-1.4.15.dev23.dist-info → portacode-1.4.16.dev5.dist-info}/entry_points.txt +0 -0
- {portacode-1.4.15.dev23.dist-info → portacode-1.4.16.dev5.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.4.15.dev23.dist-info → portacode-1.4.16.dev5.dist-info}/top_level.txt +0 -0
portacode/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.4.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 4,
|
|
31
|
+
__version__ = version = '1.4.16.dev5'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 4, 16, 'dev5')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -381,6 +381,7 @@ Starts a previously provisioned, Portacode-managed LXC container. Handled by [`S
|
|
|
381
381
|
**Payload Fields:**
|
|
382
382
|
|
|
383
383
|
* `ctid` (string, required): Identifier of the container to start.
|
|
384
|
+
* `child_device_id` (string, required): Dashboard `Device.id` of the container that triggered the request; the handler validates the CT belongs to that device before issuing the start.
|
|
384
385
|
|
|
385
386
|
**Responses:**
|
|
386
387
|
|
|
@@ -394,6 +395,7 @@ Stops a running Portacode-managed container. Handled by [`StopProxmoxContainerHa
|
|
|
394
395
|
**Payload Fields:**
|
|
395
396
|
|
|
396
397
|
* `ctid` (string, required): Identifier of the container to stop.
|
|
398
|
+
* `child_device_id` (string, required): Dashboard `Device.id` that owns the container; the handler rejects the request if the CT is mapped to another device.
|
|
397
399
|
|
|
398
400
|
**Responses:**
|
|
399
401
|
|
|
@@ -407,6 +409,7 @@ Deletes a managed container from Proxmox (stopping it first if necessary) and re
|
|
|
407
409
|
**Payload Fields:**
|
|
408
410
|
|
|
409
411
|
* `ctid` (string, required): Identifier of the container to delete.
|
|
412
|
+
* `child_device_id` (string, required): Dashboard `Device.id` that should own the container metadata being purged.
|
|
410
413
|
|
|
411
414
|
**Responses:**
|
|
412
415
|
|
|
@@ -1133,22 +1136,40 @@ Provides system information in response to a `system_info` action. Handled by [`
|
|
|
1133
1136
|
* `bridge` (string): The bridge interface configured (typically `vmbr1`).
|
|
1134
1137
|
* `health` (string|null): `"healthy"` when the connectivity verification succeeded.
|
|
1135
1138
|
* `node_status` (object|null): Status response returned by the Proxmox API when validating the token.
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1139
|
+
* `managed_containers` (object): Cached summary of the Portacode-managed containers:
|
|
1140
|
+
* `updated_at` (string): ISO timestamp when this snapshot was last refreshed.
|
|
1141
|
+
* `count` (integer): Number of managed containers.
|
|
1142
|
+
* `total_ram_mib` (integer): RAM footprint summed across all containers.
|
|
1143
|
+
* `total_disk_gib` (integer): Disk footprint summed across all containers.
|
|
1144
|
+
* `total_cpu_share` (number): CPU shares requested across all containers.
|
|
1145
|
+
* `containers` (array[object]): Container summaries with the following fields:
|
|
1146
|
+
* `vmid` (string|null): Numeric CT ID.
|
|
1147
|
+
* `hostname` (string|null): Hostname configured in the CT.
|
|
1148
|
+
* `template` (string|null): Template identifier used.
|
|
1149
|
+
* `storage` (string|null): Storage pool backing the rootfs.
|
|
1150
|
+
* `disk_gib` (integer): Rootfs size in GiB.
|
|
1151
|
+
* `ram_mib` (integer): Memory size in MiB.
|
|
1152
|
+
* `cpu_share` (number): vCPU-equivalent share requested at creation.
|
|
1153
|
+
* `status` (string): Lowercase lifecycle status (e.g., `running`, `stopped`, `deleted`).
|
|
1154
|
+
* `created_at` (string|null): ISO timestamp recorded when the CT was provisioned.
|
|
1155
|
+
* `managed` (boolean): `true` for Portacode-managed entries.
|
|
1156
|
+
* `unmanaged_containers` (array[object]): Facts about containers Portacode did not provision; fields mirror the managed list but are marked `managed=false`.
|
|
1157
|
+
* `unmanaged_count` (integer): Number of unmanaged containers detected on the node.
|
|
1158
|
+
* `allocated_ram_mib` (integer): Total RAM reserved by both managed and unmanaged containers.
|
|
1159
|
+
* `allocated_disk_gib` (integer): Total disk reserved by both managed and unmanaged containers.
|
|
1160
|
+
* `allocated_cpu_share` (number): Total CPU shares requested by both managed and unmanaged containers.
|
|
1161
|
+
* `available_ram_mib` (integer|null): Remaining RAM after subtracting all reservations from the host total (null when unavailable).
|
|
1162
|
+
* `available_disk_gib` (integer|null): Remaining disk GB after subtracting allocations from the host total.
|
|
1163
|
+
* `available_cpu_share` (number|null): Remaining CPU shares after allocations.
|
|
1164
|
+
* `host_total_ram_mib` (integer|null): Host memory capacity observed via Proxmox.
|
|
1165
|
+
* `host_total_disk_gib` (integer|null): Host disk capacity observed via Proxmox.
|
|
1166
|
+
* `host_total_cpu_cores` (integer|null): Number of CPU cores reported by Proxmox.
|
|
1167
|
+
* `default_storage` (string|null): Storage pool name selected during infrastructure configuration.
|
|
1168
|
+
* `default_storage_snapshot` (object|null): Fresh stats for the default storage pool:
|
|
1169
|
+
* `storage` (string): Storage pool identifier.
|
|
1170
|
+
* `total_gib` (integer|null): Capacity of the storage pool.
|
|
1171
|
+
* `avail_gib` (integer|null): Available space remaining.
|
|
1172
|
+
* `used_gib` (integer|null): Space already consumed.
|
|
1152
1173
|
* `portacode_version` (string): Installed CLI version returned by `portacode.__version__`.
|
|
1153
1174
|
|
|
1154
1175
|
### `proxmox_infra_configured`
|
|
@@ -21,6 +21,24 @@ from .git_manager import GitManager
|
|
|
21
21
|
from .file_system_watcher import FileSystemWatcher
|
|
22
22
|
from ....logging_categories import get_categorized_logger, LogCategory
|
|
23
23
|
|
|
24
|
+
def _deterministic_file_tab_id(file_path: str) -> str:
|
|
25
|
+
try:
|
|
26
|
+
resolved = os.path.abspath(file_path)
|
|
27
|
+
mtime = int(Path(resolved).stat().st_mtime)
|
|
28
|
+
except OSError:
|
|
29
|
+
mtime = "unknown"
|
|
30
|
+
resolved = os.path.abspath(file_path)
|
|
31
|
+
return f"{resolved}:{mtime}"
|
|
32
|
+
|
|
33
|
+
def _deterministic_diff_tab_id(
|
|
34
|
+
file_path: str,
|
|
35
|
+
from_ref: str,
|
|
36
|
+
to_ref: str,
|
|
37
|
+
from_hash: Optional[str],
|
|
38
|
+
to_hash: Optional[str]
|
|
39
|
+
) -> str:
|
|
40
|
+
return f"diff:{os.path.abspath(file_path)}:{from_ref}:{to_ref}:{from_hash or ''}:{to_hash or ''}"
|
|
41
|
+
|
|
24
42
|
logger = get_categorized_logger(__name__)
|
|
25
43
|
|
|
26
44
|
# Global singleton instance
|
|
@@ -657,10 +675,10 @@ class ProjectStateManager:
|
|
|
657
675
|
# Create new file tab using tab factory
|
|
658
676
|
from ..tab_factory import get_tab_factory
|
|
659
677
|
tab_factory = get_tab_factory()
|
|
660
|
-
|
|
678
|
+
|
|
661
679
|
try:
|
|
662
680
|
logger.info(f"About to create tab for file: {file_path}")
|
|
663
|
-
new_tab = await tab_factory.create_file_tab(file_path)
|
|
681
|
+
new_tab = await tab_factory.create_file_tab(file_path, tab_id=_deterministic_file_tab_id(file_path))
|
|
664
682
|
logger.info(f"Tab created successfully, adding to project state")
|
|
665
683
|
project_state.open_tabs[tab_key] = new_tab
|
|
666
684
|
if set_active:
|
|
@@ -842,7 +860,11 @@ class ProjectStateManager:
|
|
|
842
860
|
diff_title = f"{os.path.basename(file_path)} ({' '.join(title_parts)})"
|
|
843
861
|
|
|
844
862
|
diff_tab = await tab_factory.create_diff_tab_with_title(
|
|
845
|
-
file_path,
|
|
863
|
+
file_path,
|
|
864
|
+
original_content,
|
|
865
|
+
modified_content,
|
|
866
|
+
diff_title,
|
|
867
|
+
tab_id=_deterministic_diff_tab_id(file_path, from_ref, to_ref, from_hash, to_hash),
|
|
846
868
|
diff_details=diff_details
|
|
847
869
|
)
|
|
848
870
|
|
|
@@ -7,6 +7,7 @@ import json
|
|
|
7
7
|
import logging
|
|
8
8
|
import math
|
|
9
9
|
import os
|
|
10
|
+
import re
|
|
10
11
|
import secrets
|
|
11
12
|
import shlex
|
|
12
13
|
import shutil
|
|
@@ -18,7 +19,7 @@ import time
|
|
|
18
19
|
import threading
|
|
19
20
|
from datetime import datetime, timezone
|
|
20
21
|
from pathlib import Path
|
|
21
|
-
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
22
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Set, Tuple
|
|
22
23
|
|
|
23
24
|
import platformdirs
|
|
24
25
|
|
|
@@ -201,6 +202,76 @@ def _current_time_iso() -> str:
|
|
|
201
202
|
return datetime.now(timezone.utc).isoformat()
|
|
202
203
|
|
|
203
204
|
|
|
205
|
+
def _to_int(value: Any, default: int = 0) -> int:
|
|
206
|
+
try:
|
|
207
|
+
return int(value)
|
|
208
|
+
except (TypeError, ValueError):
|
|
209
|
+
return default
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _to_float(value: Any, default: float = 0.0) -> float:
|
|
213
|
+
try:
|
|
214
|
+
return float(value)
|
|
215
|
+
except (TypeError, ValueError):
|
|
216
|
+
return default
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _calculate_available(total: Optional[float], used: float) -> Optional[float]:
|
|
220
|
+
if total is None:
|
|
221
|
+
return None
|
|
222
|
+
delta = total - used
|
|
223
|
+
return delta if delta >= 0 else 0.0
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _normalize_bytes(value: Any) -> float:
|
|
227
|
+
if isinstance(value, (int, float)):
|
|
228
|
+
return float(value)
|
|
229
|
+
text = str(value or "").strip()
|
|
230
|
+
if not text:
|
|
231
|
+
return 0.0
|
|
232
|
+
match = re.match(r"(?i)^\s*([0-9]*\.?[0-9]+)\s*([kmgtp]?i?b?)?\s*$", text)
|
|
233
|
+
if not match:
|
|
234
|
+
return 0.0
|
|
235
|
+
number = match.group(1)
|
|
236
|
+
unit = (match.group(2) or "").lower()
|
|
237
|
+
try:
|
|
238
|
+
value = float(number)
|
|
239
|
+
except ValueError:
|
|
240
|
+
return 0.0
|
|
241
|
+
if unit.startswith("k"):
|
|
242
|
+
return value * 1024
|
|
243
|
+
if unit.startswith("m"):
|
|
244
|
+
return value * 1024**2
|
|
245
|
+
if unit.startswith("g"):
|
|
246
|
+
return value * 1024**3
|
|
247
|
+
if unit.startswith("t"):
|
|
248
|
+
return value * 1024**4
|
|
249
|
+
if unit.startswith("p"):
|
|
250
|
+
return value * 1024**5
|
|
251
|
+
return value
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _bytes_to_mib(value: Any) -> int:
|
|
255
|
+
return int(round(_normalize_bytes(value) / (1024**2)))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _bytes_to_gib(value: Any) -> int:
|
|
259
|
+
return int(round(_normalize_bytes(value) / (1024**3)))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _normalize_storage_name(name: Any) -> str:
|
|
263
|
+
if not name:
|
|
264
|
+
return ""
|
|
265
|
+
return str(name).strip().lower()
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _parse_bool_flag(value: Any) -> bool:
|
|
269
|
+
if isinstance(value, bool):
|
|
270
|
+
return value
|
|
271
|
+
text = str(value or "").strip().lower()
|
|
272
|
+
return text in {"1", "true", "yes", "on"}
|
|
273
|
+
|
|
274
|
+
|
|
204
275
|
def _parse_iso_timestamp(value: str) -> Optional[datetime]:
|
|
205
276
|
if not value:
|
|
206
277
|
return None
|
|
@@ -381,35 +452,96 @@ def _load_managed_container_records() -> List[Dict[str, Any]]:
|
|
|
381
452
|
return records
|
|
382
453
|
|
|
383
454
|
|
|
384
|
-
def
|
|
455
|
+
def _extract_host_totals(node_status: Dict[str, Any] | None) -> Tuple[Optional[int], Optional[int], Optional[int]]:
|
|
456
|
+
if not node_status:
|
|
457
|
+
return None, None, None
|
|
458
|
+
memory_total = node_status.get("memory", {}).get("total")
|
|
459
|
+
disk_total = node_status.get("disk", {}).get("total")
|
|
460
|
+
cpu_cores = node_status.get("cpuinfo", {}).get("cores")
|
|
461
|
+
host_ram = _bytes_to_mib(memory_total) if memory_total is not None else None
|
|
462
|
+
host_disk = _bytes_to_gib(disk_total) if disk_total is not None else None
|
|
463
|
+
host_cpu = _to_int(cpu_cores) if cpu_cores is not None else None
|
|
464
|
+
return host_ram, host_disk, host_cpu
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _build_unmanaged_container_entry(ct: Dict[str, Any], cfg: Dict[str, Any], vmid: str) -> Dict[str, Any]:
|
|
468
|
+
ram_mib = _to_int(cfg.get("memory")) or _bytes_to_mib(ct.get("maxmem"))
|
|
469
|
+
disk_gib = _bytes_to_gib(ct.get("maxdisk"))
|
|
470
|
+
cpu_share = _to_float(
|
|
471
|
+
cfg.get("cpulimit")
|
|
472
|
+
or cfg.get("cpus")
|
|
473
|
+
or cfg.get("cores")
|
|
474
|
+
or ct.get("cpus")
|
|
475
|
+
or ct.get("cpu")
|
|
476
|
+
)
|
|
477
|
+
hostname = ct.get("name") or cfg.get("hostname") or f"ct{vmid}"
|
|
478
|
+
storage = cfg.get("storage") or ct.get("storage")
|
|
479
|
+
status = (ct.get("status") or "unknown").lower()
|
|
480
|
+
reserved = _parse_bool_flag(cfg.get("onboot"))
|
|
481
|
+
return {
|
|
482
|
+
"vmid": vmid,
|
|
483
|
+
"hostname": hostname,
|
|
484
|
+
"template": cfg.get("ostemplate"),
|
|
485
|
+
"storage": storage,
|
|
486
|
+
"disk_gib": disk_gib,
|
|
487
|
+
"ram_mib": ram_mib,
|
|
488
|
+
"cpu_share": cpu_share,
|
|
489
|
+
"reserve_on_boot": reserved,
|
|
490
|
+
"status": status,
|
|
491
|
+
"managed": False,
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _get_storage_snapshot(proxmox: Any, node: str, storage_name: str | None) -> Dict[str, Any] | None:
|
|
496
|
+
if not storage_name:
|
|
497
|
+
return None
|
|
498
|
+
try:
|
|
499
|
+
storage = proxmox.nodes(node).storage(storage_name).status.get()
|
|
500
|
+
except Exception as exc:
|
|
501
|
+
logger.debug("Unable to read storage status %s:%s: %s", node, storage_name, exc)
|
|
502
|
+
return None
|
|
503
|
+
total_bytes = storage.get("total")
|
|
504
|
+
avail_bytes = storage.get("avail")
|
|
505
|
+
used_bytes = storage.get("used")
|
|
506
|
+
return {
|
|
507
|
+
"storage": storage_name,
|
|
508
|
+
"total_gib": _bytes_to_gib(total_bytes) if total_bytes is not None else None,
|
|
509
|
+
"avail_gib": _bytes_to_gib(avail_bytes) if avail_bytes is not None else None,
|
|
510
|
+
"used_gib": _bytes_to_gib(used_bytes) if used_bytes is not None else None,
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _storage_matches_default(storage_name: Any, default_storage: str | None) -> bool:
|
|
516
|
+
if not default_storage:
|
|
517
|
+
return True
|
|
518
|
+
return _normalize_storage_name(storage_name) == _normalize_storage_name(default_storage)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _build_managed_containers_summary(
|
|
522
|
+
records: List[Dict[str, Any]],
|
|
523
|
+
unmanaged_records: List[Dict[str, Any]],
|
|
524
|
+
node_status: Dict[str, Any] | None,
|
|
525
|
+
storage_snapshot: Dict[str, Any] | None,
|
|
526
|
+
default_storage: str | None,
|
|
527
|
+
) -> Dict[str, Any]:
|
|
528
|
+
managed_containers: List[Dict[str, Any]] = []
|
|
385
529
|
total_ram = 0
|
|
386
530
|
total_disk = 0
|
|
387
531
|
total_cpu_share = 0.0
|
|
388
|
-
containers: List[Dict[str, Any]] = []
|
|
389
|
-
|
|
390
|
-
def _as_int(value: Any) -> int:
|
|
391
|
-
try:
|
|
392
|
-
return int(value)
|
|
393
|
-
except (TypeError, ValueError):
|
|
394
|
-
return 0
|
|
395
532
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
return 0.0
|
|
401
|
-
|
|
402
|
-
for record in sorted(records, key=lambda entry: _as_int(entry.get("vmid"))):
|
|
403
|
-
ram_mib = _as_int(record.get("ram_mib"))
|
|
404
|
-
disk_gib = _as_int(record.get("disk_gib"))
|
|
405
|
-
cpu_share = _as_float(record.get("cpus"))
|
|
533
|
+
for record in sorted(records, key=lambda entry: _to_int(entry.get("vmid"))):
|
|
534
|
+
ram_mib = _to_int(record.get("ram_mib"))
|
|
535
|
+
disk_gib = _to_int(record.get("disk_gib"))
|
|
536
|
+
cpu_share = _to_float(record.get("cpus"))
|
|
406
537
|
total_ram += ram_mib
|
|
407
538
|
total_disk += disk_gib
|
|
408
539
|
total_cpu_share += cpu_share
|
|
409
540
|
status = (record.get("status") or "unknown").lower()
|
|
410
|
-
|
|
541
|
+
managed_containers.append(
|
|
411
542
|
{
|
|
412
|
-
"vmid": str(
|
|
543
|
+
"vmid": str(_to_int(record.get("vmid"))) if record.get("vmid") is not None else None,
|
|
544
|
+
"device_id": record.get("device_id"),
|
|
413
545
|
"hostname": record.get("hostname"),
|
|
414
546
|
"template": record.get("template"),
|
|
415
547
|
"storage": record.get("storage"),
|
|
@@ -418,44 +550,105 @@ def _build_managed_containers_summary(records: List[Dict[str, Any]]) -> Dict[str
|
|
|
418
550
|
"cpu_share": cpu_share,
|
|
419
551
|
"created_at": record.get("created_at"),
|
|
420
552
|
"status": status,
|
|
553
|
+
"managed": True,
|
|
554
|
+
"uses_default_storage": _storage_matches_default(record.get("storage"), default_storage),
|
|
421
555
|
}
|
|
422
556
|
)
|
|
423
557
|
|
|
558
|
+
unmanaged_total_ram = sum(
|
|
559
|
+
_to_int(entry.get("ram_mib"))
|
|
560
|
+
for entry in unmanaged_records
|
|
561
|
+
if entry.get("reserve_on_boot")
|
|
562
|
+
)
|
|
563
|
+
unmanaged_total_disk = sum(
|
|
564
|
+
_to_int(entry.get("disk_gib"))
|
|
565
|
+
for entry in unmanaged_records
|
|
566
|
+
if entry.get("reserve_on_boot")
|
|
567
|
+
and _storage_matches_default(entry.get("storage"), default_storage)
|
|
568
|
+
)
|
|
569
|
+
unmanaged_total_cpu = sum(
|
|
570
|
+
_to_float(entry.get("cpu_share"))
|
|
571
|
+
for entry in unmanaged_records
|
|
572
|
+
if entry.get("reserve_on_boot")
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
allocated_ram = total_ram + unmanaged_total_ram
|
|
576
|
+
allocated_disk = total_disk + unmanaged_total_disk
|
|
577
|
+
allocated_cpu = total_cpu_share + unmanaged_total_cpu
|
|
578
|
+
|
|
579
|
+
host_ram_node, host_disk_node, host_cpu_node = _extract_host_totals(node_status)
|
|
580
|
+
storage_host_disk = storage_snapshot.get("total_gib") if storage_snapshot else None
|
|
581
|
+
host_ram = host_ram_node
|
|
582
|
+
host_disk = storage_host_disk if storage_host_disk is not None else host_disk_node
|
|
583
|
+
host_cpu = host_cpu_node
|
|
584
|
+
|
|
585
|
+
|
|
424
586
|
return {
|
|
425
587
|
"updated_at": datetime.utcnow().isoformat() + "Z",
|
|
426
|
-
"count": len(
|
|
588
|
+
"count": len(managed_containers),
|
|
427
589
|
"total_ram_mib": total_ram,
|
|
428
590
|
"total_disk_gib": total_disk,
|
|
429
591
|
"total_cpu_share": round(total_cpu_share, 2),
|
|
430
|
-
"containers":
|
|
592
|
+
"containers": managed_containers,
|
|
593
|
+
"unmanaged_containers": unmanaged_records,
|
|
594
|
+
"unmanaged_count": len(unmanaged_records),
|
|
595
|
+
"allocated_ram_mib": allocated_ram,
|
|
596
|
+
"allocated_disk_gib": allocated_disk,
|
|
597
|
+
"allocated_cpu_share": round(allocated_cpu, 2),
|
|
598
|
+
"available_ram_mib": _calculate_available(host_ram, allocated_ram) if host_ram is not None else None,
|
|
599
|
+
"available_disk_gib": _calculate_available(host_disk, allocated_disk) if host_disk is not None else None,
|
|
600
|
+
"available_cpu_share": _calculate_available(host_cpu, allocated_cpu) if host_cpu is not None else None,
|
|
601
|
+
"host_total_ram_mib": host_ram,
|
|
602
|
+
"host_total_disk_gib": host_disk,
|
|
603
|
+
"host_total_cpu_cores": host_cpu,
|
|
604
|
+
"default_storage": default_storage,
|
|
605
|
+
"default_storage_snapshot": storage_snapshot,
|
|
431
606
|
}
|
|
432
607
|
|
|
433
608
|
|
|
434
609
|
def _get_managed_containers_summary(force: bool = False) -> Dict[str, Any]:
|
|
435
|
-
def _refresh_container_statuses(
|
|
436
|
-
|
|
437
|
-
|
|
610
|
+
def _refresh_container_statuses(
|
|
611
|
+
records: List[Dict[str, Any]],
|
|
612
|
+
config: Dict[str, Any] | None,
|
|
613
|
+
managed_vmids: Set[str],
|
|
614
|
+
default_storage: str | None,
|
|
615
|
+
) -> Tuple[Dict[str, str], List[Dict[str, Any]], Dict[str, Any] | None, Dict[str, Any] | None]:
|
|
616
|
+
statuses: Dict[str, str] = {}
|
|
617
|
+
unmanaged: List[Dict[str, Any]] = []
|
|
618
|
+
node_status: Dict[str, Any] | None = None
|
|
619
|
+
if not config:
|
|
620
|
+
return statuses, unmanaged, node_status, None
|
|
621
|
+
proxmox = None
|
|
622
|
+
node = None
|
|
438
623
|
try:
|
|
439
624
|
proxmox = _connect_proxmox(config)
|
|
440
625
|
node = _get_node_from_config(config)
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
626
|
+
node_status = proxmox.nodes(node).status.get()
|
|
627
|
+
for ct in proxmox.nodes(node).lxc.get():
|
|
628
|
+
vmid_val = ct.get("vmid")
|
|
629
|
+
if vmid_val is None:
|
|
630
|
+
continue
|
|
631
|
+
vmid_key = str(_to_int(vmid_val))
|
|
632
|
+
statuses[vmid_key] = (ct.get("status") or "unknown").lower()
|
|
633
|
+
if vmid_key in managed_vmids:
|
|
634
|
+
continue
|
|
635
|
+
cfg: Dict[str, Any] = {}
|
|
636
|
+
try:
|
|
637
|
+
cfg = proxmox.nodes(node).lxc(vmid_key).config.get() or {}
|
|
638
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
639
|
+
logger.debug("Failed to read config for container %s: %s", vmid_key, exc)
|
|
640
|
+
description = (cfg.get("description") or "")
|
|
641
|
+
if MANAGED_MARKER in description:
|
|
642
|
+
continue
|
|
643
|
+
unmanaged.append(_build_unmanaged_container_entry(ct, cfg, vmid_key))
|
|
445
644
|
except Exception as exc: # pragma: no cover - best effort
|
|
446
645
|
logger.debug("Failed to refresh container statuses: %s", exc)
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
vmid_key = str(int(vmid))
|
|
454
|
-
except (ValueError, TypeError):
|
|
455
|
-
continue
|
|
456
|
-
status = statuses.get(vmid_key)
|
|
457
|
-
if status:
|
|
458
|
-
record["status"] = status
|
|
646
|
+
storage_snapshot = (
|
|
647
|
+
_get_storage_snapshot(proxmox, node, default_storage)
|
|
648
|
+
if proxmox and node
|
|
649
|
+
else None
|
|
650
|
+
)
|
|
651
|
+
return statuses, unmanaged, node_status, storage_snapshot
|
|
459
652
|
|
|
460
653
|
now = time.monotonic()
|
|
461
654
|
with _MANAGED_CONTAINERS_CACHE_LOCK:
|
|
@@ -465,8 +658,22 @@ def _get_managed_containers_summary(force: bool = False) -> Dict[str, Any]:
|
|
|
465
658
|
return cached
|
|
466
659
|
config = _load_config()
|
|
467
660
|
records = _load_managed_container_records()
|
|
468
|
-
|
|
469
|
-
|
|
661
|
+
managed_vmids: Set[str] = {
|
|
662
|
+
str(_to_int(record.get("vmid"))) for record in records if record.get("vmid") is not None
|
|
663
|
+
}
|
|
664
|
+
default_storage = config.get("default_storage") if config else None
|
|
665
|
+
statuses, unmanaged, node_status, storage_snapshot = _refresh_container_statuses(
|
|
666
|
+
records, config, managed_vmids, default_storage
|
|
667
|
+
)
|
|
668
|
+
for record in records:
|
|
669
|
+
vmid = record.get("vmid")
|
|
670
|
+
if vmid is None:
|
|
671
|
+
continue
|
|
672
|
+
vmid_key = str(_to_int(vmid))
|
|
673
|
+
status = statuses.get(vmid_key)
|
|
674
|
+
if status:
|
|
675
|
+
record["status"] = status
|
|
676
|
+
summary = _build_managed_containers_summary(records, unmanaged, node_status, storage_snapshot, default_storage)
|
|
470
677
|
with _MANAGED_CONTAINERS_CACHE_LOCK:
|
|
471
678
|
_MANAGED_CONTAINERS_CACHE["timestamp"] = now
|
|
472
679
|
_MANAGED_CONTAINERS_CACHE["summary"] = summary
|
|
@@ -607,7 +814,18 @@ def _build_bootstrap_steps(
|
|
|
607
814
|
)
|
|
608
815
|
steps.extend(
|
|
609
816
|
[
|
|
610
|
-
{
|
|
817
|
+
{
|
|
818
|
+
"name": "user_exists",
|
|
819
|
+
"cmd": (
|
|
820
|
+
f"id -u {user} >/dev/null 2>&1 || "
|
|
821
|
+
f"(if command -v adduser >/dev/null 2>&1 && adduser --disabled-password --help >/dev/null 2>&1; then "
|
|
822
|
+
f" adduser --disabled-password --gecos '' {user}; "
|
|
823
|
+
"else "
|
|
824
|
+
f" useradd -m -s /bin/sh {user}; "
|
|
825
|
+
"fi)"
|
|
826
|
+
),
|
|
827
|
+
"retries": 0,
|
|
828
|
+
},
|
|
611
829
|
{
|
|
612
830
|
"name": "add_sudo",
|
|
613
831
|
"cmd": (
|
|
@@ -633,7 +851,7 @@ def _build_bootstrap_steps(
|
|
|
633
851
|
{
|
|
634
852
|
"name": "add_sudoers",
|
|
635
853
|
"cmd": (
|
|
636
|
-
f"printf '%s ALL=(ALL) NOPASSWD:ALL
|
|
854
|
+
f"printf '%s ALL=(ALL) NOPASSWD:ALL\\n' {shlex.quote(user)} >/etc/sudoers.d/portacode && "
|
|
637
855
|
"chmod 0440 /etc/sudoers.d/portacode"
|
|
638
856
|
),
|
|
639
857
|
"retries": 0,
|
|
@@ -865,12 +1083,17 @@ def _parse_ctid(message: Dict[str, Any]) -> int:
|
|
|
865
1083
|
|
|
866
1084
|
|
|
867
1085
|
def _ensure_container_managed(
|
|
868
|
-
proxmox: Any, node: str, vmid: int
|
|
1086
|
+
proxmox: Any, node: str, vmid: int, *, device_id: Optional[str] = None
|
|
869
1087
|
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
|
870
1088
|
record = _read_container_record(vmid)
|
|
871
1089
|
ct_cfg = proxmox.nodes(node).lxc(str(vmid)).config.get()
|
|
872
1090
|
if not ct_cfg or MANAGED_MARKER not in (ct_cfg.get("description") or ""):
|
|
873
1091
|
raise RuntimeError(f"Container {vmid} is not managed by Portacode.")
|
|
1092
|
+
record_device_id = record.get("device_id")
|
|
1093
|
+
if device_id and str(record_device_id or "") != str(device_id):
|
|
1094
|
+
raise RuntimeError(
|
|
1095
|
+
f"Container {vmid} is managed for device {record_device_id!r}, not {device_id!r}."
|
|
1096
|
+
)
|
|
874
1097
|
return record, ct_cfg
|
|
875
1098
|
|
|
876
1099
|
|
|
@@ -1371,6 +1594,13 @@ def _instantiate_container(proxmox: Any, node: str, payload: Dict[str, Any]) ->
|
|
|
1371
1594
|
ssh_public_keys=payload.get("ssh_public_key") or None,
|
|
1372
1595
|
)
|
|
1373
1596
|
status, elapsed = _wait_for_task(proxmox, node, upid)
|
|
1597
|
+
exitstatus = (status or {}).get("exitstatus")
|
|
1598
|
+
if exitstatus and exitstatus.upper() != "OK":
|
|
1599
|
+
msg = status.get("status") or "unknown error"
|
|
1600
|
+
details = status.get("error") or status.get("errmsg") or status.get("description") or status
|
|
1601
|
+
raise RuntimeError(
|
|
1602
|
+
f"Container creation task failed ({exitstatus}): {msg} details={details}"
|
|
1603
|
+
)
|
|
1374
1604
|
return vmid, elapsed
|
|
1375
1605
|
except ResourceException as exc:
|
|
1376
1606
|
raise RuntimeError(f"Failed to create container: {exc}") from exc
|
|
@@ -1817,10 +2047,13 @@ class StartProxmoxContainerHandler(SyncHandler):
|
|
|
1817
2047
|
|
|
1818
2048
|
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1819
2049
|
vmid = _parse_ctid(message)
|
|
2050
|
+
child_device_id = (message.get("child_device_id") or "").strip()
|
|
2051
|
+
if not child_device_id:
|
|
2052
|
+
raise ValueError("child_device_id is required for start_proxmox_container")
|
|
1820
2053
|
config = _ensure_infra_configured()
|
|
1821
2054
|
proxmox = _connect_proxmox(config)
|
|
1822
2055
|
node = _get_node_from_config(config)
|
|
1823
|
-
_ensure_container_managed(proxmox, node, vmid)
|
|
2056
|
+
_ensure_container_managed(proxmox, node, vmid, device_id=child_device_id)
|
|
1824
2057
|
|
|
1825
2058
|
status, elapsed = _start_container(proxmox, node, vmid)
|
|
1826
2059
|
_update_container_record(vmid, {"status": "running"})
|
|
@@ -1847,10 +2080,13 @@ class StopProxmoxContainerHandler(SyncHandler):
|
|
|
1847
2080
|
|
|
1848
2081
|
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1849
2082
|
vmid = _parse_ctid(message)
|
|
2083
|
+
child_device_id = (message.get("child_device_id") or "").strip()
|
|
2084
|
+
if not child_device_id:
|
|
2085
|
+
raise ValueError("child_device_id is required for stop_proxmox_container")
|
|
1850
2086
|
config = _ensure_infra_configured()
|
|
1851
2087
|
proxmox = _connect_proxmox(config)
|
|
1852
2088
|
node = _get_node_from_config(config)
|
|
1853
|
-
_ensure_container_managed(proxmox, node, vmid)
|
|
2089
|
+
_ensure_container_managed(proxmox, node, vmid, device_id=child_device_id)
|
|
1854
2090
|
|
|
1855
2091
|
status, elapsed = _stop_container(proxmox, node, vmid)
|
|
1856
2092
|
final_status = status.get("status") or "stopped"
|
|
@@ -1883,10 +2119,13 @@ class RemoveProxmoxContainerHandler(SyncHandler):
|
|
|
1883
2119
|
|
|
1884
2120
|
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1885
2121
|
vmid = _parse_ctid(message)
|
|
2122
|
+
child_device_id = (message.get("child_device_id") or "").strip()
|
|
2123
|
+
if not child_device_id:
|
|
2124
|
+
raise ValueError("child_device_id is required for remove_proxmox_container")
|
|
1886
2125
|
config = _ensure_infra_configured()
|
|
1887
2126
|
proxmox = _connect_proxmox(config)
|
|
1888
2127
|
node = _get_node_from_config(config)
|
|
1889
|
-
_ensure_container_managed(proxmox, node, vmid)
|
|
2128
|
+
_ensure_container_managed(proxmox, node, vmid, device_id=child_device_id)
|
|
1890
2129
|
|
|
1891
2130
|
stop_status, stop_elapsed = _stop_container(proxmox, node, vmid)
|
|
1892
2131
|
delete_status, delete_elapsed = _delete_container(proxmox, node, vmid)
|
portacode/connection/terminal.py
CHANGED
|
@@ -685,6 +685,7 @@ class TerminalManager:
|
|
|
685
685
|
try:
|
|
686
686
|
# Initialize project state
|
|
687
687
|
project_state = await manager.initialize_project_state(session_name, project_folder_path)
|
|
688
|
+
await self._restore_tabs_from_session_metadata(manager, project_state, session)
|
|
688
689
|
|
|
689
690
|
# Send initial project state to the client
|
|
690
691
|
initial_state_payload = {
|
|
@@ -710,6 +711,85 @@ class TerminalManager:
|
|
|
710
711
|
|
|
711
712
|
except Exception as exc:
|
|
712
713
|
logger.exception("terminal_manager: Error initializing project states for new sessions: %s", exc)
|
|
714
|
+
|
|
715
|
+
async def _restore_tabs_from_session_metadata(self, manager, project_state, session):
|
|
716
|
+
"""Restore open tabs/active tab from client session metadata if available."""
|
|
717
|
+
if not session or not project_state:
|
|
718
|
+
return
|
|
719
|
+
|
|
720
|
+
descriptors = session.get("open_tabs") or []
|
|
721
|
+
if not descriptors:
|
|
722
|
+
return
|
|
723
|
+
|
|
724
|
+
session_id = project_state.client_session_id
|
|
725
|
+
logger.info("terminal_manager: 🧭 Restoring %d tabs from metadata for session %s", len(descriptors), session_id)
|
|
726
|
+
|
|
727
|
+
for descriptor in descriptors:
|
|
728
|
+
parsed = self._parse_tab_descriptor(descriptor)
|
|
729
|
+
if not parsed:
|
|
730
|
+
continue
|
|
731
|
+
|
|
732
|
+
tab_type = parsed.get("tab_type")
|
|
733
|
+
file_path = parsed.get("file_path")
|
|
734
|
+
metadata = parsed.get("metadata", {})
|
|
735
|
+
|
|
736
|
+
if tab_type == "file" and file_path:
|
|
737
|
+
try:
|
|
738
|
+
await manager.open_file(session_id, file_path, set_active=False)
|
|
739
|
+
except Exception as exc:
|
|
740
|
+
logger.warning("terminal_manager: Failed to restore file tab %s for session %s: %s", file_path, session_id, exc)
|
|
741
|
+
continue
|
|
742
|
+
|
|
743
|
+
if tab_type == "diff" and file_path:
|
|
744
|
+
from_ref = metadata.get("from") or metadata.get("from_ref")
|
|
745
|
+
to_ref = metadata.get("to") or metadata.get("to_ref")
|
|
746
|
+
if not from_ref or not to_ref:
|
|
747
|
+
logger.warning("terminal_manager: Skipping diff tab %s for session %s because from/to references are missing", file_path, session_id)
|
|
748
|
+
continue
|
|
749
|
+
from_hash = metadata.get("from_hash") or metadata.get("fromHash")
|
|
750
|
+
to_hash = metadata.get("to_hash") or metadata.get("toHash")
|
|
751
|
+
try:
|
|
752
|
+
await manager.open_diff_tab(session_id, file_path, from_ref, to_ref, from_hash=from_hash, to_hash=to_hash)
|
|
753
|
+
except Exception as exc:
|
|
754
|
+
logger.warning("terminal_manager: Failed to restore diff tab %s for session %s: %s", file_path, session_id, exc)
|
|
755
|
+
continue
|
|
756
|
+
|
|
757
|
+
logger.debug("terminal_manager: Unknown tab descriptor ignored for session %s: %s", session_id, descriptor)
|
|
758
|
+
|
|
759
|
+
active_index = session.get("active_tab")
|
|
760
|
+
try:
|
|
761
|
+
active_index_int = int(active_index) if active_index is not None else None
|
|
762
|
+
except (TypeError, ValueError):
|
|
763
|
+
active_index_int = None
|
|
764
|
+
|
|
765
|
+
if active_index_int is not None and active_index_int >= 0:
|
|
766
|
+
current_tabs = list(project_state.open_tabs.values())
|
|
767
|
+
if 0 <= active_index_int < len(current_tabs):
|
|
768
|
+
try:
|
|
769
|
+
await manager.set_active_tab(session_id, current_tabs[active_index_int].tab_id)
|
|
770
|
+
except Exception as exc:
|
|
771
|
+
logger.warning("terminal_manager: Failed to set active tab for session %s: %s", session_id, exc)
|
|
772
|
+
else:
|
|
773
|
+
logger.debug("terminal_manager: Active tab index %s out of range for session %s", active_index_int, session_id)
|
|
774
|
+
|
|
775
|
+
def _parse_tab_descriptor(self, descriptor: str) -> Optional[Dict[str, Any]]:
|
|
776
|
+
"""Parse a URL-friendly tab descriptor string."""
|
|
777
|
+
if not descriptor:
|
|
778
|
+
return None
|
|
779
|
+
|
|
780
|
+
try:
|
|
781
|
+
parts = descriptor.split("|")
|
|
782
|
+
tab_type = parts[0] if parts else None
|
|
783
|
+
file_path = parts[1] if len(parts) > 1 else None
|
|
784
|
+
metadata = {}
|
|
785
|
+
for part in parts[2:]:
|
|
786
|
+
if "=" in part:
|
|
787
|
+
key, value = part.split("=", 1)
|
|
788
|
+
metadata[key] = value
|
|
789
|
+
return {"tab_type": tab_type, "file_path": file_path, "metadata": metadata}
|
|
790
|
+
except Exception as exc:
|
|
791
|
+
logger.warning("terminal_manager: Failed to parse tab descriptor '%s': %s", descriptor, exc)
|
|
792
|
+
return None
|
|
713
793
|
|
|
714
794
|
async def _send_targeted_terminal_list(self, message: Dict[str, Any], target_sessions: List[str]) -> None:
|
|
715
795
|
"""Send terminal_list command to specific client sessions.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
portacode/README.md,sha256=4dKtpvR8LNgZPVz37GmkQCMWIr_u25Ao63iW56s7Ke4,775
|
|
2
2
|
portacode/__init__.py,sha256=oB3sV1wXr-um-RXio73UG8E5Xx6cF2ZVJveqjNmC-vQ,1086
|
|
3
3
|
portacode/__main__.py,sha256=jmHTGC1hzmo9iKJLv-SSYe9BSIbPPZ2IOpecI03PlTs,296
|
|
4
|
-
portacode/_version.py,sha256=
|
|
4
|
+
portacode/_version.py,sha256=7ZUO3SboTD37oV4BJWi-Sx9y4MGlZpUHiLsekZex9ds,719
|
|
5
5
|
portacode/cli.py,sha256=mGLKoZ-T2FBF7IA9wUq0zyG0X9__-A1ao7gajjcVRH8,21828
|
|
6
6
|
portacode/data.py,sha256=5-s291bv8J354myaHm1Y7CQZTZyRzMU3TGe5U4hb-FA,1591
|
|
7
7
|
portacode/keypair.py,sha256=0OO4vHDcF1XMxCDqce61xFTlFwlTcmqe5HyGsXFEt7s,5838
|
|
@@ -12,9 +12,9 @@ portacode/connection/README.md,sha256=f9rbuIEKa7cTm9C98rCiBbEtbiIXQU11esGSNhSMiJ
|
|
|
12
12
|
portacode/connection/__init__.py,sha256=atqcVGkViIEd7pRa6cP2do07RJOM0UWpbnz5zXjGktU,250
|
|
13
13
|
portacode/connection/client.py,sha256=jtLb9_YufqPkzi9t8VQH3iz_JEMisbtY6a8L9U5weiU,14181
|
|
14
14
|
portacode/connection/multiplex.py,sha256=L-TxqJ_ZEbfNEfu1cwxgJ5vUdyRzZjsMy2Kx1diiZys,5237
|
|
15
|
-
portacode/connection/terminal.py,sha256=
|
|
15
|
+
portacode/connection/terminal.py,sha256=n1Uu92JacV5K6d1Qwx94Tw9OB2Tpke5HqsW2NDn76Ls,49032
|
|
16
16
|
portacode/connection/handlers/README.md,sha256=HsLZG1QK1JNm67HsgL6WoDg9nxzKXxwkc5fJPFJdX5g,12169
|
|
17
|
-
portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=
|
|
17
|
+
portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=K7ZjHIWwscX0nBLinpEclO2TGSAiZPBr_JMJUGrn0To,103095
|
|
18
18
|
portacode/connection/handlers/__init__.py,sha256=WSeBmi65GWFQPYt9M3E10rn0uZ_EPCJzNJOzSf2HZyw,2921
|
|
19
19
|
portacode/connection/handlers/base.py,sha256=oENFb-Fcfzwk99Qx8gJQriEMiwSxwygwjOiuCH36hM4,10231
|
|
20
20
|
portacode/connection/handlers/chunked_content.py,sha256=h6hXRmxSeOgnIxoU8CkmvEf2Odv-ajPrpHIe_W3GKcA,9251
|
|
@@ -22,7 +22,7 @@ portacode/connection/handlers/diff_handlers.py,sha256=iYTIRCcpEQ03vIPKZCsMTE5aZb
|
|
|
22
22
|
portacode/connection/handlers/file_handlers.py,sha256=nAJH8nXnX07xxD28ngLpgIUzcTuRwZBNpEGEKdRqohw,39507
|
|
23
23
|
portacode/connection/handlers/project_aware_file_handlers.py,sha256=AqgMnDqX2893T2NsrvUSCwjN5VKj4Pb2TN0S_SuboOE,9803
|
|
24
24
|
portacode/connection/handlers/project_state_handlers.py,sha256=v6ZefGW9i7n1aZLq2jOGumJIjYb6aHlPI4m1jkYewm8,1686
|
|
25
|
-
portacode/connection/handlers/proxmox_infra.py,sha256=
|
|
25
|
+
portacode/connection/handlers/proxmox_infra.py,sha256=Z7C0ekRkTB7g4oeye2pE6K5EYdIg6QF6OAnmq1dBLBQ,80780
|
|
26
26
|
portacode/connection/handlers/registry.py,sha256=qXGE60sYEWg6ZtVQzFcZ5YI2XWR6lMgw4hAL9x5qR1I,6181
|
|
27
27
|
portacode/connection/handlers/session.py,sha256=uNGfiO_1B9-_yjJKkpvmbiJhIl6b-UXlT86UTfd6WYE,42219
|
|
28
28
|
portacode/connection/handlers/system_handlers.py,sha256=fr12QpOr_Z8KYGUU-AYrTQwRPAcrLK85hvj3SEq1Kw8,14757
|
|
@@ -35,7 +35,7 @@ portacode/connection/handlers/project_state/__init__.py,sha256=5ucIqk6Iclqg6bKkL
|
|
|
35
35
|
portacode/connection/handlers/project_state/file_system_watcher.py,sha256=r9_UKxWTbzum0jGqxIafe68Ced2Y3xOp3ZmkpBOfRpw,8573
|
|
36
36
|
portacode/connection/handlers/project_state/git_manager.py,sha256=iGQ7LYIA7uHsZHdj3HEc_LYo7S1Lqv6-AeyyMwknBPo,70027
|
|
37
37
|
portacode/connection/handlers/project_state/handlers.py,sha256=qgOSt26rxAGNxW07AoevTwDPBdxblX4J-dX-EjOKtg4,38232
|
|
38
|
-
portacode/connection/handlers/project_state/manager.py,sha256=
|
|
38
|
+
portacode/connection/handlers/project_state/manager.py,sha256=ori_QpeoY1sdpY8WDYIx-kl_gNfZ7o8eq84CcOBlvIs,67915
|
|
39
39
|
portacode/connection/handlers/project_state/models.py,sha256=EZTKvxHKs8QlQUbzI0u2IqfzfRRXZixUIDBwTGCJATI,4313
|
|
40
40
|
portacode/connection/handlers/project_state/utils.py,sha256=LsbQr9TH9Bz30FqikmtTxco4PlB_n0kUIuPKQ6Fb_mo,1665
|
|
41
41
|
portacode/link_capture/__init__.py,sha256=93LjyYDqzOimsIDBhsPibTl7tr-8DiIzyDF7JWQkE2A,1231
|
|
@@ -65,7 +65,7 @@ portacode/utils/__init__.py,sha256=NgBlWTuNJESfIYJzP_3adI1yJQJR0XJLRpSdVNaBAN0,3
|
|
|
65
65
|
portacode/utils/diff_apply.py,sha256=4Oi7ft3VUCKmiUE4VM-OeqO7Gk6H7PF3WnN4WHXtjxI,15157
|
|
66
66
|
portacode/utils/diff_renderer.py,sha256=S76StnQ2DLfsz4Gg0m07UwPfRp8270PuzbNaQq-rmYk,13850
|
|
67
67
|
portacode/utils/ntp_clock.py,sha256=VqCnWCTehCufE43W23oB-WUdAZGeCcLxkmIOPwInYHc,2499
|
|
68
|
-
portacode-1.4.
|
|
68
|
+
portacode-1.4.16.dev5.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
|
|
69
69
|
test_modules/README.md,sha256=Do_agkm9WhSzueXjRAkV_xEj6Emy5zB3N3VKY5Roce8,9274
|
|
70
70
|
test_modules/__init__.py,sha256=1LcbHodIHsB0g-g4NGjSn6AMuCoGbymvXPYLOb6Z7F0,53
|
|
71
71
|
test_modules/test_device_online.py,sha256=QtYq0Dq9vME8Gp2O4fGSheqVf8LUtpsSKosXXk56gGM,1654
|
|
@@ -91,8 +91,8 @@ testing_framework/core/playwright_manager.py,sha256=Tw46qwxIhOFkS48C2IWIQHHNpEe-
|
|
|
91
91
|
testing_framework/core/runner.py,sha256=j2QwNJmAxVBmJvcbVS7DgPJUKPNzqfLmt_4NNdaKmZU,19297
|
|
92
92
|
testing_framework/core/shared_cli_manager.py,sha256=BESSNtyQb7BOlaOvZmm04T8Uezjms4KCBs2MzTxvzYQ,8790
|
|
93
93
|
testing_framework/core/test_discovery.py,sha256=2FZ9fJ8Dp5dloA-fkgXoJ_gCMC_nYPBnA3Hs2xlagzM,4928
|
|
94
|
-
portacode-1.4.
|
|
95
|
-
portacode-1.4.
|
|
96
|
-
portacode-1.4.
|
|
97
|
-
portacode-1.4.
|
|
98
|
-
portacode-1.4.
|
|
94
|
+
portacode-1.4.16.dev5.dist-info/METADATA,sha256=qKD5PeIEODD6P9-ODtKSE5q5Tqg1UCk0e7N3xt_Ws-4,13051
|
|
95
|
+
portacode-1.4.16.dev5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
96
|
+
portacode-1.4.16.dev5.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
|
|
97
|
+
portacode-1.4.16.dev5.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
98
|
+
portacode-1.4.16.dev5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|