StackLogHr (stack_log_hr колекція)
#draft Модель: StackLogHrModel.ts.
Універсальний audit-log усіх подій навколо HR-хайрингу: створення/редагування оператора, зміна статусу картки (StackStatusCard), назначення/зняття HR ↔ Operator, HR ↔ Supervisor, HR ↔ Recruiter, Recruiter ↔ Operator.
Поля
| Поле | Тип | Обов’язкове (schema) | Призначення |
|---|---|---|---|
id | string | ні | mongo _id (transform plugin) |
timestamp | number | ні (default: Date.now) | коли сталась подія |
initiatorStackId | string | ні | хто ініціював дію (актор — HR/Recruiter/Director/…). Часто відсутній для системних подій (referral, telegram-bot, besocial.studio form) |
hrStackId | string | ні (хоч тип у IStackLogHr не позначений ?) | HR, з яким пов’язана дія. Подвійна семантика — див. нижче |
recruiterStackId | string? | ні | Recruiter, з яким пов’язана дія |
role | StackRoles | ні | фактично ніде не заповнюється при .create() (перевірено grep-ом по всій кодовій базі) — не покладатись |
userStackId | string | так | суб’єкт логу: юзер (оператор/supervisor/recruiter), якого стосується подія |
event | StackHrLogEventType | StackStatusCard | так | подвійне призначення — див. нижче |
family | Families? | ні | заведене в схемі, але жоден .create() виклик його не встановлює — мертве поле |
source | CardSource? | ні | звідки прийшов оператор, ставиться лише разом з create_operator |
event: два незалежні “словники” в одному полі
StackHrLogEventTypeCombined = StackStatusCard | StackHrLogEventType — тобто в одному стовпці мішаються:
- Структурні переходи (
StackHrLogEventType):set_stack_operator/unset_stack_operator,set_stack_supervisor/unset_stack_supervisor,set_stack_recruiter/unset_stack_recruiter,create_operator,edit_operator. - Бізнес-статуси картки (
StackStatusCard):New,In progress,Approved,Hired,Company declined,Applicant declined,No response,Duplicate, …
Немає discriminator-поля — кожен reader змушений або .includes() по одному з enum-ів, або switch з подвійним as-кастом (log.event as StackStatusCard | StackHrLogEventType — так і зроблено в JobseekerService.convertLogs).
Де пишеться (StackLogHrModel.create)
- JobseekerService.ts
manualCreateOperator—create_operator(+Newякщо немає hr)createOperatorBesocialStudioForm—New+create_operator(source: besocialStudioForm)referralCreateOperator—create_operator(source: referral)telegramBot2CreateCardForCandidateOperator—create_operator(source: telegramBot)manualEditOperator—edit_operatorsaveLogAndUpdateStatusStack(викликається зacceptOperatorByInitiator/changeStatusStack) — будь-якийStackStatusCard, опційноhrStackId/recruiterStackIdякщо оператор має hr/recruiter
- StackHrRepository.ts
setStackHrToOperator/unsetStackHrFromOperator—set_stack_operator/unset_stack_operatorзhrStackId(+ каскаднийIn progress/Newлог)setStackHrToSupervisor/unsetStackHrFromSupervisor—set_stack_supervisor/unset_stack_supervisorзhrStackId(супервайзор технічно “прив’язується до HR”, хоч у домені він не підпорядкований HR напряму)
- recruiter.service.ts
setHrToRecruiter—unset_stack_recruiter(старий hr) потімset_stack_recruiter(новий hr), обидва зhrStackIdsetRecruiterToOperator—set_stack_operatorзrecruiterStackId(безhrStackId); якщо в оператора ще немає HR — попередньо каскадно викликаєstackHrRepository.setStackHrToOperator(окремий лог ізhrStackId)unsetRecruiterFromOperator—unset_stack_operatorзrecruiterStackId(безhrStackId)
Де читається
StackHrRepository.getHrAllSupervisors/findLogsByStackHrId— фільтр поhrStackIdStackHrService.getHrRecruiterPeriods→calculateTimestampPeriodsForRecruiters— будує періоди hr↔recruiter зset_stack_recruiter/unset_stack_recruiterStackStatisticsService(isUserOnHrLogs,isUserUnsetFromHrLogs,isOperatorHaveChangeHrByOperatorLogs,usersOnHrByTimeByLogs,getHrOperatorStatistics) — основний споживач для щоденної статистики відвідуваності HR; трактує послідовністьset_stack_operator/unset_stack_operator(поhrStackId) як таймлайн періодів “оператор був на HR”RecruiterGenerateDashboardService(recruiter-generate-dashboard.service.ts) — dashboard/decision-logs, групує поuserStackIdJobseekerService.getCard— шукаєoperatorLogAccepted(Hired/Approved) для визначення, хто найняв (hrStackId/recruiterStackIdна цьому логу);convertLogs(WIP) формує людський список подій картки
Нюанси / gotchas
eventбез discriminator — легко пропустити кейс при додаванні нового статусу/типу, бо перевірка робиться вручну в кожному місці (switch,.includes()).roleфактично мертве поле — типізовано як обов’язкове (role: StackRoles, без?), але жоден.create()його не заповнює. Будь-яка майбутня логіка, що читаєlog.role, отримаєundefined.- Неузгоджена опційність у TS-типі:
hrStackId: string(обов’язкове за типом) vsrecruiterStackId?: string(опційне), хоча в mongoose-схемі обидва неrequired, і на практиці обидва часто відсутні одночасно (напр.create_operatorбез hr). - Асиметрія HR/Recruiter-шляхів для однієї логічної дії “оператора призначили”: шлях через
StackHrRepository.setStackHrToOperatorпишеhrStackId, шлях черезrecruiter.service.setRecruiterToOperatorпише лишеrecruiterStackId(і лиш каскадно створює окремий hr-лог, якщо hr ще не було). Статистика (getHrOperatorStatistics) фільтрує виключно поhrStackId— отже коректність спирається на те, що каскад завжди відпрацює при першому призначенні. Це неявний контракт між двома сервісами. hrStackIdдля supervisor-подій (set_stack_supervisor/unset_stack_supervisor) семантично означає “HR, під яким супервайзор”, а не “HR виконав дію” — те саме поле перевикористовується з різним змістом залежно відevent.familyне заповнюється ніде — попри наявність у схемі й інтерфейсі, тому “family-scoped” статистика (StackStatisticsService) змушена окремо резолвити family черезfindOperatorFamiliesBy...замість прямого фільтру по логах.- Один лог = максимум один hr і один recruiter. Немає місця для довільної кількості “сторін” події (напр. одночасно старий+новий hr, або team-lead окремо від hr).
Ідеї для більшої гнучкості hrStackId / recruiterStackId
Наведені нижче — варіанти рефакторингу, не застосовані (потребують окремого рішення, бо чіпають кілька сервісів і читальні запити):
-
Generic “relations” map замість окремих колонок:
relations?: Partial<Record<'hr' | 'recruiter' | 'supervisor', string>>;Додавання нового типу зв’язку (напр.
teamlead,clientManager) не вимагає нової колонки/індексу — просто новий ключ. Запити переписуються наrelations.hrзамістьhrStackId. -
Масив “learties” (
counterparts) — розв’язує проблему “1 hr + 1 recruiter максимум” і прибирає асиметрію (одна подіяset_stack_operatorмогла б одразу нести і hr, і recruiter, без каскадного другого запису):counterparts: { role: StackRoles; userStackId: string; } [];Дає змогу писати один лог на дію замість двох (HR-каскад + Recruiter-лог), і статистика фільтрує по
counterpartsчерез$elemMatch. -
Discriminator для
event:kind: 'transition' | 'status'; event: StackHrLogEventType | StackStatusCard;Прибирає потребу вручну перевіряти належність значення до одного з двох enum-ів у кожному читальному місці.
-
Зробити
roleзначущим або прибрати. Якщо задум був “роль ініціатора” — перейменувати наinitiatorRoleі почати заповнювати; якщо не потрібне — видалити з інтерфейсу/схеми, щоб не вводити в оману. -
Заповнювати
familyпри записі (благо поле вже є) — дає змогу фільтрувати статистику по family напряму в MongoDB-запиті, без додаткового join черезuserRepository.findOperatorFamiliesBy.... -
Явний контракт для “cascade” HR-логу — задокументувати/винести в один спільний хелпер (
ensureHrLogForOperator), яким користуються іStackHrRepository, іRecruiterService, щоб асиметрія з п.4 не залежала від того, що обидва сервіси “пам’ятають” викликати одне одного.
Будь-яка з цих змін — це міграція існуючих записів + узгоджена зміна усіх read-сайтів вище. Раджу впроваджувати поступово (напр. спочатку discriminator для
event, він найдешевший і найменш ризикований), а не одним великим рефакторингом.
ТЗ: Логи в картці оператора / кандидата (Audit Log)
Формалізовані бізнес-правила для читабельного відображення історії змін картки (ICardResult.logs, формується в JobseekerService.convertLogs).
1. Формат лога
[Date Time] — [User Name] ([Role]) — [Event]
або
[Date Time] — System — [Event]
User Name— користувач, що виконав дію.Role— Recruiter, HR, TeamLead, CEO.System— використовувати лише якщо дія відбулась повністю автоматично, без участі користувача.- Якщо дію ініціював користувач — навіть якщо саму зміну виконала система автоматично — в лозі відображається саме цей користувач (не
System).
2. Події
2.1 Створення оператора (Operator creation)
Operator created (Manual)Operator created (Telegram Bot)Operator created (Referral)Operator created (Website Form)
2.2 Поля картки (Card fields)
При зміні будь-якого поля картки показувати назву поля, старе і нове значення:
{Field Name} updated: {Old Value} → {New Value}
Приклади: First name updated: Ivan → John, Last name updated: Petrenko → Smith, Email updated: old@email.com → new@email.com, Phone updated: +34600000000 → +34611111111, Telegram updated: @old → @new, Country updated: Ukraine → Spain, Language updated: English → English, Spanish.
- Якщо за одне збереження змінено кілька полів — кожна зміна пишеться окремим записом в історії.
- Лог створюється тільки при фактичній зміні даних (немає діффа → немає логу).
2.3 Призначення (Assignments)
HR:
HR assigned: {Name} (HR)HR changed: {Old Name} (HR) → {New Name} (HR)HR unassigned: {Name} (HR)
Recruiter:
Recruiter assigned: {Name} (Recruiter)Recruiter changed: {Old Name} (Recruiter) → {New Name} (Recruiter)Recruiter unassigned: {Name} (Recruiter)
2.4 Family
Family assigned: {New Family}Family TeamLead assigned: {New Name} (TeamLead)Family TeamLead changed: {Old Name} (TeamLead) → {New Name} (TeamLead)Family TeamLead unassigned: {New Name} (TeamLead)Family changed: {Old Family} → {New Family}Family TeamLead changed: {Old Name} (TeamLead) → {New Name} (TeamLead)Family changed: {Old Family} → {New Family}
Правило парних lifecycle-логів: послідовність delete_family → create_family або
delete_family → recovery_delete_family означає переміщення й відображається одним записом
Family changed: {Old Family} → {New Family}. Обидва технічні записи окремо в картці не
дублюються. Кожна lifecycle-подія, яка не входить у таку пару, відображається окремо.
2.5 Статус (Status)
Status changed: {Old Status} → {New Status}
Значення: New, In Progress, Approved, Hired, Applicant Declined, Company Declined, No Response, Duplicate.
2.6 Доступ (Access)
BlockedUnblocked
3. Приклади логів
15 Jul 2026 14:20 — System — Operator created (Telegram Bot)
15 Jul 2026 14:23 — Alina (Recruiter) — Operator created (Manual)
15 Jul 2026 14:25 — Alina (Recruiter) — First name updated: Ivan → John
15 Jul 2026 14:26 — Alina (Recruiter) — Email updated: old@email.com → new@email.com
15 Jul 2026 14:31 — Alina (Recruiter) — Recruiter assigned: Alina (Recruiter)
15 Jul 2026 14:35 — Diana (HR) — HR assigned: Diana (HR)
15 Jul 2026 14:40 — Stanislav (CEO) — TeamLead assigned: Kateryna KitKat (TeamLead)
15 Jul 2026 14:42 — Stanislav (CEO) — Recruiter changed: Alina (Recruiter) → Kate (Recruiter)
15 Jul 2026 14:45 — Stanislav (CEO) — HR changed: Diana (HR) → Mike (HR)
15 Jul 2026 14:47 — Stanislav (CEO) — Status changed: In Progress → Approved
15 Jul 2026 14:49 — Stanislav (CEO) — Status changed: Approved → Hired
15 Jul 2026 14:52 — Alina (Recruiter) — Family changed: Silver Family → Golden Family
15 Jul 2026 14:54 — Alina (Recruiter) — Family TeamLead changed: Kateryna KitKat (TeamLead) → Olena (TeamLead)
15 Jul 2026 14:56 — Diana (HR) — Blocked
4. Правила
- Кожен лог містить дату й час, користувача (або
System), роль користувача і опис події. Systemвикористовувати лише для повністю автоматичних дій.- Якщо дію ініціював користувач — завжди відображати цього користувача (навіть якщо виконання автоматичне).
- Для змін полів картки, Family, статусів і призначень відображати старе і нове значення, якщо старе значення існувало.
- Для всіх призначень відображати користувача, якого призначили, зняли або замінили.
- Якщо змінено кілька полів картки за раз — для кожного поля створювати окремий запис в історії.
- Не створювати лог при відкритті картки, перегляді картки або збереженні без фактичних змін.
Примітка: це ТЗ підготував менеджер продукту. Бізнес-логіка та послідовність подій тут коректні, і більшу частину має реалізувати розробник (
convertLogs/IStackLogResult). Але конкретні назви (значенняsource— Manual/Telegram Bot/Referral/Website Form, назви статусів — New/In Progress/Approved/Hired/… тощо) можуть не збігатися 1:1 з реальними enum-ами системи (CardSource,StackStatusCard,StackHrLogEventType) — при імплементації брати канонічні значення з@it-monkeys/stack-commonsіStackLogHrModel.ts, а не з тексту ТЗ дослівно. #Entity
StackLogHr
src/stack/model/StackLogHrModel.ts — колекція stack_log_hr: аудит-лог кадрових подій оператора (створення картки, зміни статусу, призначення/зняття HR, Recruiter-а, тімліда).
Для менеджера: які події фіксуємо і як показуємо логи
Які дії фіксуємо:
- Створення анкети оператора/кандидата — вручну, по реферальному посиланню, через Telegram-бота або через форму на сайті besocial.studio.
- Редагування картки оператора.
- Призначення та зняття HR, Recruiter-а, тімліда.
- Переведення Recruiter-а від одного HR до іншого.
- Будь-яку зміну статусу картки:
New,In progress,Approved,Hired,Applicant declined,Company declined,No response,Duplicate.
Як зараз виглядають логи:
Логи показуються в картці оператора як історія дій, українською мовою, у хронологічному порядку. Для більшості подій видно лише саму дію (“Створення оператора”, “Назначення оператора на HR” тощо, без деталей). Для двох ключових статусів — Approved і Hired — додатково показується, хто прийняв рішення: Recruiter, HR чи директор. Ім’я конкретної людини, яка ініціювала дію, на екрані зараз не показується.
Приклади повідомлень — вже реалізовано
Так виглядає текст логу в картці оператора зараз (беремо реальний текст з коду, JobseekerService.convertLogs):
| Що сталось | Що бачить менеджер |
|---|---|
| Створив реферал (система) | “Створення оператора Ref системою” |
| Створив Telegram-бот | ”Створення оператора Telegram ботом” |
| Заповнив форму на besocial.studio | ”Створення оператора: besocial.studio/form” |
| Оператора вручну створив Recruiter | ”Створення оператора Recruiter-ом” |
| Оператора вручну створив HR | ”Створення оператора HR-ом” |
| Оператора створили без HR/Recruiter-а | ”Створення оператора” |
| Відредагували картку | ”Редагування оператора” |
| Призначили оператора на HR | ”Назначення оператора на HR” |
| Призначили оператора на Recruiter-а | ”Назначення оператора на Recruiter-а” |
| Зняли оператора з HR | ”Зняття оператора з HR” |
| Зняли оператора з Recruiter-а | ”Зняття оператора з Recruiter-а” |
| Призначили тімліда | ”Назначення тімліда на HR” |
| Зняли тімліда | ”Зняття тімліда з HR” |
| HR погодив картку | ”Approved HR-ом” |
| Recruiter найняв оператора | ”Hired Recruiter-ом” |
| Директор погодив/найняв напряму (без HR і Recruiter-а) | “Approved директором” / “Hired директором” |
Статус змінили на New / In progress / Applicant declined / Company declined / No response / Duplicate | показується без перекладу, так як записано в базі: “New” / “In progress” / “Applicant declined” / “Company declined” / “No response” / “Duplicate” |
Нюанс: для статусів “Approved” і “Hired” слово залишається англійською, а виконавець дописується українською (“Approved HR-ом”) — переклад лише частковий, це не помилка документації, а поточна поведінка коду.
Пропозиції щодо покращення відображення логів
Ідеї для доопрацювання, ще не реалізовано. Нижче — як виглядає зараз і як могло б виглядати.
- Логи блокування/розблокування оператора. Зараз таких рядків в історії немає взагалі. Могло б бути: “Заблоковано директором” / “Розблоковано HR-ом”, а для сімейних подій — “Заблоковано (сім’я Chathouse)” / “Розблоковано (сім’я Golden)“.
- Ім’я, Нікнейм і Роль ініціатора лога. Зараз: “Назначення оператора на HR” (без імені того, хто призначив). Могло б бути: “Назначення оператора на HR — Олена Коваль (нікнейм: elena_k, роль: HR)” — за наявності
initiatorStackId, аналогічно тому, як вже підтягується автор до коментарів уgetCard. - Логи зміни family. Зараз таких рядків в історії немає. Могло б бути: “Створено сім’ю Golden”, “Заблоковано сім’ю Chathouse”, “Переведено з Prime на Udate” — на основі поля
family, яке вже є у схемі, але зараз ніде не заповнюється.
Поля
- timestamp — час події (мс), за замовчуванням момент запису.
- initiatorStackId — хто ініціював дію (директор / HR / recruiter). Відсутнє для системних подій (телеграм-бот, реферальне створення, besocial.studio/form).
- hrStackId — HR, повʼязаний з подією: або кому призначили/у кого зняли оператора чи тімліда, або HR оператора на момент запису статусу картки.
- recruiterStackId — Recruiter, повʼязаний з подією (аналогічно
hrStackId, але для Recruiter-а). - userStackId — оператор/кандидат/recruiter/тімлід, якого стосується подія (обовʼязкове).
- event — тип події. Значення беруться з двох різних enum одразу в одне поле:
StackHrLogEventType(службові дії) абоStackStatusCard(статус картки, пишеться напряму як рядок, без окремої обгортки). - source — джерело створення оператора (
CardSource), заповнюється лише дляcreate_operator. - family — присутнє у схемі, але жоден з записів у сервісі stack його не заповнює.
Типи подій (event)
Службові (StackHrLogEventType)
| Подія | Коли пишеться | Де в коді |
|---|---|---|
create_operator | При будь-якому створенні оператора/кандидата (ручне, реферал, telegram-бот, besocial.studio/form) | JobseekerService.manualCreateOperator / createOperatorBesocialStudioForm / referralCreateOperator / telegramBot2CreateCardForCandidateOperator |
edit_operator | При кожному ручному редагуванні картки оператора | JobseekerService.manualEditOperator |
set_stack_operator | Призначення оператора на HR (hrStackId) або на Recruiter-а (recruiterStackId) — те саме значення event, розрізняється лише заповненим полем | StackHrRepository.setStackHrToOperator (HR) / recruiter.service.setRecruiterToOperator (Recruiter) |
unset_stack_operator | Зняття оператора з HR або з Recruiter-а (аналогічно, за тим самим принципом) | StackHrRepository.unsetStackHrFromOperator (HR) / recruiter.service.unsetRecruiterFromOperator (Recruiter) |
set_stack_supervisor / unset_stack_supervisor | Призначення/зняття тімліда (Supervisor) з HR | StackHrRepository.setStackHrToSupervisor / unsetStackHrFromSupervisor |
set_stack_recruiter / unset_stack_recruiter | Переназначення Recruiter-а на іншого HR — пишуться обидві події одним викликом (спочатку unset, потім set) | recruiter.service.setHrToRecruiter |
Статуси картки (StackStatusCard, пишуться напряму як event)
Будь-яка зміна статусу картки (New, In progress, Approved, Hired, Applicant declined, Company declined, No response, Duplicate) записується як окремий лог з event, рівним значенню статусу:
- Центральна точка запису —
JobseekerService.saveLogAndUpdateStatusStack(викликається зchangeStatusStackіacceptOperatorByInitiator). manualCreateOperatorіcreateOperatorBesocialStudioFormпишутьNewнапряму одразу при створенні оператора без HR.StackHrRepository.setStackHrToOperator/unsetStackHrFromOperatorпишуть побічний лог зміни статусу (New→In progressпри призначенні HR і навпаки при знятті) — це автоматичний ефект призначення/зняття HR, а не окрема дія користувача.
Відображення (JobseekerService.convertLogs)
Логи оператора повертаються в картці (getCard → ICardResult.logs) як історія дій. Перед видачею фронту convertLogs перетворює технічне значення event на людський текст українською:
- Для
create_operatorтекст залежить відsource(реферал / telegram-бот / besocial.studio/form) або, якщо джерело не системне, від того, чи заповненіhrStackId/recruiterStackId(створено HR-ом / Recruiter-ом / просто “Створення оператора”). - Для
set_stack_operator/unset_stack_operatorтекст залежить від того, яке поле заповнене —recruiterStackId(дія на Recruiter-і) чиhrStackId(дія на HR). - Для статусів
ApprovedіHiredдо тексту статусу додається виконавець: “Recruiter-ом”, “HR-ом” або “директором” (за наявністюhrStackId/recruiterStackId). - Статуси
In progress,New,Applicant declined,Company declinedта будь-яка інша подія повертаються без перекладу (as is) —defaultуswitchтеж повертає лог без змін.
Де використовується (тільки сервіс stack)
| Місце | Що робить з логами |
|---|---|
JobseekerService.getCard | Тягне всі логи оператора (find({ userStackId })), прогонює через convertLogs, повертає як ICardResult.logs — історія картки на фронті |
JobseekerService.getCard / hiringInManualEditOperator / cooperationInManualEditOperator | Шукають лог Hired/Approved, щоб визначити, чи оператора вже наймали — після цього HR/Recruiter не можна поміняти напряму |
StackStatisticsService | Вибирає set_stack_operator/unset_stack_operator по hrStackId за період — рахує всіх операторів, які коли-небудь були на HR |
StackHrService.getHrRecruiterPeriods | Вибирає set_stack_recruiter/unset_stack_recruiter — рахує часові періоди співпраці Recruiter-а з конкретним HR |
StackHrService (celebrations) | Шукає лог Hired по операторам — дата логу = дата прийняття, звідти рахується річниця співпраці для нагадувань |
RecruiterGenerateDashboardService | Вибирає широкий діапазон подій (усі StackStatusCard + create_operator + set_stack_operator + set_stack_recruiter) за день, групує по оператору, рахує “останній вирішальний лог” для щоденної статистики |
StackHrRepository.getHrAllSupervisors | Вибирає set_stack_supervisor/unset_stack_supervisor — знаходить усіх тімлідів, які колись були призначені HR (навіть уже знятих) |
StackHrRepository.findLogsByStackHrId | Тягне логи конкретного оператора під конкретним HR до заданої дати (сирі дані, без трансформації) |
StackSupervisorService2 | Бере всі логи по hrStackId, для кожного тімліда шукає останній лог до заданої дати — визначає, чи був він активний тімлідом на конкретний момент |
UserRepository.findOperatorCardsForRecruiter / findCardsByString / customizeRecruiters | Шукають Hired/Approved логи по recruiterStackId — показують картки, що колись пройшли через recruiter-а (навіть якщо recruiterId картки вже змінився), і рахують заблокованих/активних |
HiringWorkspaceService | При повному видаленні hire з системи видаляє всі його логи (deleteMany({ userStackId })) разом з іншими повʼязаними колекціями |
Нюанси
eventв одному й тому ж полі може бути якStackHrLogEventType, так іStackStatusCard— при читанні завжди потрібенswitch/includesпо обох enum одразу (як уconvertLogs).set_stack_operator/unset_stack_operator— одна й та сама подія для двох різних дій (HR і Recruiter); розрізняти можна лише по тому, яке з полівhrStackId/recruiterStackIdзаповнене.- Призначення/зняття HR через
StackHrRepositoryавтоматично тригерить ще один лог зміни статусу картки (New⇄In progress) — це побічний ефект, не окрема дія користувача. - Колекція не має TTL — записи не видаляються, окрім явного видалення при повному видаленні hire (
HiringWorkspaceService).