Tayori.Digest (Tayori v0.1.0)

Copy Markdown View Source

An entry made of the inbox's changes (Tayori.Inbox): the changes waiting, for the public, put in an entry for the people who use the app.

{:ok, entry} = Tayori.Digest.run()
{:ok, entry} = Tayori.Digest.run(ids: ["0193…", "0194…"])

The entry is a draft for the admin to read and publish, unless the :digest config says publish: :publish; the changes it takes are then "drafted", with the entry's id. A change for the team only (audience: "internal"), or put aside, never goes into one.

Written by the model, or plain

When the admin has set up AI (Tayori.AI.configured?/0), the model writes the entry from the changes: a title, a summary and a body, in the first language and the product's voice, the highlights first. It gets each change's title, body, category and highlight, never its ref, link or author, and never a change for the team. With digest: [translate: true], it also translates the entry into the other languages. When the model fails, the digest is plain, and a warning is logged.

A plain digest, without AI or with ai: false: one change makes an entry of its own, its title and its body; several make an entry titled with the day, with a section for each category, the highlighted first.

The entry's categories and segments are always the changes', not the model's.

When

The :every of the :digest config: :manual, when the admin asks; :ingest, each time changes come (after_ingest/1); or a schedule, which Tayori's scheduler keeps (next_run/3). A scheduled digest with fewer changes than :minimum makes nothing, and its changes wait for the next time. With the :oban option, it runs in a job of Tayori.Worker; without, in a task. status/0 says where it stands.

Summary

Types

A schedule: the :every and :at of the :digest or :emails config.

Functions

Tells the digest that changes came: with every: :ingest, a digest runs when at least one is new. Tayori.Inbox.ingest/0 calls it, and so does whatever else adds changes in batches, with the count of new ones.

Whether the scheduled digest of the config is due at a time: its next_run/3 after the last one has come.

The time of the next scheduled run, after the last one (or after now, when there was none), or nil when the schedule has no time

Makes an entry of the changes waiting. Options

Where the digest stands, for the admin: its :every, the time of the next scheduled one (nil for :manual and :ingest), the last scheduled one (nil before the first), and how many changes wait.

Types

schedule()

@type schedule() :: %{:every => term(), :at => Time.t(), optional(atom()) => term()}

A schedule: the :every and :at of the :digest or :emails config.

Functions

after_ingest(added)

@spec after_ingest(non_neg_integer()) :: :ok

Tells the digest that changes came: with every: :ingest, a digest runs when at least one is new. Tayori.Inbox.ingest/0 calls it, and so does whatever else adds changes in batches, with the count of new ones.

due?(config \\ Config.get(), now \\ DateTime.utc_now())

@spec due?(Tayori.Config.t(), DateTime.t()) :: boolean()

Whether the scheduled digest of the config is due at a time: its next_run/3 after the last one has come.

next_run(map, last_run_at, now)

@spec next_run(schedule(), DateTime.t() | nil, DateTime.t()) :: DateTime.t() | nil

The time of the next scheduled run, after the last one (or after now, when there was none), or nil when the schedule has no time:

  • {:weekly, day} - the first such day after the last run, at :at;
  • :monthly - the first day of the month after the last run, at :at;
  • {:days, n} - n days after the day of the last run, at :at;
  • :manual, :ingest (and the :each of :emails) - nil.

A time that is past is due: a run missed while the app was down is made once, at the next look.

Examples

iex> schedule = %{every: {:weekly, :monday}, at: ~T[09:00:00]}
iex> Tayori.Digest.next_run(schedule, ~U[2026-10-19 09:00:30Z], ~U[2026-10-20 12:00:00Z])
~U[2026-10-26 09:00:00Z]

iex> schedule = %{every: :monthly, at: ~T[09:00:00]}
iex> Tayori.Digest.next_run(schedule, nil, ~U[2026-10-20 12:00:00Z])
~U[2026-11-01 09:00:00Z]

iex> Tayori.Digest.next_run(%{every: :manual, at: ~T[09:00:00]}, nil, ~U[2026-10-20 12:00:00Z])
nil

run(opts \\ [])

@spec run(keyword()) ::
  {:ok, Tayori.Entry.t()} | {:error, :nothing | Ecto.Changeset.t()}

Makes an entry of the changes waiting. Options:

  • :ids - these changes only (still only the new, public ones).
  • :since, :until - the changes that happened in this time.
  • :publish - :draft or :publish, instead of the config's.
  • :ai - false for a plain digest, even with AI set up.
  • :now - the time of the digest (now).
  • :author - the entry's author, as the resolver's author/1 gives it.

Returns {:ok, entry}, {:error, :nothing} when no change is waiting, or {:error, changeset}.

status()

@spec status() :: %{
  every: term(),
  next_run_at: DateTime.t() | nil,
  last_run_at: DateTime.t() | nil,
  pending: non_neg_integer()
}

Where the digest stands, for the admin: its :every, the time of the next scheduled one (nil for :manual and :ingest), the last scheduled one (nil before the first), and how many changes wait.