feat: enhance diarization skipped job logging with meeting creator ID and improve dev server robustness with execvp error handling.
This commit is contained in:
@@ -121,16 +121,43 @@ pub fn get_effective_server_url(state: State<'_, Arc<AppState>>) -> EffectiveSer
|
||||
|
||||
// Otherwise, use config (which tracks env vs default)
|
||||
let default_url = cfg.server.default_address.clone();
|
||||
let (h, p) = if let Some(pos) = default_url.find(':') {
|
||||
(&default_url[..pos], &default_url[pos + 1..])
|
||||
} else {
|
||||
(&default_url[..], "")
|
||||
};
|
||||
let (host, port) = parse_host_port(&default_url);
|
||||
|
||||
EffectiveServerUrl {
|
||||
url: default_url.clone(),
|
||||
host: h.to_string(),
|
||||
port: p.to_string(),
|
||||
host,
|
||||
port,
|
||||
source: cfg.server.address_source,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_host_port(address: &str) -> (String, String) {
|
||||
let trimmed = address.trim();
|
||||
if trimmed.is_empty() {
|
||||
return (String::new(), String::new());
|
||||
}
|
||||
|
||||
if let Some(rest) = trimmed.strip_prefix('[') {
|
||||
if let Some(end) = rest.find(']') {
|
||||
let host = &rest[..end];
|
||||
let port = rest[end + 1..].strip_prefix(':').unwrap_or("");
|
||||
return (host.to_string(), sanitize_port(port));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((host, port)) = trimmed.rsplit_once(':') {
|
||||
if !host.is_empty() && !port.is_empty() && !host.contains(':') {
|
||||
return (host.to_string(), sanitize_port(port));
|
||||
}
|
||||
}
|
||||
|
||||
(trimmed.to_string(), String::new())
|
||||
}
|
||||
|
||||
fn sanitize_port(port: &str) -> String {
|
||||
if port.parse::<u16>().is_ok() {
|
||||
port.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,23 +14,23 @@ vi.mock('@/hooks/use-toast', () => ({
|
||||
}));
|
||||
|
||||
describe('TranscriptSegmentActions', () => {
|
||||
it('renders copy button and handles click', () => {
|
||||
it('renders copy button and handles click', async () => {
|
||||
render(<TranscriptSegmentActions segmentText="Hello world" />);
|
||||
|
||||
|
||||
const copyButton = screen.getByTitle('Copy text');
|
||||
expect(copyButton).toBeDefined();
|
||||
|
||||
fireEvent.click(copyButton);
|
||||
|
||||
await fireEvent.click(copyButton);
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('Hello world');
|
||||
});
|
||||
|
||||
it('renders play button when onPlay is provided', () => {
|
||||
const onPlay = vi.fn();
|
||||
render(<TranscriptSegmentActions segmentText="Hello world" onPlay={onPlay} />);
|
||||
|
||||
|
||||
const playButton = screen.getByTitle('Play audio');
|
||||
expect(playButton).toBeDefined();
|
||||
|
||||
|
||||
fireEvent.click(playButton);
|
||||
expect(onPlay).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -9,6 +9,10 @@ export interface TranscriptSegmentActionsProps {
|
||||
|
||||
export function TranscriptSegmentActions({ segmentText, onPlay }: TranscriptSegmentActionsProps) {
|
||||
const handleCopy = async () => {
|
||||
if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') {
|
||||
toast({ description: 'Clipboard not supported', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(segmentText);
|
||||
toast({ description: 'Text copied to clipboard' });
|
||||
|
||||
@@ -76,11 +76,22 @@ function normalizeSizes(prefs: PanelPreferences): PanelPreferences {
|
||||
const visibleSidePanels = (showNotesPanel ? 1 : 0) + (showStatsPanel ? 1 : 0);
|
||||
|
||||
if (visibleSidePanels === SIDE_PANEL_COUNT) {
|
||||
const sideSpace = (remaining - SIDE_PANEL_COUNT * COLLAPSED_SIZE_PERCENT) / SIDE_PANEL_COUNT;
|
||||
const totalMinSide = MIN_NOTES_SIZE_PERCENT + MIN_STATS_SIZE_PERCENT;
|
||||
let newNotesSize = MIN_NOTES_SIZE_PERCENT;
|
||||
let newStatsSize = MIN_STATS_SIZE_PERCENT;
|
||||
|
||||
if (remaining > totalMinSide) {
|
||||
const excess = remaining - totalMinSide;
|
||||
const totalPref = clampedNotesSize + clampedStatsSize;
|
||||
const notesRatio = totalPref > 0 ? clampedNotesSize / totalPref : 0.5;
|
||||
newNotesSize += excess * notesRatio;
|
||||
newStatsSize += excess * (1 - notesRatio);
|
||||
}
|
||||
|
||||
return {
|
||||
...prefs,
|
||||
notesPanelSize: Math.max(sideSpace, MIN_NOTES_SIZE_PERCENT),
|
||||
statsPanelSize: Math.max(sideSpace, MIN_STATS_SIZE_PERCENT),
|
||||
notesPanelSize: clamp(newNotesSize, MIN_NOTES_SIZE_PERCENT, MAX_NOTES_SIZE_PERCENT),
|
||||
statsPanelSize: clamp(newStatsSize, MIN_STATS_SIZE_PERCENT, MAX_STATS_SIZE_PERCENT),
|
||||
transcriptPanelSize: transcriptSize,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ export default function RecordingPage() {
|
||||
const handleJumpToLive = useCallback(() => {
|
||||
const scrollElement = transcriptScrollRef.current;
|
||||
if (scrollElement) {
|
||||
scrollElement.scrollTo({ top: scrollElement.scrollHeight, behavior: 'smooth' });
|
||||
scrollElement.scrollTo({ top: scrollElement.scrollHeight, behavior: 'auto' });
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"include": [
|
||||
"client/src",
|
||||
"src/",
|
||||
"clinet/src-tauri"
|
||||
"client/src-tauri"
|
||||
|
||||
],
|
||||
"ignore": {
|
||||
@@ -110,4 +110,4 @@
|
||||
"tokenCount": {
|
||||
"encoding": "o200k_base"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,11 @@ def run_server() -> None:
|
||||
nested subprocess spawning that causes zombie processes when watchfiles
|
||||
restarts on file changes.
|
||||
"""
|
||||
os.execvp(sys.executable, [sys.executable, "-m", "noteflow.grpc.server"])
|
||||
try:
|
||||
os.execvp(sys.executable, [sys.executable, "-m", "noteflow.grpc.server"])
|
||||
except OSError as exc:
|
||||
print(f"Failed to exec gRPC server: {exc}", file=sys.stderr)
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
@@ -30,6 +30,21 @@ def _set_diarization_span_attributes(span: Span, ctx: DiarizationJobContext) ->
|
||||
span.set_attribute("diarization.num_speakers", ctx.num_speakers)
|
||||
|
||||
|
||||
async def _load_meeting_creator_id(ctx: DiarizationJobContext) -> str | None:
|
||||
"""Best-effort lookup of meeting creator for audit context."""
|
||||
from ..converters import parse_meeting_id_or_none
|
||||
|
||||
meeting_id = parse_meeting_id_or_none(ctx.meeting_id)
|
||||
if meeting_id is None:
|
||||
return None
|
||||
|
||||
async with ctx.host.create_repository_provider() as repo:
|
||||
meeting = await repo.meetings.get(meeting_id)
|
||||
if meeting is None or meeting.created_by_id is None:
|
||||
return None
|
||||
return str(meeting.created_by_id)
|
||||
|
||||
|
||||
async def _run_diarization_refinement(
|
||||
ctx: DiarizationJobContext,
|
||||
span: Span,
|
||||
@@ -79,19 +94,24 @@ async def _execute_diarization_with_span(
|
||||
span.set_attribute("diarization.cancelled", True)
|
||||
await ctx.host.handle_job_cancelled(ctx.job_id, ctx.job, ctx.meeting_id)
|
||||
raise # Re-raise to propagate cancellation
|
||||
except NoDiarizationAudioError as exc:
|
||||
except NoDiarizationAudioError:
|
||||
# Expected for simulated transcripts or meetings without audio capture.
|
||||
# Mark as completed with 0 updates rather than failed.
|
||||
skip_reason = "no_audio"
|
||||
span.set_attribute("diarization.skipped", True)
|
||||
span.set_attribute("diarization.skip_reason", str(exc))
|
||||
span.set_attribute("diarization.skip_reason", skip_reason)
|
||||
created_by_id = await _load_meeting_creator_id(ctx)
|
||||
logger.info(
|
||||
"Diarization skipped for meeting %s: %s",
|
||||
ctx.meeting_id,
|
||||
exc,
|
||||
"diarization_skipped",
|
||||
job_id=ctx.job_id,
|
||||
meeting_id=ctx.meeting_id,
|
||||
reason=skip_reason,
|
||||
created_by_id=created_by_id,
|
||||
)
|
||||
# Complete the job with 0 updates - this is a valid "nothing to do" state
|
||||
await ctx.host.update_job_completed(ctx.job_id, ctx.job, 0, [])
|
||||
span.add_event("job_completed_no_audio")
|
||||
return
|
||||
# INTENTIONAL BROAD HANDLER: Job error boundary
|
||||
# - Diarization can fail in many ways (model errors, audio issues, etc.)
|
||||
# - Must capture any failure and update job status
|
||||
|
||||
Reference in New Issue
Block a user