diff --git a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala index a9572832..be76d45b 100644 --- a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala +++ b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala @@ -53,7 +53,7 @@ object BackendApp { loginImpl <- Login[F](store) signupImpl <- OSignup[F](store) joexImpl <- OJoex(JoexClient(httpClient), store) - collImpl <- OCollective[F](store, utStore, joexImpl) + collImpl <- OCollective[F](store, utStore, queue, joexImpl) sourceImpl <- OSource[F](store) tagImpl <- OTag[F](store) equipImpl <- OEquipment[F](store) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OCollective.scala b/modules/backend/src/main/scala/docspell/backend/ops/OCollective.scala index 955a4649..5e9b5aaf 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OCollective.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OCollective.scala @@ -8,12 +8,13 @@ import docspell.backend.PasswordCrypt import docspell.backend.ops.OCollective._ import docspell.common._ import docspell.store.queries.QCollective +import docspell.store.queue.JobQueue import docspell.store.records._ import docspell.store.usertask.UserTask import docspell.store.usertask.UserTaskStore import docspell.store.{AddResult, Store} -import com.github.eikek.calev.CalEvent +import com.github.eikek.calev._ trait OCollective[F[_]] { @@ -49,6 +50,7 @@ trait OCollective[F[_]] { def findEnabledSource(sourceId: Ident): F[Option[RSource]] + def startLearnClassifier(collective: Ident): F[Unit] } object OCollective { @@ -102,6 +104,7 @@ object OCollective { def apply[F[_]: Effect]( store: Store[F], uts: UserTaskStore[F], + queue: JobQueue[F], joex: OJoex[F] ): Resource[F, OCollective[F]] = Resource.pure[F, OCollective[F]](new OCollective[F] { @@ -131,6 +134,21 @@ object OCollective { _ <- joex.notifyAllNodes } yield () + def startLearnClassifier(collective: Ident): F[Unit] = + for { + id <- Ident.randomId[F] + ut <- UserTask( + id, + LearnClassifierArgs.taskName, + true, + CalEvent(WeekdayComponent.All, DateEvent.All, TimeEvent.All), + LearnClassifierArgs(collective) + ).encode.toPeriodicTask(AccountId(collective, LearnClassifierArgs.taskName)) + job <- ut.toJob + _ <- queue.insert(job) + _ <- joex.notifyAllNodes + } yield () + def findSettings(collective: Ident): F[Option[OCollective.Settings]] = store.transact(RCollective.getSettings(collective)) diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 1a20db8d..a03a0e2e 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1047,6 +1047,28 @@ paths: application/json: schema: $ref: "#/components/schemas/ContactList" + + /sec/collective/classifier/startonce: + post: + tags: [ Collective ] + summary: Starts the learn-classifier task + description: | + If the collective has classification enabled, this will submit + the task for learning a classifier from existing data. This + task is usally run periodically as determined by the + collective settings. + + The request is empty, settings are used from the collective. + security: + - authTokenHeader: [] + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + /sec/user: get: tags: [ Collective ] diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/CollectiveRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/CollectiveRoutes.scala index 2aed289f..bf7eaddd 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/CollectiveRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/CollectiveRoutes.scala @@ -88,6 +88,12 @@ object CollectiveRoutes { resp <- Ok(ContactList(res.map(Conversions.mkContact))) } yield resp + case POST -> Root / "classifier" / "startonce" => + for { + _ <- backend.collective.startLearnClassifier(user.account.collective) + resp <- Ok(BasicResult(true, "Task submitted")) + } yield resp + case GET -> Root => for { collDb <- backend.collective.find(user.account.collective) diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index 10bcf7ff..ccba8570 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -88,6 +88,7 @@ module Api exposing , setItemNotes , setTags , setUnconfirmed + , startClassifier , startOnceNotifyDueItems , startOnceScanMailbox , startReIndex @@ -795,6 +796,19 @@ versionInfo flags receive = --- Collective +startClassifier : + Flags + -> (Result Http.Error BasicResult -> msg) + -> Cmd msg +startClassifier flags receive = + Http2.authPost + { url = flags.config.baseUrl ++ "/api/v1/sec/collective/classifier/startonce" + , account = getAccount flags + , body = Http.emptyBody + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + getTagCloud : Flags -> (Result Http.Error TagCloud -> msg) -> Cmd msg getTagCloud flags receive = Http2.authGet diff --git a/modules/webapp/src/main/elm/Comp/CollectiveSettingsForm.elm b/modules/webapp/src/main/elm/Comp/CollectiveSettingsForm.elm index 87696d85..1efef12d 100644 --- a/modules/webapp/src/main/elm/Comp/CollectiveSettingsForm.elm +++ b/modules/webapp/src/main/elm/Comp/CollectiveSettingsForm.elm @@ -30,6 +30,7 @@ type alias Model = , fullTextConfirmText : String , fullTextReIndexResult : Maybe BasicResult , classifierModel : Comp.ClassifierSettingsForm.Model + , startClassifierResult : Maybe BasicResult } @@ -60,6 +61,7 @@ init flags settings = , fullTextConfirmText = "" , fullTextReIndexResult = Nothing , classifierModel = cm + , startClassifierResult = Nothing } , Cmd.map ClassifierSettingMsg cc ) @@ -91,6 +93,8 @@ type Msg | TriggerReIndexResult (Result Http.Error BasicResult) | ClassifierSettingMsg Comp.ClassifierSettingsForm.Msg | SaveSettings + | StartClassifierTask + | StartClassifierResp (Result Http.Error BasicResult) update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe CollectiveSettings ) @@ -169,12 +173,30 @@ update flags msg model = _ -> ( model, Cmd.none, Nothing ) + StartClassifierTask -> + ( model, Api.startClassifier flags StartClassifierResp, Nothing ) + + StartClassifierResp (Ok br) -> + ( { model | startClassifierResult = Just br } + , Cmd.none + , Nothing + ) + + StartClassifierResp (Err err) -> + ( { model + | startClassifierResult = + Just (BasicResult False (Util.Http.errorToString err)) + } + , Cmd.none + , Nothing + ) + view : Flags -> UiSettings -> Model -> Html Msg view flags settings model = div [ classList - [ ( "ui form", True ) + [ ( "ui form error success", True ) , ( "error", Maybe.map .success model.fullTextReIndexResult == Just False ) , ( "success", Maybe.map .success model.fullTextReIndexResult == Just True ) ] @@ -250,18 +272,7 @@ view flags settings model = [ text "This starts a task that clears the full-text index and re-indexes all your data again." , text "You must type OK before clicking the button to avoid accidental re-indexing." ] - , div - [ classList - [ ( "ui message", True ) - , ( "error", Maybe.map .success model.fullTextReIndexResult == Just False ) - , ( "success", Maybe.map .success model.fullTextReIndexResult == Just True ) - , ( "hidden invisible", model.fullTextReIndexResult == Nothing ) - ] - ] - [ Maybe.map .message model.fullTextReIndexResult - |> Maybe.withDefault "" - |> text - ] + , renderResultMessage model.fullTextReIndexResult ] , h3 [ classList @@ -279,6 +290,19 @@ view flags settings model = ] [ Html.map ClassifierSettingMsg (Comp.ClassifierSettingsForm.view model.classifierModel) + , div [ class "ui vertical segment" ] + [ button + [ classList + [ ( "ui small secondary basic button", True ) + , ( "disabled", not model.classifierModel.enabled ) + ] + , title "Starts a task to train a classifier" + , onClick StartClassifierTask + ] + [ text "Start now" + ] + , renderResultMessage model.startClassifierResult + ] ] , div [ class "ui divider" ] [] , button @@ -291,3 +315,19 @@ view flags settings model = [ text "Save" ] ] + + +renderResultMessage : Maybe BasicResult -> Html msg +renderResultMessage result = + div + [ classList + [ ( "ui message", True ) + , ( "error", Maybe.map .success result == Just False ) + , ( "success", Maybe.map .success result == Just True ) + , ( "hidden invisible", result == Nothing ) + ] + ] + [ Maybe.map .message result + |> Maybe.withDefault "" + |> text + ]