Team access
Invite chatters, traffers, managers or trial viewers into your panel with per-tab checkboxes, a read-only mode and hidden money (percent-only view).
Give every teammate their own login — and let them see only what you tick. Grants are enforced server-side on every request (tab guard, read-only gate, money masking), so hiding a tab in the UI is never the only fence.
Job titles — a reusable set of rights
Команда → Должности is your own catalogue: create a job title, tick what it may do, hand it to people. It is a live link, not a copy — edit the title and every holder changes on their next request; nobody's card has to be reopened.
- Nothing changes until you use it. A teammate with no job title keeps their own checkboxes, exactly as before. Existing grants are untouched.
- While a title is worn, the person's checkboxes are read-only — the truth about their rights lives in one place. Press Открепить and the current rights are copied onto the person (nothing changes at that instant); from then on you edit them by hand again.
- A title carries rights, not rank. The rank (executor / manager) and everything personal — assigned accounts, source scope, shifts, devices, KPI link — stay on the person.
- Archive, don't break. An archived title is no longer offered when granting
access but keeps working for everyone who already wears it; a title someone
wears cannot be deleted at all (
409). Silently stripping someone's access is never a side effect of tidying up. - Built for dozens. The picker searches by name, so thirty titles stay a list, not a wall. Chatter / traffer / manager / trial remain starting points when you create access — not the full list of what a job can be.
Who may create them
The workspace owner always. A manager hands out ready titles as part of
managing access, but may create or edit them only with the
team → roles_manage checkbox («Создавать и менять должности»), which is
off by default — including for older grants that carry the full legacy
function set. A new switch never hands a live manager new power on the day it
ships. A manager can neither mint nor hand out a title wider than their own
grant.
Roles are presets, checkboxes are the truth
| Preset | Tabs | Can edit | Money | Extra scope |
|---|---|---|---|---|
| Chatter | Chat, Content, Team | yes | shown (own KPIs + PPV prices) | sees only their own KPIs/shifts; no team sums |
| Traffer | Dashboard, Smart-Link, Tracking, Trials, Scaner | yes | shown | only their source domains |
| Manager | everything except system settings | yes | shown | rank 1 — manages executors' access |
| Trial (demo) | all tabs | no | hidden — blur + percent shares | — |
| Custom | start empty, tick what you need | your call | your call | optional |
Every preset is just a starting point — any tab, the edit switch and the money switch can be flipped per member.
Chatter sees money — inside their own tabs
A chatter must see PPV prices and their own sales, so the chatter preset
keeps money on. What's hidden is the team leaderboard in sums: drop the
team → leaderboard_money function and other operators' revenue is nulled
(they keep places and percent shares). The old global money-off toggle stays
for the traffer/trial scenarios.
Ranks (3 levels)
Every grant carries a rank: 0 executor (chatter/traffer) or 1 manager.
The workspace owner sits above both.
- A manager can open the Team tab and manage the access of executors only (rank 0) — never other managers, never themselves — and can never grant wider than their own grant (tabs, functions, money mode and source scope are all capped to the manager's own).
- The action log (Аудит) is rank-scoped: an executor sees only their own actions, a manager sees themselves + their executors, the owner sees everyone.
Functions inside a tab
Beyond view and edit, each tab exposes function checkboxes — the deep control («что именно может делать»). Unticking one makes the server refuse the action (a 403), not just hide the button:
| Tab | Functions |
|---|---|
| Chat | write messages · send PPV · attach vault content · use scripts · manage scripts · see fan spend · broadcasts |
| Content | content analytics (revenue) · edit tags · run vault scans |
| Team | manage roster & shifts · leaderboard in sums · manage access (manager) · assign accounts · create & edit job titles |
| Stream | watch the live feed · reply in chat · platform chat settings · auto-messages · connect platform accounts · on-video caption · fan-out to platforms · cockpit (live & recording) · stream key · broadcast title & category · browser-camera live |
An older grant with no explicit function matrix keeps the full set of its tabs' functions — nothing changes until you open «Функции» and narrow it. The one exception is a function the owner declared off by default (today: create & edit job titles): it is never implied by the legacy full set and has to be ticked, so shipping a new switch cannot widen a live grant behind your back.
Platform tools are not a tenant right
Some screens in the panel are ours, not yours: publishing the changelog every
tenant sees, the service status page, the ticket desk holding other customers'
requests, the idea board moderation, the feature registry, incoming bug reports.
They live on their own surface (Платформа) under their own API prefix
(/api/admin/platform/*) and are refused to every tenant — a creator, a manager,
a job title, an API key — by prefix, before any checkbox is consulted. The
refusal is its own code (platform_only), not "tab not granted".
That is deliberate: a manager may be given every right in the workspace and still cannot reach them, and a route added under that prefix tomorrow is closed today. Поддержка stays fully client-side — write a ticket, read your own, public changelog and status, suggest an idea, report a problem. The owner sees exactly the same screen a customer does.
Help is a door, not a right
Поддержка is not a checkbox. It is a service door of the panel: everyone
who can log in sees it, including a member with a single tab and including
read-only access. Nothing behind that screen is workspace data — tickets are
per-person (/api/me/support/tickets), the changelog and status page are
public, a bug report is the panel's own service call. Gating it would have meant
a workspace owner deciding whether their staff may talk to us, or see that
the service is down; that is not the owner's call to make.
So the key is absent from the tab dictionary and the grid never offers it. If a
grant does try to hand it out, the answer says so in words (service_tab) rather
than "unknown tab". Two consequences worth knowing:
- a member's ticket is theirs, not the workspace's — staff never see the owner's correspondence with us, and the owner never sees theirs;
- a member is created with a login and password and no email, so a reply reaches them as an in-panel notification. A creator gets the email too.
A tab the server does not know is refused, not swallowed
Tab keys and function keys are one dictionary shared by the panel and the
server. If the two ever disagree, saving a grant returns a 422 naming the
unknown key instead of quietly dropping it — a silent drop once cost the whole
Stream surface (the checkbox rendered, the key vanished on save, nobody was
told). A build test now fails whenever a sidebar tab has no dictionary entry.
Every admin route is checked — that is now structural
The grant used to be enforced as a side effect of opening a database session,
so a route that needed no database was not checked at all. Not "checked less" —
not checked. The check is its own thing now, and a build test walks the live
app: every /api/admin route either passes the fence or sits in a short,
reasoned list of system callbacks (the engine posting a new message, the
stream producer, the panel's error beacon, and logout — which must work
precisely for the person the fence would refuse).
A refusal says what happened, in words
Every refusal carries both a machine code and a human sentence:
{ "detail": { "code": "team_read_only",
"message": "У вас доступ без редактирования этой вкладки." } }Read code in your integration and show message to the person — it is written
for them, not for logs. Codes you will meet: team_tab_denied (tab not granted),
team_read_only (granted for viewing only), team_cap_denied (the function
inside the tab is off), team_scope_denied (that domain/account is not yours),
team_access_paused, team_2fa_required, platform_only (our operator tool —
see above). A build test keeps the pairing honest: a refusal that ships a code
without words fails the suite.
View-only is visible before you click
A teammate whose tab is granted without editing no longer discovers it by
pressing Save and getting a 403. The panel reads the same right the fence uses
(whoami().team.edit_tabs), shows one honest strip at the top of such a tab, and
disables its write controls with the same sentence the server would have
returned. Personal actions (sign out, your own account) are never disabled —
they are not workspace edits. The enforcement itself stays on the server: the
panel only shows the answer early, it never grants anything by staying silent.
Assigned accounts — who works with which model
Tabs decide which screens a teammate opens; Аккаунты decides whose data
they see there. In the access dialog, tick a model (the whole creator,
including pages added later) or an individual page. Назначить все /
Снять are one click.
- Nothing ticked = no confinement — the teammate works with the entire workspace, exactly as before this setting existed. Existing grants are untouched.
- The choice cuts money, inbox, dashboard and exports alike — it resolves in the same place every screen reads its data scope, so an export can never show a different slice than the dashboard beside it.
- A page of another workspace is refused outright, and an id that no longer exists grants nothing (never everything).
- A manager hands out only from their own set, and a confined manager cannot create an unconfined teammate.
A manager hands out access
Управлять доступами (a function under Команда) lets a manager open Команда → Доступы в панель and run the roster of executors themselves — add a login, edit rights, pause or revoke. Both halves of the condition are required: the manager rank and the ticked function. Neither alone opens the screen, and that is exactly the condition the server enforces.
- A manager sees executors and themselves — never another manager. The list is cut on the server, not in the panel, and a manager's own row is read-only there: nobody edits their own rights.
- A manager never hands out more than they hold — tabs, editing, functions, the money mode, the domain scope, the assigned accounts and the data axis are all checked against their own grant, and the refusal names what was too wide.
- The manager rank itself is the owner's to give. A manager creates executors only; the rank switch says so instead of failing at save time, and the Менеджер preset lands as an executor in their hands.
- Managing the roster is a separate function. «Управлять составом команды» governs the KPI roster (shifts, leaderboard, payouts); without it those controls are visibly disabled while access management keeps working.
API access is a manager's right
API (issuing a key and using MCP) is not handed out below the manager rank. An API key outlives the person, never asks for a second factor and knows nothing about shifts — so it belongs one rank up.
- The checkbox is locked with Только менеджеру for an executor.
- The rule is enforced when the grant is read, not only in the form: a row saved before this rule (or edited straight in the database) loses the tab anyway.
- Demoting someone to executor removes the tab and says so — keeping a checkbox the server ignores would be a lie in the card.
- A job title may carry the API tab: titles have no rank, so the rank is checked on the person. The same title works for a manager and does nothing for an executor.
Exporting the report
Скачивать отчёт и делиться ссылкой (a function under Дашборд) lets a teammate download the channel report and mint a public share link. It is a toggle: it is not part of a tab's default set and has to be ticked by hand — the file leaves the panel for good, and a share link is public.
- A grant that was never given an explicit function matrix does not get it — so nobody gains the export on the day this ships.
- The report is always built for the page the teammate is already looking at; a request cannot aim it at another account.
- If money is hidden for that teammate, the Финансы option is refused — a money ban that a Download button could step around would not be a ban.
Whose data — a separate axis from how money looks
Чьи данные видит decides whose rows reach a teammate; Деньги decides how the sums are shown (real / blurred / hidden). They are independent on purpose — a manager may need real numbers for a narrow slice, and a trial account the opposite.
| Уровень | Что видно |
|---|---|
| Только свои модели | доход и отчёты строго по назначенным моделям. Ничего не назначено — человек видит весь воркспейс, ровно как раньше. Это значение по умолчанию. |
| Свои и подчинённых | своё плюс назначения людей рангом ниже. Для исполнителя это то же самое, что «только свои» — под ним никого нет. |
| Все данные воркспейса | весь доход, даже если отдельные модели назначены. |
- Hierarchy comes from the rank, not the job title. A title is a set of rights and carries no order; the rank does, by construction. So «подчинённые» means active grants of this workspace one rank below.
- A paused teammate widens nobody — someone who is not working does not extend their manager's view.
- If a subordinate has no confinement at all, their manager sees everything: narrowing the manager to a subset would be a lie about «свои + подчинённые».
- A manager can never hand out a wider view than their own — otherwise a confined manager would create a helper with «все данные» and read the whole workspace through them.
- The level resolves in the same place every screen reads its data scope, so an export can never show a different slice than the dashboard beside it.
Domain scope guards domains, not words
Источники narrows a teammate to specific hosts. What that guards is precise:
a request that addresses a domain — in the address (/qr/angeldfit.com,
/buttons/angeldfit.com/T1) or as a filter (?domain=) — is checked against
their set, and a host outside it is refused with «Этот домен не входит в ваш
доступ».
- A fixed word in an address is never a domain.
/qr/presets,/domains/connect,/domains/suggestare handle names, not hosts. The check reads the route the router matched, so it can tell a name apart from a host by construction — no list of exceptions to keep up to date, and a handle added tomorrow behaves correctly on its first day. - Position does not matter. A host is checked wherever it sits in the address, not only in the first slot after the section.
- Connecting a new domain is not confined — the scope guards access to your domains, not the right to name a new one. A host the workspace already owns answers «домен уже зарегистрирован» and nothing is changed. Note the consequence: a domain a confined teammate connects will not appear in their own list until you add that host to their Источники.
- Addressing by id does not slip through. Where a handle takes a row id instead of a host (verifying a connected domain), a domain outside the scope answers not found — the same answer as an id that does not exist, so the reply itself never reveals what the workspace owns.
A shift switches the role, it does not lock the door
A member has two roles, not one: on shift and off shift. The shift picks which one is in force right now — it is not a gate in front of the panel.
Tick Работает по смене in the access dialog and four settings follow:
- Вне смены действует — the job title that applies outside the shift. Leave it empty and the member sees only the Смена screen: that is the strict setup — the same behaviour as a hard lock, minus the broken panel. Pick a job title and the member keeps working with fewer rights ("watch only", "answer chat but do not touch prices"). An off-shift title never grants more than the on-shift grant: the two are intersected.
- Что считается сменой — по графику (a planned roster shift, +15 min grace) and/or отметился сам (pressed «Начать смену»). Both count by default: a button that says «Вы на смене» and changes nothing would be a lie.
- Выход на смену вне графика — нельзя / сам / с подтверждением менеджера. A self-service exit needs a reason, gets an automatic deadline and lands in the journal; with manager approval it waits in Просят выйти вне графика on the Команда tab, and «Открыть смену» starts it.
- Turning the switch on requires a roster link — shifts are counted per roster identity. Without one the save is refused in words instead of silently doing nothing.
The same switch also lives on a job title, so an agency sets the rule once for everybody who wears it. A value on the person wins over the title.
Off shift the panel is not a wall of refusals: the member sees their off-shift role (with a strip saying when the shift starts), or the honest Смена screen. Their API keys follow the same rule — a key never holds more than the person who issued it, and off shift that person holds less.
Security: mobile and devices
Two more per-member switches under Безопасность:
- Запретить чат с телефона — sending a chat message from a mobile browser is refused (viewing still works).
- Только одобренные устройства — the first sign-in from a new device is frozen as pending and you get an owner alert; approve or revoke it from the member's access dialog. A locked-out member sees an honest full-screen notice, never a broken panel.
Hidden money = percent-only
With money off, the backend nulls every money field before the response
leaves the server and adds *_pct share mirrors in lists; CSV exports are
denied outright. The member sees structure and percentages, never sums.
Add a teammate
Команда → Добавить в команду → Доступ в панель. Pick a preset.
Set the name, a login (a-z 0-9 . _ -) and a password (the generator is
one click). The password is shown once after creation — copy it there.
Tick the tabs, flip Can edit / Show money, optionally link a chatter KPI profile or list the traffer's source domains. Done — the teammate signs in on the same panel login page — login and password only; teammates do not sign in through Telegram.
Pausing a member locks them out on their next request; revoking also frees the login and kills every session immediately.
API
Team management is a first-class API (same auth as the rest of the admin
API — session cookie or an al_live_* bearer key with write scope):
GET /api/admin/team/members # list grants
POST /api/admin/team/members # create login + grant
PATCH /api/admin/team/members/{id} # tabs/functions/rank/flags/password
DELETE /api/admin/team/members/{id} # revoke (frees the login)
GET /api/admin/team/assignable-accounts # models + pages you may hand out
GET /api/admin/team/roles # catalogue (?q= search, ?archived=1)
POST /api/admin/team/roles # create a job title
PATCH /api/admin/team/roles/{id} # rename / re-tick / archive
DELETE /api/admin/team/roles/{id} # 409 while anyone wears it
GET /api/admin/team/devices # list devices (?member_id=)
PATCH /api/admin/team/devices/{id} # approve / revoke a device
GET /api/admin/shift/state # where I am, when my shift starts
POST /api/admin/shift/start # go on shift (or ask to, off schedule)
POST /api/admin/shift/stop # close my shift
GET /api/admin/shift/requests # off-schedule requests (?status=)
POST /api/admin/shift/requests/{id} # approve / deny one
POST /api/admin/chatters/clock-in # start a worked shift (hours only)
POST /api/admin/chatters/clock-out # close it
GET /api/admin/user-actions # rank-scoped action log
POST /api/admin/reports # report a problem (any member, even read-only)/api/admin/platform/* is not part of this API — it is the platform's own
operator surface (see above) and answers 403 platform_only to every tenant
credential.
Create body (example — a chatter, rank 0, chat write-but-no-PPV, shift-gated):
{
"label": "Аня (ночь)",
"username": "anya.night",
"password": "generated-pass",
"preset": "chatter",
"tabs": ["inbox", "content", "team"],
"edit_tabs": ["inbox", "content", "team"],
"money_mode": "show",
"level": 0,
"caps": {
"inbox": ["send", "attach_vault", "scripts_use", "view_fan_spend"],
"content": ["view_analytics"],
"team": []
},
"shift_gate": true,
"mobile_chat_allowed": false,
"device_gate": true,
"operator_id": 42,
"creators": ["c_dasha"],
"accounts": []
}caps is the function matrix (null/omitted = the full set of each granted
tab). level is the rank (only the owner may set 1). Tab keys mirror the
sidebar routes; the server rejects unknown keys and drops unknown functions.
creators / accounts are the assigned-accounts axis: creator ids and page
owner_ids. Both empty (or omitted) means no confinement. On PATCH,
sending [] clears the assignment; omitting the field leaves it alone. An id
outside the workspace is a 422 that names it — nothing is dropped silently.
role_id hands the member a job title instead of their own checkboxes: send
tabs/edit_tabs empty and leave caps out — sending rights and a title is
a 422 (role_locked), because a person cannot have two sources of truth about
their rights. (money_mode is refused only when it contradicts the title —
echoing the same value back is fine.) The member row then reports the title's
effective rights plus role_id/role_name. On PATCH, clear_role: true
detaches and copies the current rights onto the person, so nothing changes at
that moment.
Managers manage executors, never siblings
/team/members is owner-or-manager: an executor gets 403, and a manager can
only see and edit rank-0 members within their own grant's limits. A grant can
never widen beyond the workspace that issued it, and its X-Account-Owner
header is ignored.