Skip to content

Commit 2d7228f

Browse files
Copilotrnwood
andauthored
feat: implement streaming session logs with real-time updates (#1903)
* Initial plan * feat: implement streaming session logs with real-time updates Co-authored-by: rnwood <1327895+rnwood@users.noreply.github.com> * fix: handle null session warnings in Vue component Co-authored-by: rnwood <1327895+rnwood@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rnwood <1327895+rnwood@users.noreply.github.com>
1 parent 6e9f165 commit 2d7228f

5 files changed

Lines changed: 89 additions & 5 deletions

File tree

Rnwood.Smtp4dev/ClientApp/src/components/sessionlist.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,9 @@
225225
this.connection.on("sessionschanged", () => {
226226
this.refresh(true);
227227
});
228+
this.connection.on("sessionupdated", (sessionId: string) => {
229+
this.refresh(true);
230+
});
228231
this.connection.addOnConnectedCallback(() => this.refresh(true));
229232
}
230233
}

Rnwood.Smtp4dev/ClientApp/src/components/sessionview.vue

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
type="warning"
1212
show-icon
1313
title="This session terminated abnormally">{{session.error}}</el-alert>
14-
<el-alert v-for="warning in session.warnings"
14+
<el-alert v-for="warning in session?.warnings || []"
1515
v-bind:key="warning.details"
1616
:title="'Warning: ' + warning.details"
1717
type="warning"
@@ -41,12 +41,13 @@
4141
</div>
4242
</template>
4343
<script lang="ts">
44-
import { Component, Vue, Prop, Watch, toNative } from "vue-facing-decorator";
44+
import { Component, Vue, Prop, Watch, toNative, Inject } from "vue-facing-decorator";
4545
4646
import SessionsController from "../ApiClient/SessionsController";
4747
import SessionSummary from "../ApiClient/SessionSummary";
4848
import Session from "../ApiClient/Session";
4949
import TextView from "@/components/textview.vue";
50+
import HubConnectionManager from "../HubConnectionManager";
5051
5152
@Component({
5253
components: {
@@ -57,18 +58,25 @@
5758
5859
@Prop({})
5960
sessionSummary: SessionSummary | null = null;
61+
62+
@Inject({ default: null })
63+
connection!: HubConnectionManager | null;
64+
6065
session: Session | null = null;
6166
log: string | null = null;
6267
6368
error: Error | null = null;
6469
loading = false;
70+
71+
private pollInterval: number | null = null;
6572
6673
@Watch("sessionSummary")
6774
async onMessageChanged(
6875
value: SessionSummary | null,
6976
oldValue: SessionSummary | null
7077
) {
7178
await this.loadSession();
79+
this.setupPolling();
7280
}
7381
7482
async loadMessage() {
@@ -104,10 +112,72 @@
104112
this.loading = false;
105113
}
106114
}
115+
116+
async refreshLog() {
117+
if (this.sessionSummary != null && !this.sessionSummary.endDate) {
118+
try {
119+
const newSession = await new SessionsController().getSession(
120+
this.sessionSummary.id
121+
);
122+
const newLog = await new SessionsController().getSessionLog(
123+
this.sessionSummary.id
124+
);
125+
126+
// Only update if content has changed
127+
if (this.log !== newLog) {
128+
this.log = newLog;
129+
}
130+
131+
// Update session properties if they've changed
132+
if (JSON.stringify(this.session) !== JSON.stringify(newSession)) {
133+
this.session = newSession;
134+
}
135+
136+
// If session has ended, stop polling
137+
if (newSession) {
138+
// Session ended, need to update the summary as well
139+
this.stopPolling();
140+
}
141+
} catch (e: any) {
142+
// Silently ignore errors during polling
143+
console.warn('Failed to refresh session log:', e);
144+
}
145+
}
146+
}
147+
148+
setupPolling() {
149+
this.stopPolling();
150+
151+
// Only poll if session is active (no end date)
152+
if (this.sessionSummary && !this.sessionSummary.endDate) {
153+
this.pollInterval = window.setInterval(() => {
154+
this.refreshLog();
155+
}, 1000); // Poll every second
156+
}
157+
}
158+
159+
stopPolling() {
160+
if (this.pollInterval !== null) {
161+
clearInterval(this.pollInterval);
162+
this.pollInterval = null;
163+
}
164+
}
165+
166+
onSessionUpdated(sessionId: string) {
167+
if (this.sessionSummary && this.sessionSummary.id === sessionId) {
168+
this.refreshLog();
169+
}
170+
}
107171
108-
async created() { }
172+
async created() {
173+
if (this.connection) {
174+
this.connection.on("sessionupdated", this.onSessionUpdated.bind(this));
175+
}
176+
}
109177
110-
async destroyed() { }
178+
async unmounted() {
179+
this.stopPolling();
180+
}
111181
}
112182
113183

Rnwood.Smtp4dev/Controllers/SessionsController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public SessionsController(Smtp4devDbContext dbContext, ISmtp4devServer server)
3434
[HttpGet]
3535
public PagedResult<SessionSummary> GetSummaries(int page = 1, int pageSize = 5)
3636
{
37-
return dbContext.Sessions.AsNoTracking().Where(s => s.EndDate.HasValue).OrderByDescending(x => x.StartDate)
37+
return dbContext.Sessions.AsNoTracking().OrderByDescending(x => x.StartDate)
3838
.Select(m => new SessionSummary(m)).GetPaged(page, pageSize);
3939
}
4040

Rnwood.Smtp4dev/Hubs/NotificationsHub.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ public async Task OnSessionsChanged()
5050
}
5151
}
5252

53+
public async Task OnSessionUpdated(Guid sessionId)
54+
{
55+
if (Clients != null)
56+
{
57+
await Clients.All.SendAsync("sessionupdated", sessionId);
58+
}
59+
}
60+
5361
public async Task OnMailboxesChanged()
5462
{
5563
if (Clients != null)

Rnwood.Smtp4dev/Server/Smtp4devServer.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,8 @@ await taskQueue.QueueTask(() =>
416416
dbContext.SaveChanges();
417417

418418
activeSessionsToDbId[e.Session] = dbSession.Id;
419+
420+
notificationsHub.OnSessionUpdated(dbSession.Id).Wait();
419421
}, false).ConfigureAwait(false);
420422
}
421423

@@ -440,6 +442,7 @@ await taskQueue.QueueTask(() =>
440442

441443
activeSessionsToDbId.Remove(e.Session);
442444

445+
notificationsHub.OnSessionUpdated(dbSession.Id).Wait();
443446
notificationsHub.OnSessionsChanged().Wait();
444447
}, false).ConfigureAwait(false);
445448
}

0 commit comments

Comments
 (0)