Skip to content

Commit b5d9e8f

Browse files
authored
Merge pull request #210 from chvvkumar/dev
Mosaics Customization, Activity Feed Improvements, and UI Polish
2 parents 2ed6c0c + 020d406 commit b5d9e8f

39 files changed

Lines changed: 1259 additions & 714 deletions

backend/alembic/versions/0002_consolidated_schema_evolution.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,81 @@ def upgrade() -> None:
227227

228228
conn.execute(text("UPDATE mosaics SET needs_review = true"))
229229

230+
# -- Activity events: parent_id for hierarchical grouping (was 0003) -----
231+
232+
if not _column_exists("activity_events", "parent_id"):
233+
op.add_column(
234+
"activity_events",
235+
sa.Column(
236+
"parent_id",
237+
sa.BigInteger(),
238+
sa.ForeignKey("activity_events.id", ondelete="CASCADE"),
239+
nullable=True,
240+
),
241+
)
242+
op.execute(text(
243+
"CREATE INDEX IF NOT EXISTS ix_activity_events_parent_id "
244+
"ON activity_events (parent_id)"
245+
))
246+
247+
# -- Custom columns: mosaic support (was 0004) ----------------------------
248+
249+
op.alter_column("custom_column_values", "target_id", nullable=True)
250+
251+
op.execute(text("ALTER TYPE applies_to_enum ADD VALUE IF NOT EXISTS 'mosaic'"))
252+
253+
_add_col(
254+
"custom_column_values",
255+
sa.Column(
256+
"mosaic_id",
257+
UUID(as_uuid=True),
258+
sa.ForeignKey("mosaics.id", ondelete="CASCADE"),
259+
nullable=True,
260+
),
261+
)
262+
op.execute(text(
263+
"CREATE INDEX IF NOT EXISTS ix_custom_column_values_mosaic "
264+
"ON custom_column_values (mosaic_id)"
265+
))
266+
267+
# Rebuild unique constraint to include mosaic_id
268+
try:
269+
op.drop_index("uq_custom_column_value", table_name="custom_column_values")
270+
except Exception:
271+
pass # index may not exist yet on fresh installs
272+
op.execute(text("""
273+
CREATE UNIQUE INDEX IF NOT EXISTS uq_custom_column_value
274+
ON custom_column_values (
275+
column_id,
276+
COALESCE(target_id, '00000000-0000-0000-0000-000000000000'),
277+
COALESCE(mosaic_id, '00000000-0000-0000-0000-000000000000'),
278+
COALESCE(session_date, '1970-01-01'),
279+
COALESCE(rig_label, '')
280+
)
281+
"""))
282+
230283

231284
def downgrade() -> None:
285+
# -- Undo mosaic custom columns (was 0004) --------------------------------
286+
op.drop_index("uq_custom_column_value", table_name="custom_column_values")
287+
op.execute(text("""
288+
CREATE UNIQUE INDEX uq_custom_column_value
289+
ON custom_column_values (
290+
column_id, target_id,
291+
COALESCE(session_date, '1970-01-01'),
292+
COALESCE(rig_label, '')
293+
)
294+
"""))
295+
op.execute(text("DROP INDEX IF EXISTS ix_custom_column_values_mosaic"))
296+
if _column_exists("custom_column_values", "mosaic_id"):
297+
op.drop_column("custom_column_values", "mosaic_id")
298+
op.alter_column("custom_column_values", "target_id", nullable=False)
299+
300+
# -- Undo activity parent_id (was 0003) -----------------------------------
301+
op.execute(text("DROP INDEX IF EXISTS ix_activity_events_parent_id"))
302+
if _column_exists("activity_events", "parent_id"):
303+
op.drop_column("activity_events", "parent_id")
304+
232305
op.execute(text("DROP TABLE IF EXISTS mosaic_panel_sessions"))
233306
op.execute(text("DROP TABLE IF EXISTS target_catalog_memberships"))
234307
op.execute(text("DROP TABLE IF EXISTS activity_events"))

