# Прочитаност на пораките за тикети

Операцијата `root.data` користи корисничко име и лозинка во секое JSON-барање, како и постојниот client/conversation envelope.

## Читање — Operation_Type 312

`data.request.Feedback_Ticket.TicketId` го избира тикетот. Секоја порака во `data.messages` има `read_at`: `null` или серверско време во формат `yyyy-MM-dd HH:mm:ss`. Читањето на деталите и листата само по себе не означува прочитаност.

## Потврда по прикажување — Operation_Type 315

```json
{
  "Operation_Type": 315,
  "Feedback_Ticket": {
    "TicketId": 123,
    "MessageIds": [456, 457]
  }
}
```

Овој објект се испраќа како `data.request` во автентицирано `root.data` барање. Потврдата се испраќа откако клиентот ќе го прикаже разговорот. `MessageIds` ги содржи само реално добиените пораки; најмногу 5000 идентификатори по барање.

Listener проверува пристап преку најавениот испраќач на тикетот или активна, двострано одобрена конекција со фирмата. За кориснички тикети `user:<id>` пристап има испраќачот. Нема доверба во клиентски user id, reader side или време.

Се ажурираат само пораките од `author_type='admin'` во избраниот тикет, со `read_at=CURRENT_TIMESTAMP` и услов `read_at IS NULL`. Сопствените кориснички пораки остануваат непроменети. Повторената потврда го задржува првото време.

Одговорот е `data.success=true` и `data.messages`, со `id` и `read_at` за потврдените административни пораки. Клиентот го прикажува статусот од серверскиот одговор.

root Manager директно ги потврдува прикажаните пораки од `author_type='user'` на истиот начин. Се користи само постојната `00_x.feedback_messages.read_at DATETIME NULL DEFAULT NULL`, без дополнителна табела. Постарите пораки почнуваат со `NULL`.

Во UI се прикажува „Непрочитано“ или „Прочитано · dd.MM.yyyy HH:mm:ss“. Кај испраќачот новата потврда се гледа при следното отворање/освежување на разговорот. Потврдата означува прикажување на разговорот, а не гаранција дека корисникот го прочитал секој збор.

## Almanac unread support replies - 20.10.2026 (historical)

The following initial read-through-cache behavior is superseded by the background-refresh contract below.

`almanac.feedback.unread` is a read-only, authenticated Almanac operation. Send
`data.companyGuid` for an active company with an approved UserOk/CompanyOk link.
The response is `data.feedback = {companyGuid, unreadReplies, checkedAtUtc, cacheSeconds:30}`.
`unreadReplies` counts admin messages with `read_at IS NULL` in all tickets of the
selected company, matching the company-scoped Support list. It counts messages,
not tickets. The current read receipt is shared per message, not per viewer.

Normal authenticated Almanac commands may opt in with `data.feedbackCompanyGuid`.
Their completed response can contain the same optional top-level `feedback` object.
The command data/operation outcome is unchanged. Unauthorized scopes get no summary.
The optional attachment waits at most 150 ms; on timeout/failure it is omitted.
Clients retain the previous known value and must not interpret omission as zero.
An explicit unread request waits for the shared bounded DB read and reports failure
normally. No notification data is attached to failed authentication or logout.

The Listener stores counts for 30 seconds in a bounded in-process cache. Access is
checked from the current Companies/UserConnections caches on every call, before
reading the count. Concurrent checks share one parameterized COUNT query using the
existing company/ticket indexes. Query timeout is 2 seconds, and failures are cached
for the TTL to avoid repeated outage queries. No schema or stored procedure changes.
A successful 315 receipt invalidates the selected company count immediately.

The web client checks on institution selection after login, on focus/visibility
return, and every 60 seconds while visible (ordinary response summaries suppress
redundant checks). A selected conversation refreshes every 20 seconds while visible.
It retains unsent form inputs and confirms only displayed admin message IDs. Timers
and listeners are disposed on navigation/logout; no automatic write retries.

### Rooty notification metadata — 20.10.2026
The existing feedback summary additionally returns `latestUnreadReplyId` and
`latestUnreadTicketId` (0 when no unread admin replies exist). These identifiers
come from the same authorized company-scoped aggregate and a primary-key lookup
in one SQL statement. The 30-second cache, access checks and receipt invalidation
are unchanged; there are no schema or stored procedure changes.

Almanac's Rooty widget uses the first received snapshot as a silent baseline.
Only a later, higher unread reply ID can trigger a brief notification animation
and an optional quiet sound. Counts changing due to read receipts do not replay
old notifications. Preferences and last-notified IDs are local; ticket bodies,
AI conversations and credentials are not persisted by the widget. Sound defaults
to off, requires a user gesture to enable, and only plays in the focused visible
tab. A motion preference and prefers-reduced-motion disable animated effects.

The floating mascot opens a compact flyout. Flyout actions open a non-modal right
panel on Ask or Support; clicking outside dismisses only the flyout. Ask reuses
the existing director-only Rooty path. Support reuses operations 311/312/315,
retains conversation state while switching tabs, checks company scope again in
the client, and confirms displayed IDs after a visible paint. Receipt errors
are not automatically retried. The selected support conversation refreshes
while its panel is visible. All application permissions remain server-enforced.

### Rooty background refresh — 20.10.2026

Овој договор го заменува претходниот read-through кеш и проверките само во видлива страница.

`almanac.feedback.unread` прифаќа `data.companyGuid` и `data.mode`:

- `"cached"` (стандардно): само меморискиот snapshot, без SQL за известувања,
  вклучително и кога кешот недостига, е стар или базата не е достапна.
  Одговорот е `{feedback:null,cacheState:"warming"}` ако нема успешен snapshot;
  тоа не значи нула непрочитани одговори. Инаку `cacheState` е `"ready"` и
  `checkedAtUtc` го покажува вистинското време на последното читање од табелите.
- `"refresh"`: експлицитно отворање Поддршка/ВИ или успешна потврда за прочитаност.
  Чека ново читање од табелите, започнато по барањето, и го обновува истиот кеш.

`feedback` останува `{companyGuid,unreadReplies,checkedAtUtc,cacheSeconds:30,
latestUnreadReplyId,latestUnreadTicketId}`. Непознат mode враќа 422.
Постојната автентикација и авторизација важат за секој повик; cached режимот
не ги прескокнува. Опционалниот top-level `feedback` кај обичните успешни
Almanac повици сега исто така се зема исклучиво од меморијата, без SQL и без
претходното чекање до 150 ms. Пропуштен snapshot никогаш не е лажна нула.

`AlmanacFeedbackRefresh` е независен hosted worker. За секој активен центар
ги чита непрочитаните административни пораки од `00_x.feedback_tickets` и
`00_x.feedback_messages` на приближно 30 секунди. На секунда проверува само
мемориски рокови; SQL има само за доспеани активни центри, со најмногу 4
паралелни читања и рок од 2 секунди по читање. Последниот успешен snapshot
се задржува при грешка, со неговиот стар `checkedAtUtc`; следен обид по 30 секунди.
Нема scheduler зависен од обичен клиентски повик, schema или procedure измени.

Пристапот се проверува во тековните Users/Companies/UserConnections кешови
при секое барање и пред позадинско читање. Се чуваат најмногу 2048 центри и
32 viewer ID/heartbeat времиња по центар, без credentials или содржина на пораки.
Автоматски повик го продолжува активниот рок; 10 минути без повици го
отстрануваат центарот од следење. Првиот cached повик само регистрира интерес;
worker-от го загрева кешот. Експлицитниот refresh го пополнува веднаш.
Паралелни читања споделуваат еден in-flight read по центар. По успешна 315
потврда се поништува snapshot-от и се закажува освежување; верзиска проверка
спречува старо читање започнато пред потврдата да го врати стариот број.

Веб-страницата испраќа cached проверка на приближно 60 секунди и кога нема
фокус/е скриена. Обични API одговори не го одложуваат овој независен тајмер.
Избор на центар, отворање Поддршка/ВИ и успешна потврда на прочитаност користат
refresh. Враќање фокус/видливост бара веднаш cached проверка. При мрежен прекин
се задржува последниот број, без автоматско повторување на write операции.
Во нормални услови комбинацијата DB 30 s + клиент 60 s може да даде до околу
90 s до известување. Прелистувачот може дополнително да забави/замрзне скриен
таб; точен интервал во замрзнат таб или затворен прелистувач не е гарантиран.

Рути го покажува бројот во менито и на роботчето. Првиот snapshot е тивка
основа; повисок нов reply ID активира кратка анимација и еден звук. Во скриен
таб визуелната анимација се задржува до враќање на видливоста, а звукот може
да се емитува и без фокус ако AudioContext веќе е дозволен. Ако е блокиран,
звучната реакција останува за следната дозволена корисничка интеракција.
Нулта непрочитаност ги поништува преостанатите реакции за веќе прочитаното.
Web Locks + зачуван last-notified ID спречуваат дупли звуци меѓу табови;
во прелистувачи без Web Locks локалната ID проверка е best effort.

Звукот е стандардно вклучен, а претходно рачно исклучување се зачувува.
Прв клик/копче на страницата го дозволува аудиото според browser autoplay
правилата. `https://almanac.mk/assets/rooty-notification.wav` е точниот
користен сигнал: 0.40 s, две меки синусни ноти 660/880 Hz, mono PCM 44.1 kHz.
Опциите за звук, анимации и prefers-reduced-motion остануваат достапни.

### Rooty cache authority repair — 20.10.2026

Овој дел го дополнува background-refresh договорот. Almanac ја чита листата
на установи директно од Hetz; нов центар затоа може да биде достапен на
страницата, а сè уште да нема реплика во локалниот Companies кеш. Претходно
ова даваше `403 institution-access` за известувањата, иако постоеше одобрена
фирмена врска и тикетот можеше да се отвори преку својот авторизиран пат.

Експлицитниот `mode:"refresh"` сега ја синхронизира недостапната фирмена
реплика преку постојниот Companies метод. Ако недостига одобрена врска во
кешот, прво ја синхронизира врската и повторно бара `UserOk=true` и
`CompanyOk=true` пред читање фирма/број. Нема прескокнување авторизација.
Обичен повик, автоматски `mode:"cached"` и позадинските проверки не добиваат
SQL fallback за синхронизација на дозволи. Самите cached проверки остануваат
чист мемориски пат; успешниот експлицитен пристап го подготвува за нив.

Клиентската иницијализација на тајмерот сега се извршува и ако првиот summary
пристигне како metadata од обичен API одговор, пред render на навигацијата.
Одбиените проверки логираат само mode/error code во конзолата, без credentials
или содржина. Непрочитан reply ID од првиот успешен snapshot повеќе не се
проголтува тивко по претходни грешки; веќе емитуван звук останува дедуплициран.
Пристапот до ВИ/тикети е преку роботчето; двете дупликат ставки се отстранети
од левото мени. Прочитаноста и schema/procedure договорите не се променети.
