Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions GUI/src/vast/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,6 @@ def reposition_badge():
"Ground Image": self.ground_view,
"Auth": self.auth_status


main
}
for view in self.views.values():
self.stack.addWidget(view)
Expand Down
116 changes: 83 additions & 33 deletions GUI/src/vast/views/ground_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Optional, Any, Dict, List

from PyQt6.QtCore import Qt, QTimer, QSize, QRectF
from PyQt6.QtGui import QPixmap, QKeyEvent, QPainter, QColor, QPen, QBrush
from PyQt6.QtGui import QPixmap, QKeyEvent, QPainter, QColor, QPen, QFont
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QProgressBar, QMessageBox, QSizePolicy, QFrame
Expand All @@ -13,11 +13,9 @@
# GUI never touches MinIO directly – it uses DashboardApi only.
from vast.dashboard_api import DashboardApi


GROUND_BUCKET = os.getenv("GROUND_BUCKET", "ground")
GROUND_PREFIX = os.getenv("GROUND_PREFIX", "")


# ----------------------------
# PHI data model
# ----------------------------
Expand All @@ -29,7 +27,7 @@ class PhiSnapshot:
severity_avg: Optional[float] # usually 0..1 (clamped)
trend: Optional[float]
week_start: Optional[str]
source: str = "" # textual hint of data source
source: str = "" # textual hint of data source (NOT shown in UI)


def _phi_band_color(v: float) -> str:
Expand All @@ -51,62 +49,95 @@ def _safe_float(x) -> Optional[float]:


# ----------------------------
# Visual PHI circle
# Visual PHI circle (pie)
# ----------------------------
class PhiCircleWidget(QWidget):
"""
Draws a full circle:
- green background (healthy part)
- red pie slice for the infected part (severity in 0..1)
Draws a pie:
- red slice = severity in [0..1]
- green slice = 1 - severity (healthy remainder)
Always draws red on top so it's never hidden.
Also draws the severity percentage text centered on the pie.
"""
def __init__(self, parent=None):
super().__init__(parent)
self._severity = 0.0 # 0..1
self.setMinimumHeight(140)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.setAttribute(Qt.WidgetAttribute.WA_OpaquePaintEvent, True)
self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
self.setToolTip("Red slice = severity (0..1). Green = healthy remainder.")

def sizeHint(self):
return QSize(120, 120)

def minimumSizeHint(self):
return QSize(100, 100)

def setSeverity(self, value: float) -> None:
# Clamp to [0,1] and repaint
try:
v = float(value)
except Exception:
v = 0.0
self._severity = max(0.0, min(1.0, v))
self.update()
self.update() # ensure repaint

def paintEvent(self, event) -> None:
# Guard rendering so drawing can never crash the app
try:
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
painter.setPen(Qt.PenStyle.NoPen)

pad = 12
pad = 10
size = min(self.width(), self.height()) - 2 * pad
if size <= 0:
return
cx = (self.width() - size) / 2
cy = (self.height() - size) / 2
rect = QRectF(cx, cy, size, size)

# Healthy background (green)
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(QBrush(QColor("#16a34a")))
painter.drawEllipse(rect)
start = 90 * 16 # 12 o'clock (Qt: 0° is 3 o'clock)
full = -360 * 16

s = self._severity
# Degenerate cases
if s <= 1e-6:
painter.setBrush(QColor("#16a34a"))
painter.drawEllipse(rect)
elif s >= 1 - 1e-6:
painter.setBrush(QColor("#dc2626"))
painter.drawEllipse(rect)
else:
span_red = int(round(full * s)) # negative (clockwise)
span_green = full - span_red # the remainder

# Infected slice (red) from 12 o'clock clockwise
if self._severity > 0.0:
start_angle = 90 * 16 # 12 o'clock
span_angle = -360 * float(self._severity) * 16 # clockwise negative
painter.setBrush(QBrush(QColor("#dc2626")))
painter.drawPie(rect, start_angle, span_angle)
# Draw green remainder first
painter.setBrush(QColor("#16a34a"))
painter.drawPie(rect, start + span_red, span_green)

# Draw red slice on top (so it's always visible)
painter.setBrush(QColor("#dc2626"))
painter.drawPie(rect, start, span_red)