backend/app/api/activity.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,13 @@ async def list_activity(
5151
):
5252
"""Return paginated activity events, newest first.
5353
54-
Keyset pagination via `cursor` (encodes timestamp + id of last row from
55-
previous page). `since` is used by the error toast poller.
54+
Only top-level events (parent_id IS NULL) are returned. Children are
55+
batch-loaded and nested under their parent's `children` field.
5656
"""
57-
count_q = select(func.count(ActivityEvent.id))
58-
items_q = select(ActivityEvent)
57+
top_level_filter = ActivityEvent.parent_id.is_(None)
58+
59+
count_q = select(func.count(ActivityEvent.id)).where(top_level_filter)
60+
items_q = select(ActivityEvent).where(top_level_filter)
5961

6062
valid_sev = [s for s in severity if s in _VALID_SEVERITIES]
6163
if valid_sev:
@@ -99,8 +101,28 @@ async def list_activity(
99101
last = rows[-1]
100102
next_cursor = _encode_cursor(last.timestamp, last.id)
101103

104+
parent_ids = [r.id for r in rows]
105+
children_by_parent: dict[int, list[ActivityItem]] = {}
106+
if parent_ids:
107+
children_q = (
108+
select(ActivityEvent)
109+
.where(ActivityEvent.parent_id.in_(parent_ids))
110+
.order_by(ActivityEvent.timestamp.asc())
111+
)
112+
children_result = await session.execute(children_q)
113+
for child in children_result.scalars().all():
114+
children_by_parent.setdefault(child.parent_id, []).append(
115+
ActivityItem.model_validate(child)
116+
)
117+
118+
items = []
119+
for r in rows:
120+
item = ActivityItem.model_validate(r)
121+
item.children = children_by_parent.get(r.id) or None
122+
items.append(item)
123+
102124
return PaginatedActivityResponse(
103-
items=[ActivityItem.model_validate(r) for r in rows],
125+
items=items,
104126
next_cursor=next_cursor,
105127
total=total,
106128
)

backend/app/api/custom_columns.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,34 @@ async def get_custom_values(
151151
return [
152152
CustomColumnValueResponse(
153153
column_id=str(v.column_id), column_slug=slug,
154-
target_id=str(v.target_id), session_date=v.session_date,
154+
target_id=str(v.target_id) if v.target_id else None,
155+
mosaic_id=str(v.mosaic_id) if v.mosaic_id else None,
156+
session_date=v.session_date,
157+
rig_label=v.rig_label, value=v.value,
158+
updated_by=str(v.updated_by), updated_at=v.updated_at,
159+
)
160+
for v, slug in rows
161+
]
162+
163+
164+
@router.get("/values/mosaic/{mosaic_id}", response_model=list[CustomColumnValueResponse])
165+
async def get_mosaic_custom_values(
166+
mosaic_id: uuid.UUID,
167+
session: AsyncSession = Depends(get_session),
168+
user: User = Depends(get_current_user),
169+
):
170+
q = (
171+
select(CustomColumnValue, CustomColumn.slug)
172+
.join(CustomColumn)
173+
.where(CustomColumnValue.mosaic_id == mosaic_id)
174+
)
175+
rows = (await session.execute(q)).all()
176+
return [
177+
CustomColumnValueResponse(
178+
column_id=str(v.column_id), column_slug=slug,
179+
target_id=str(v.target_id) if v.target_id else None,
180+
mosaic_id=str(v.mosaic_id) if v.mosaic_id else None,
181+
session_date=v.session_date,
155182
rig_label=v.rig_label, value=v.value,
156183
updated_by=str(v.updated_by), updated_at=v.updated_at,
157184
)
@@ -166,7 +193,8 @@ async def set_custom_value(
166193
user: User = Depends(get_current_user),
167194
):
168195
col_id = uuid.UUID(body.column_id)
169-
target_id = uuid.UUID(body.target_id)
196+
target_id = uuid.UUID(body.target_id) if body.target_id else None
197+
mosaic_id = uuid.UUID(body.mosaic_id) if body.mosaic_id else None
170198

171199
col = await session.get(CustomColumn, col_id)
172200
if not col:
@@ -179,8 +207,15 @@ async def set_custom_value(
179207

180208
conditions = [
181209
CustomColumnValue.column_id == col_id,
182-
CustomColumnValue.target_id == target_id,
183210
]
211+
if target_id is not None:
212+
conditions.append(CustomColumnValue.target_id == target_id)
213+
else:
214+
conditions.append(CustomColumnValue.target_id.is_(None))
215+
if mosaic_id is not None:
216+
conditions.append(CustomColumnValue.mosaic_id == mosaic_id)
217+
else:
218+
conditions.append(CustomColumnValue.mosaic_id.is_(None))
184219
if body.session_date is not None:
185220
conditions.append(CustomColumnValue.session_date == body.session_date)
186221
else:
@@ -201,6 +236,7 @@ async def set_custom_value(
201236
val = CustomColumnValue(
202237
column_id=col_id,
203238
target_id=target_id,
239+
mosaic_id=mosaic_id,
204240
session_date=body.session_date,
205241
rig_label=body.rig_label,
206242
value=body.value,

backend/app/api/mosaics.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from app.database import get_session
1212
from app.models import Image, Target, User, UserSettings, SETTINGS_ROW_ID
13+
from app.models.custom_column import CustomColumn, CustomColumnValue, AppliesTo
1314
from app.models.mosaic import Mosaic
1415
from app.models.mosaic_panel import MosaicPanel
1516
from app.models.mosaic_panel_session import MosaicPanelSession
@@ -613,6 +614,19 @@ async def dismiss_suggestion(
613614
return {"status": "ok"}
614615

615616

617+
@router.post("/clear-reviews")
618+
async def clear_all_reviews(
619+
session: AsyncSession = Depends(get_session),
620+
user: User = Depends(get_current_user),
621+
):
622+
from sqlalchemy import update
623+
await session.execute(
624+
update(Mosaic).where(Mosaic.needs_review == True).values(needs_review=False)
625+
)
626+
await session.commit()
627+
return {"ok": True}
628+
629+
616630
@router.get("", response_model=list[MosaicSummary])
617631
async def list_mosaics(
618632
session: AsyncSession = Depends(get_session),
@@ -739,6 +753,25 @@ async def list_mosaics(
739753
else:
740754
panel_dates_map[pid] = (None, None)
741755

756+
# Batch-load custom column values for all mosaics
757+
mosaic_ids = [m.id for m in mosaics]
758+
custom_values_map: dict[str, dict[str, str]] = {}
759+
if mosaic_ids:
760+
cv_q = (
761+
select(CustomColumnValue.mosaic_id, CustomColumn.slug, CustomColumnValue.value)
762+
.join(CustomColumn)
763+
.where(
764+
CustomColumnValue.mosaic_id.in_(mosaic_ids),
765+
CustomColumn.applies_to == AppliesTo.mosaic,
766+
)
767+
)
768+
cv_rows = (await session.execute(cv_q)).all()
769+
for mid, slug, val in cv_rows:
770+
mid_str = str(mid)
771+
if mid_str not in custom_values_map:
772+
custom_values_map[mid_str] = {}
773+
custom_values_map[mid_str][slug] = val
774+
742775
results = []
743776
for m in mosaics:
744777
total_int = 0
@@ -778,6 +811,7 @@ async def list_mosaics(
778811
first_session=mosaic_first,
779812
last_session=mosaic_last,
780813
needs_review=m.needs_review,
814+
custom_values=custom_values_map.get(str(m.id)),
781815
))
782816
return results
783817

backend/app/models/activity_event.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,9 @@ class ActivityEvent(Base):
3030
)
3131
actor: Mapped[str | None] = mapped_column(String(64), nullable=True)
3232
duration_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
33+
parent_id: Mapped[int | None] = mapped_column(
34+
BigInteger,
35+
ForeignKey("activity_events.id", ondelete="CASCADE"),
36+
nullable=True,
37+
index=True,
38+
)

backend/app/models/custom_column.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class AppliesTo(str, enum.Enum):
2121
target = "target"
2222
session = "session"
2323
rig = "rig"
24+
mosaic = "mosaic"
2425

2526

2627
class CustomColumn(Base):
@@ -50,8 +51,11 @@ class CustomColumnValue(Base):
5051
column_id: Mapped[uuid.UUID] = mapped_column(
5152
UUID(as_uuid=True), ForeignKey("custom_columns.id", ondelete="CASCADE"), nullable=False,
5253
)
53-
target_id: Mapped[uuid.UUID] = mapped_column(
54-
UUID(as_uuid=True), ForeignKey("targets.id", ondelete="CASCADE"), nullable=False,
54+
target_id: Mapped[uuid.UUID | None] = mapped_column(
55+
UUID(as_uuid=True), ForeignKey("targets.id", ondelete="CASCADE"), nullable=True,
56+
)
57+
mosaic_id: Mapped[uuid.UUID | None] = mapped_column(
58+
UUID(as_uuid=True), ForeignKey("mosaics.id", ondelete="CASCADE"), nullable=True,
5559
)
5660
session_date: Mapped[datetime | None] = mapped_column(Date, nullable=True)
5761
rig_label: Mapped[str | None] = mapped_column(String(255), nullable=True)
@@ -66,11 +70,14 @@ class CustomColumnValue(Base):
6670
__table_args__ = (
6771
Index(
6872
"uq_custom_column_value",
69-
"column_id", "target_id",
73+
"column_id",
74+
func.coalesce(target_id, "00000000-0000-0000-0000-000000000000"),
75+
func.coalesce(mosaic_id, "00000000-0000-0000-0000-000000000000"),
7076
func.coalesce(session_date, "1970-01-01"),
7177
func.coalesce(rig_label, ""),
7278
unique=True,
7379
),
7480
Index("ix_custom_column_values_target", "target_id"),
7581
Index("ix_custom_column_values_column", "column_id"),
82+
Index("ix_custom_column_values_mosaic", "mosaic_id"),
7683
)

backend/app/schemas/activity.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ class ActivityItem(BaseModel):
1818
target_id: UUID | None = None
1919
actor: str | None = None
2020
duration_ms: int | None = None
21+
parent_id: int | None = None
22+
children: list[ActivityItem] | None = None
2123

2224
model_config = {"from_attributes": True}
2325

backend/app/schemas/custom_column.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
class CustomColumnCreate(BaseModel):
99
name: str = Field(..., min_length=1, max_length=255)
1010
column_type: str = Field(..., pattern=r"^(boolean|text|dropdown)$")
11-
applies_to: str = Field(..., pattern=r"^(target|session|rig)$")
11+
applies_to: str = Field(..., pattern=r"^(target|session|rig|mosaic)$")
1212
dropdown_options: list[str] | None = None
1313

1414

@@ -34,7 +34,8 @@ class CustomColumnResponse(BaseModel):
3434

3535
class CustomColumnValueSet(BaseModel):
3636
column_id: str
37-
target_id: str
37+
target_id: str | None = None
38+
mosaic_id: str | None = None
3839
session_date: date | None = None
3940
rig_label: str | None = None
4041
value: str
@@ -43,7 +44,8 @@ class CustomColumnValueSet(BaseModel):
4344
class CustomColumnValueResponse(BaseModel):
4445
column_id: str
4546
column_slug: str
46-
target_id: str
47+
target_id: str | None = None
48+
mosaic_id: str | None = None
4749
session_date: date | None = None
4850
rig_label: str | None = None
4951
value: str

backend/app/schemas/mosaic.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class MosaicSummary(BaseModel):
7575
first_session: str | None = None
7676
last_session: str | None = None
7777
needs_review: bool = False
78+
custom_values: dict[str, str] | None = None
7879

7980

8081
class MosaicPanelBatchItem(BaseModel):

backend/app/schemas/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class ColumnVisibility(BaseModel):
6565
dashboard: TableColumnVisibility = TableColumnVisibility()
6666
session_table: TableColumnVisibility = TableColumnVisibility()
6767
session_detail: TableColumnVisibility = TableColumnVisibility()
68+
mosaic_table: TableColumnVisibility = TableColumnVisibility()
6869

6970

7071
class GraphSettings(BaseModel):

0 commit comments

Comments
 (0)