package pocketbase import ( "github.com/pocketbase/pocketbase/core" "git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations" "git.vakhrushev.me/av/transcriber/internal/entity" ) // BindPanelRules подчиняет правку записи в панели тем же правилам, что и правку // из кода. // // Панель — вход в запись наравне с конвейером, а не окно просмотра: ради правки // она и покупалась, остановленная запись возвращается в работу снятием признака. // Но правка полем идёт мимо кода, который чистит служебные поля, и владелец, // «вернувший запись в работу», получил бы запись с прежним признаком захвата // (захвату она не выдастся до конца срока), с числом отказов на пределе // (остановится от первого же отказа) и со старым временем входа в рубеж // (остановится снова первым же захватом по пределу простоя). Узнать об этом ему // неоткуда. // // Правило живёт **одним местом** — доменными `Resume` и `MoveToState`, — и хук // зовёт именно их, а не повторяет перечень служебных полей колонками. Повтор // перечня был бы вторым домом того же правила: новый сторож попал бы в домен и // не попал в панель, и владелец «вернул бы запись в работу», а она снова выпала // бы из выборки — молча. // // Хук стоит на правке **запросом**, а не на всяком сохранении записи. Модельное // событие не различает, кто пишет, и срабатывало бы на каждом переходе // конвейера: тогда пауза, поставленная шагом вместе со сменой рубежа, стиралась // бы тем же сохранением, а число отказов остановленной записи — которое // остановка хранит намеренно — приходило бы владельцу нулём. func BindPanelRules(app core.App) { app.OnRecordUpdateRequest(migrations.RecordsCollection).BindFunc(func(e *core.RecordRequestEvent) error { original := e.Record.Original() if original == nil { return e.Next() } stateChanged := original.GetString("state") != e.Record.GetString("state") // Снятие признака остановки — то самое движение, ради которого признак и // заведён: запись возвращается в работу с сохранённого рубежа. resumed := !original.GetDateTime("halted_at").IsZero() && e.Record.GetDateTime("halted_at").IsZero() if !stateChanged && !resumed { return e.Next() } // Запись читается уже с правкой человека: рубеж здесь тот, который он // выбрал, а признак остановки — тот, который он снял или оставил. record := recordToAudioRecord(e.Record) switch { case resumed: record.Resume() default: record.MoveToState(record.State) } applyOwnedByPipeline(e.Record, record) if err := e.Next(); err != nil { return err } if resumed { // Перезапуск виден в журнале событий с указанием, что его сделал // человек: иначе запись, вернувшаяся в работу, выглядела бы как // запись, которая туда и не уходила. // // Строка пишется **после** сохранения: событие о правке, которая не // прошла, соврало бы о состоянии записи. Отказ записи журнала саму // правку не отменяет — журнал никем не читается ради решения. event := &entity.RecordEvent{ RecordID: e.Record.Id, Origin: entity.EventOriginHuman, Step: "resume", Outcome: entity.EventOutcomeResumed, } if err := NewRecordEventRepository(e.App).Append(event); err != nil { e.App.Logger().Error("Failed to log record resume", "error", err, "record_id", e.Record.Id) } } return nil }) }