Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
761686a
[ADD] war-room notes: folder tree, per-note revisions, tree-aware RES…
Jul 8, 2026
6f4a93d
[ADD] war-room timelines: project linked-case timelines as toggleable…
Jul 8, 2026
7233194
[IMP] war-room timeline event: parity with case events (uuid, parent,…
Jul 8, 2026
0d8f96c
[FIX] nginx CSP: allow ws:/wss: in connect-src so /collab socket hand…
Jul 8, 2026
f90eddf
[ADD] war-room chat: sticky pins + polls (multi-select, deadline, ano…
Jul 8, 2026
4b60129
[FIX] war-room chat hooks: register missing on_postload_war_room_{mes…
Jul 8, 2026
b84a98d
[FIX] responses.response(): json.dumps unconditionally so data=None r…
Jul 9, 2026
77ce197
[FIX] collab/render.py: rewrite legacy IRIS v2.4.29 case URLs (/case/…
Jul 9, 2026
588f29e
[ADD] War room chat topics — model, migration, /topic slash, REST end…
Jul 9, 2026
cbe05ac
[ADD] War room chat topics — expose topic_id on trace-log serializer
Jul 9, 2026
09168fe
[ADD] collab: war-room-summary doc kind — resolve+flush against WarRo…
Jul 13, 2026
61ad232
[ADD] war room teams (@-mention groups) + chat message attachments — …
Jul 13, 2026
e6e3388
[ADD] war room task subtasks + tag/status filters — parent_task_id co…
Jul 13, 2026
fb1a764
[ADD] /api/v2/manage/custom-attributes/validate — dry-run schema vali…
Jul 13, 2026
4e619d8
[IMP] alerts v2 consolidation — merge alerts_filters blueprint into /…
Jul 13, 2026
27abddb
[FIX] reconcile default image tags to v2.5.0-beta.1 — README + docker…
Jul 13, 2026
8be579a
[FIX] drop iris-web/docker/frontend/Dockerfile — the SvelteKit build …
Jul 13, 2026
45a3faa
Merge upstream/develop into sync/upstream-2026-07-13
Che4ter Jul 13, 2026
a61da26
[FIX] add missing alembic merge-heads migration + ignore .claude/
Che4ter Jul 14, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ certificates/web_certificates/*.key
local_dev/
.serena/
.tokensave
.claude/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ git clone https://github.com/dfir-iris/iris-web.git
cd iris-web

# Checkout to the last tagged version
git checkout v2.4.20
git checkout v2.5.0-beta.1
# Copy the environment file
cp .env.model .env

Expand Down
2 changes: 1 addition & 1 deletion docker-compose.new-ui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ services:
# install` on boot — the image carries everything it needs.
build:
context: ${IRIS_SVELTEKIT_FRONTEND_DIR:-../iris-frontend}
dockerfile: ${IRIS_FRONTEND_DOCKERFILE:-../iris-web/docker/frontend/Dockerfile}
dockerfile: ${IRIS_FRONTEND_DOCKERFILE:-Dockerfile}
image: iris_frontend:newui
profiles: ["new-ui"]
container_name: iris_sveltekit_frontend
Expand Down
8 changes: 4 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ services:
extends:
file: docker-compose.base.yml
service: db
image: ${DB_IMAGE_NAME:-ghcr.io/dfir-iris/iriswebapp_db}:${DB_IMAGE_TAG:-v2.4.20}
image: ${DB_IMAGE_NAME:-ghcr.io/dfir-iris/iriswebapp_db}:${DB_IMAGE_TAG:-v2.5.0-beta.1}
build: docker/db/
restart: always

Expand All @@ -36,7 +36,7 @@ services:
extends:
file: docker-compose.base.yml
service: app
image: ${APP_IMAGE_NAME:-ghcr.io/dfir-iris/iriswebapp_app}:${APP_IMAGE_TAG:-v2.4.20}
image: ${APP_IMAGE_NAME:-ghcr.io/dfir-iris/iriswebapp_app}:${APP_IMAGE_TAG:-v2.5.0-beta.1}
build:
context: .
dockerfile: docker/webApp/Dockerfile
Expand All @@ -47,7 +47,7 @@ services:
extends:
file: docker-compose.base.yml
service: worker
image: ${APP_IMAGE_NAME:-ghcr.io/dfir-iris/iriswebapp_app}:${APP_IMAGE_TAG:-v2.4.20}
image: ${APP_IMAGE_NAME:-ghcr.io/dfir-iris/iriswebapp_app}:${APP_IMAGE_TAG:-v2.5.0-beta.1}
build:
context: .
dockerfile: docker/webApp/Dockerfile
Expand All @@ -58,7 +58,7 @@ services:
extends:
file: docker-compose.base.yml
service: nginx
image: ${NGINX_IMAGE_NAME:-ghcr.io/dfir-iris/iriswebapp_nginx}:${NGINX_IMAGE_TAG:-v2.4.20}
image: ${NGINX_IMAGE_NAME:-ghcr.io/dfir-iris/iriswebapp_nginx}:${NGINX_IMAGE_TAG:-v2.5.0-beta.1}
build:
context: docker/nginx/
args:
Expand Down
61 changes: 0 additions & 61 deletions docker/frontend/Dockerfile

This file was deleted.

8 changes: 7 additions & 1 deletion docker/nginx/nginx-newui.conf
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ http {
map $request_uri $csp_header {
# `blob:` is required so authenticated avatars (fetched as
# bytes and exposed via URL.createObjectURL) can render.
default "default-src 'self' https://analytics.dfir-iris.org https://127.0.0.1 http://app:8000; script-src 'self' 'unsafe-inline' https://analytics.dfir-iris.org; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; worker-src 'self' blob:;";
# `connect-src` must be explicit — otherwise browsers fall back
# to `default-src` for XHR + WebSocket connections, and `default-src`
# doesn't cover the `ws:` / `wss:` schemes Socket.IO uses for
# the /collab namespace (real-time markdown collab in case notes,
# war-room notes, and sitreps). Without `ws: wss:` here, every
# collab editor hangs on its sync-init handshake.
default "default-src 'self' https://analytics.dfir-iris.org https://127.0.0.1 http://app:8000; connect-src 'self' https://analytics.dfir-iris.org https://127.0.0.1 http://app:8000 ws: wss:; script-src 'self' 'unsafe-inline' https://analytics.dfir-iris.org; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; worker-src 'self' blob:;";
}
include /etc/nginx/mime.types;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Merge dashboard/collab merge head and war_room_teams head

The upstream sync merge brought in war_room_teams_and_chat_attachments
(f7a3b9c1d02e), based on d5e6f7a8b9c0, on a sibling branch to the
dashboard/collab merge head (b5c6d7e8f9a0), re-forking the alembic
head into two.

Revision ID: 0508647a855c
Revises: b5c6d7e8f9a0, f7a3b9c1d02e
Create Date: 2026-07-13 00:00:00.000000

"""
from alembic import op

Check failure on line 13 in source/app/alembic/versions/0508647a855c_merge_dashboard_collab_and_war_room_heads.py

View workflow job for this annotation

GitHub Actions / Static analyis checks

ruff (unused-import)

source/app/alembic/versions/0508647a855c_merge_dashboard_collab_and_war_room_heads.py:13:21: unused-import: `alembic.op` imported but unused help: Remove unused import: `alembic.op`

Check failure on line 13 in source/app/alembic/versions/0508647a855c_merge_dashboard_collab_and_war_room_heads.py

View workflow job for this annotation

GitHub Actions / Static analyis checks

ruff (unused-import)

source/app/alembic/versions/0508647a855c_merge_dashboard_collab_and_war_room_heads.py:13:21: unused-import: `alembic.op` imported but unused help: Remove unused import: `alembic.op`
import sqlalchemy as sa

Check failure on line 14 in source/app/alembic/versions/0508647a855c_merge_dashboard_collab_and_war_room_heads.py

View workflow job for this annotation

GitHub Actions / Static analyis checks

ruff (unused-import)

source/app/alembic/versions/0508647a855c_merge_dashboard_collab_and_war_room_heads.py:14:22: unused-import: `sqlalchemy` imported but unused help: Remove unused import: `sqlalchemy`

Check failure on line 14 in source/app/alembic/versions/0508647a855c_merge_dashboard_collab_and_war_room_heads.py

View workflow job for this annotation

GitHub Actions / Static analyis checks

ruff (unused-import)

source/app/alembic/versions/0508647a855c_merge_dashboard_collab_and_war_room_heads.py:14:22: unused-import: `sqlalchemy` imported but unused help: Remove unused import: `sqlalchemy`

# revision identifiers, used by Alembic.
revision = '0508647a855c'
down_revision = ('b5c6d7e8f9a0', 'f7a3b9c1d02e')
branch_labels = None
depends_on = None


def upgrade():
pass


def downgrade():
pass
98 changes: 98 additions & 0 deletions source/app/alembic/versions/c3d4e5f6a9b0_add_war_room_topics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Add war_room_topic table + topic_id column on war_room_chat_message.

Topics are top-level partitions of the chat stream — each war room
has one non-archivable "Main" topic (created lazily by the business
layer on first read) plus any number of operator-created ones. A
message with `topic_id IS NULL` is on Main; the read path treats
NULL and the Main topic row as equivalent so pre-migration rows keep
working without a backfill.

Archive is soft (`archived_at`). No hard delete — archived topics
still render (read-only) in the sidebar.

Idempotent via `_has_table` / `_table_has_column` so re-running on
environments that already applied the migration is a no-op.

Revision ID: c3d4e5f6a9b0
Revises: f3c8d2a1b47e
Create Date: 2026-07-09 10:00:00.000000
"""
import sqlalchemy as sa
from alembic import op

from app.alembic.alembic_utils import _has_table, _table_has_column


revision = 'c3d4e5f6a9b0'
down_revision = 'f3c8d2a1b47e'
branch_labels = None
depends_on = None


def upgrade():
if not _has_table('war_room_topic'):
op.create_table(
'war_room_topic',
sa.Column('topic_id', sa.BigInteger(), primary_key=True),
sa.Column(
'war_room_id', sa.BigInteger(),
sa.ForeignKey('war_room.war_room_id', ondelete='CASCADE'),
nullable=False,
),
sa.Column('name', sa.String(length=80), nullable=False),
sa.Column(
'is_main', sa.Boolean(), nullable=False,
server_default=sa.text('false'),
),
sa.Column(
'created_by_id', sa.BigInteger(),
sa.ForeignKey('user.id'), nullable=True,
),
sa.Column(
'created_at', sa.DateTime(), nullable=False,
server_default=sa.text('now()'),
),
sa.Column('archived_at', sa.DateTime(), nullable=True),
sa.UniqueConstraint('war_room_id', 'name',
name='uq_war_room_topic_name'),
)
op.create_index(
'ix_war_room_topic_war_room_id',
'war_room_topic',
['war_room_id'],
)

if _has_table('war_room_chat_message'):
if not _table_has_column('war_room_chat_message', 'topic_id'):
op.add_column(
'war_room_chat_message',
sa.Column(
'topic_id', sa.BigInteger(),
sa.ForeignKey(
'war_room_topic.topic_id', ondelete='SET NULL'
),
nullable=True,
),
)
op.create_index(
'ix_war_room_chat_message_topic_id',
'war_room_chat_message',
['topic_id'],
)


def downgrade():
if _has_table('war_room_chat_message'):
if _table_has_column('war_room_chat_message', 'topic_id'):
op.drop_index(
'ix_war_room_chat_message_topic_id',
table_name='war_room_chat_message',
)
op.drop_column('war_room_chat_message', 'topic_id')

if _has_table('war_room_topic'):
op.drop_index(
'ix_war_room_topic_war_room_id',
table_name='war_room_topic',
)
op.drop_table('war_room_topic')
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Add parent_task_id + search indexes on war_room_task.

Subtasks are single-level: a war-room task may point at another
war-room task in the same room via `parent_task_id`. Enforcement of
the "no grand-children" rule lives in the business layer (a task
that is itself a child cannot be given children) so the DB stays
simple. `ON DELETE CASCADE` on the FK: removing a parent removes its
subtasks, matching the UX expectation ("delete a task, delete its
subtasks").

Also adds three helper indexes used by the new search/filter list:
- `parent_task_id` for expand-children lookups
- `status_id` for status-filter scans
- `war_room_id, parent_task_id` composite to cheaply pull the
top-level tree on the tasks page (parent_task_id IS NULL).

Idempotent via `_has_table` / `_table_has_column` so re-running on
environments that already applied the migration is a no-op.

Revision ID: d5e6f7a8b9c0
Revises: c3d4e5f6a9b0
Create Date: 2026-07-13 10:00:00.000000
"""
import sqlalchemy as sa
from alembic import op

from app.alembic.alembic_utils import _has_table, _table_has_column


revision = 'd5e6f7a8b9c0'
down_revision = 'c3d4e5f6a9b0'
branch_labels = None
depends_on = None


def upgrade():
if not _has_table('war_room_task'):
return

if not _table_has_column('war_room_task', 'parent_task_id'):
op.add_column(
'war_room_task',
sa.Column(
'parent_task_id', sa.BigInteger(),
sa.ForeignKey('war_room_task.task_id', ondelete='CASCADE'),
nullable=True,
),
)
op.create_index(
'ix_war_room_task_parent_task_id',
'war_room_task',
['parent_task_id'],
)

op.execute(
'CREATE INDEX IF NOT EXISTS ix_war_room_task_status_id '
'ON war_room_task (status_id)'
)
op.execute(
'CREATE INDEX IF NOT EXISTS ix_war_room_task_war_room_parent '
'ON war_room_task (war_room_id, parent_task_id)'
)


def downgrade():
if not _has_table('war_room_task'):
return

op.execute('DROP INDEX IF EXISTS ix_war_room_task_war_room_parent')
op.execute('DROP INDEX IF EXISTS ix_war_room_task_status_id')

if _table_has_column('war_room_task', 'parent_task_id'):
op.execute('DROP INDEX IF EXISTS ix_war_room_task_parent_task_id')
op.drop_column('war_room_task', 'parent_task_id')
Loading
Loading