package transcript import ( "fmt" "io" "os" "sync" ) // FileWriter appends each Message to a text file as a human-readable line, // flushed to disk immediately. Unlike Broadcaster.Publish (which drops // messages for a slow/stalled SSE client on purpose), FileWriter never // drops — this is the durable record a crash shouldn't lose data from, so // every Write is synchronous and fsync'd before returning. type FileWriter struct { mu sync.Mutex f *os.File } // NewFileWriter opens path for appending, creating it if needed. Appending // (not truncating) means restarting transcriptor-ai against the same path // continues the same transcript file rather than discarding it. func NewFileWriter(path string) (*FileWriter, error) { f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return nil, fmt.Errorf("open transcript file: %w", err) } return &FileWriter{f: f}, nil } // Write appends one line for msg and fsyncs before returning. func (fw *FileWriter) Write(msg Message) error { fw.mu.Lock() defer fw.mu.Unlock() line := fmt.Sprintf("[%s] [%s] %s\n", msg.Timestamp.Format("15:04:05"), msg.Track, msg.Text) if _, err := io.WriteString(fw.f, line); err != nil { return fmt.Errorf("write transcript line: %w", err) } if err := fw.f.Sync(); err != nil { return fmt.Errorf("sync transcript file: %w", err) } return nil } func (fw *FileWriter) Close() error { fw.mu.Lock() defer fw.mu.Unlock() return fw.f.Close() }