# Outline
pen = QPen(QColor("#334155"))
pen.setWidth(2)
painter.setPen(pen)
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.drawEllipse(rect)

# Percentage text (centered)
percent_text = f"{int(round(s * 100))}%"
font = QFont()
font.setBold(True)
font.setPointSize(int(size * 0.22)) # responsive sizing
painter.setFont(font)

# Soft shadow for readability
painter.setPen(QColor(0, 0, 0, 160))
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, percent_text)
# Foreground text
painter.setPen(QColor("#ffffff"))
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, percent_text)
Comment on lines +135 to +139
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Shadow text drawn at same position as foreground text - may not create visible shadow effect

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


except Exception as e:
print(f"[PhiCircleWidget] paintEvent error: {e}")

Expand Down Expand Up @@ -191,26 +222,38 @@ def __init__(self, api: DashboardApi, parent=None):
phi_layout.setContentsMargins(12, 12, 12, 12)
phi_layout.setSpacing(8)

# Row with headline + (trimmed) details
row = QHBoxLayout()
self.phi_label = QLabel("PHI: –")
self.phi_label.setStyleSheet("font-size:16px;font-weight:700;color:#0f172a;")
row.addWidget(self.phi_label)
row.addStretch(1)
self.phi_details = QLabel("")
self.phi_details = QLabel("") # will show severity/coverage/trend (without src)
self.phi_details.setStyleSheet("color:#475569;font-size:12px;")
row.addWidget(self.phi_details)
phi_layout.addLayout(row)

# PHI progress (axis-like) + pie at its side
self.phi_bar = QProgressBar()
self.phi_bar.setRange(0, 100)
self.phi_bar.setValue(0)
self.phi_bar.setFormat("%v")
self._style_phi_bar(None)
phi_layout.addWidget(self.phi_bar)

# PHI circle (green background, red severity slice)
phi_row2 = QHBoxLayout()
phi_row2.setContentsMargins(0, 0, 0, 0)
phi_row2.setSpacing(10)
phi_row2.addWidget(self.phi_bar, stretch=1)

self.phi_circle = PhiCircleWidget()
phi_layout.addWidget(self.phi_circle)
self.phi_circle.setFixedSize(120, 120)
phi_row2.addWidget(self.phi_circle)

phi_layout.addLayout(phi_row2)

legend = QLabel("אדום = Severity | ירוק = Healthy")
legend.setStyleSheet("color:#64748b;font-size:11px;")
phi_layout.addWidget(legend)

root.addWidget(phi_frame, stretch=1)

Expand Down Expand Up @@ -297,6 +340,7 @@ def _lm(o):
self._keys = keys
self._idx = 0 if self._keys else -1
self._update_counter()
self._update_nav_buttons()
if self._idx >= 0:
self.load_current_image()
else:
Expand All @@ -312,6 +356,11 @@ def _update_counter(self) -> None:
pos = (self._idx + 1) if self._idx >= 0 else 0
self.counter_label.setText(f"({pos} / {total})")

def _update_nav_buttons(self) -> None:
has = bool(self._keys)
for b in (self.btn_prev, self.btn_next, self.btn_show_phi):
b.setEnabled(has)

def prev_image(self) -> None:
if not self._keys:
return
Expand Down Expand Up @@ -348,6 +397,7 @@ def _set_image(self, pix: Optional[QPixmap]) -> None:
target_size: QSize = self.image_label.size()
if target_size.width() <= 4 or target_size.height() <= 4:
self.image_label.setPixmap(pix)
self.image_label.setText("")
return
scaled = pix.scaled(
target_size.width(),
Expand Down Expand Up @@ -377,14 +427,14 @@ def load_current_image(self) -> None:
getter = getattr(self.api, "get_image_bytes_from_minio", None)
if not callable(getter):
self._warn("DashboardApi.get_image_bytes_from_minio is missing.")
self._set_image(None)
self._render_phi_none()
return

data = None
try:
# Prefer signature with bucket param
data = getter(key, bucket=GROUND_BUCKET)
except TypeError:
# Older signature without bucket
data = getter(key)
except Exception as e:
self._warn(f"Failed fetching image bytes: {e}")
Expand All @@ -411,6 +461,7 @@ def load_current_image(self) -> None:

except Exception as e:
self._warn(f"load_current_image error: {e}")
self._render_phi_none()

# ----------------------------
# PHI flow
Expand Down Expand Up @@ -496,6 +547,7 @@ def _render_phi(self, snap: PhiSnapshot) -> None:
val = max(0, min(100, int(round(snap.phi))))
self.phi_label.setText(f"PHI: {val}")
parts = []
# keep useful metrics, but DO NOT show 'src=' anymore
if snap.density is not None:
parts.append(f"density={snap.density:.2f}")
if snap.coverage is not None:
Expand All @@ -506,9 +558,7 @@ def _render_phi(self, snap: PhiSnapshot) -> None:
parts.append(f"trend={snap.trend:+.2f}")
if snap.week_start:
parts.append(f"week={snap.week_start}")
if snap.source:
parts.append(f"src={snap.source}")
self.phi_details.setText(" | ".join(parts))
self.phi_details.setText(" | ".join(parts)) # no src here
self.phi_bar.setValue(val)
self._style_phi_bar(val)

Expand Down
Empty file.
78 changes: 39 additions & 39 deletions mqtt_and_kafka/mqtt-router/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
FROM python:3.12-slim
# ---- Build-time toggle for NetFree CA injection ----
ARG USE_NETFREE=false
# ---- System deps (CA, curl). librdkafka1 helps if confluent-kafka wheel is not fully static on your base ----
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl librdkafka1 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# ---- Optional NetFree certificates (mount or COPY your certs/*.crt alongside the Dockerfile) ----
# If you keep certs in repo, uncomment the next line:
# COPY certs/*.crt /app/certs/
RUN if [ "$USE_NETFREE" = "true" ] && [ -d /app/certs ] && ls /app/certs/*.crt >/dev/null 2>&1; then \
echo "Configuring NetFree certificates..."; \
cp /app/certs/*.crt /usr/local/share/ca-certificates/ && update-ca-certificates; \
else \
echo "No NetFree certs applied (USE_NETFREE=$USE_NETFREE)."; \
fi
# ---- Make requests/libs use system CA (works both with and without NetFree) ----
ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \
REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \
PIP_CERT=/etc/ssl/certs/ca-certificates.crt \
PYTHONUNBUFFERED=1
# ---- Install Python deps ----
COPY requirements.txt .
# When behind NetFree, trusted-host can help even אם אין צורך זה לא מזיק:
RUN python -m pip install --no-cache-dir \
--trusted-host pypi.org --trusted-host files.pythonhosted.org \
-r requirements.txt
# ---- App code ----
COPY app.py .
ENTRYPOINT ["python", "app.py"]
FROM python:3.12-slim

# ---- Build-time toggle for NetFree CA injection ----
ARG USE_NETFREE=false

# ---- System deps (CA, curl). librdkafka1 helps if confluent-kafka wheel is not fully static on your base ----
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl librdkafka1 \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

# ---- Optional NetFree certificates (mount or COPY your certs/*.crt alongside the Dockerfile) ----
# If you keep certs in repo, uncomment the next line:
# COPY certs/*.crt /app/certs/
RUN if [ "$USE_NETFREE" = "true" ] && [ -d /app/certs ] && ls /app/certs/*.crt >/dev/null 2>&1; then \
echo "Configuring NetFree certificates..."; \
cp /app/certs/*.crt /usr/local/share/ca-certificates/ && update-ca-certificates; \
else \
echo "No NetFree certs applied (USE_NETFREE=$USE_NETFREE)."; \
fi

# ---- Make requests/libs use system CA (works both with and without NetFree) ----
ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \
REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \
PIP_CERT=/etc/ssl/certs/ca-certificates.crt \
PYTHONUNBUFFERED=1

# ---- Install Python deps ----
COPY requirements.txt .
# When behind NetFree, trusted-host can help even אם אין צורך זה לא מזיק:
RUN python -m pip install --no-cache-dir \
--trusted-host pypi.org --trusted-host files.pythonhosted.org \
-r requirements.txt

# ---- App code ----
COPY app.py .

ENTRYPOINT ["python", "app.py"]
Loading