sync: latest changes before Gitea migration
This commit is contained in:
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -135,6 +139,15 @@
|
||||
|
||||
<div class="article-content creds-card">
|
||||
<h1>docs.a2v.space Basic Auth</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/creds/docs-a2v-space-auth.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/creds\/docs-a2v-space-auth.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p>Учетные данные для доступа к docs.a2v.space</p>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -135,6 +139,15 @@
|
||||
|
||||
<div class="article-content creds-card">
|
||||
<h1>Eversolo Companion PostgreSQL</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/creds/eversolo-companion-postgres.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/creds\/eversolo-companion-postgres.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p>Учетные данные для базы данных Eversolo Companion</p>
|
||||
|
||||
+93
-89
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -142,97 +146,23 @@
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card creds-card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">docs.a2v.space Basic Auth</h3>
|
||||
<h3 class="card-title">VPS reg.ru</h3>
|
||||
|
||||
<p class="card-text">Учетные данные для доступа к docs.a2v.space</p>
|
||||
<p class="card-text">VPS хостинг (реверс-прокси)</p>
|
||||
|
||||
|
||||
<p class="text-muted mb-2"><strong>Сервис:</strong> VPS</p>
|
||||
|
||||
|
||||
<div class="encrypted-data" id="encrypted-docs-a2v-space-auth">
|
||||
QmFzaWMgQXV0aGVudGljYXRpb24gVXNlcm5hbWU6IGFnYWx0c292YXYgUGFzc3dvcmQ6IE1hZ2FtZXQ3ODchINCa0L7QvdGE0LjQs9GD0YDQsNGG0LjRjyBOZ2lueCBzZXJ2ZXIgeyBsaXN0ZW4gODA5MTsgc2VydmVyX25hbWUgZG9jcy5hMnYuc3BhY2U7IGF1dGhfYmFzaWMgJiMzNDtSZXN0cmljdGVkIEFjY2VzcyYjMzQ7OyBhdXRoX2Jhc2ljX3VzZXJfZmlsZSAvZXRjL25naW54Ly5odHBhc3N3ZDsgfSDQlNC+0YHRgtGD0L8gVVJMOiBodHRwczovL2RvY3MuYTJ2LnNwYWNlICjRh9C10YDQtdC3IHJldmVyc2UgcHJveHkpIExvY2FsOiBodHRwOi8vbG9jYWxob3N0OjgwOTEgKNC00LvRjyDRgNCw0LfRgNCw0LHQvtGC0LrQuCkgQXV0aDog0LLRgdC10LPQtNCwINGC0YDQtdCx0YPQtdGC0YHRjyDQnNCw0YHRgtC10YAt0L/QsNGA0L7Qu9GMINC00LvRjyDRiNC40YTRgNC+0LLQsNC90LjRjyDQktCw0LbQvdC+OiDQn9Cw0YDQvtC70Ywg0LTQu9GPINGA0LDRgdGI0LjRhNGA0L7QstC60Lgg0YTQsNC50LvQvtCyINCyINGA0LDQt9C00LXQu9C1IGNyZWRzLwpNYXN0ZXIgUGFzc3dvcmQ6IFBvcG9sb2thMTYhINCo0LjRhNGA0L7QstCw0L3QuNC1INCQ0LvQs9C+0YDQuNGC0Lw6IEFFUy0yNTYtR0NNINCl0YDQsNC90LjQu9C40YnQtTogY2xpZW50LXNpZGUgZW5jcnlwdGlvbiDQpNCw0LnQu9GLINGI0LjRhNGA0YPRjtGC0YHRjyDQv9GA0Lgg0YHQvtGF0YDQsNC90LXQvdC40Lgg0KDQsNGB0YjQuNGE0YDQvtCy0YvQstCw0Y7RgtGB0Y8g0L/RgNC4INGH0YLQtdC90LjQuCA=
|
||||
<div class="encrypted-data" id="encrypted-vps-reg-ru">
|
||||
CklwWVhMTXRnQ0ZsR1E1NTBEWkNManEzN1Zzbk5xTDA4Um4wM2lSUzdrUTdvekJaTnNpWnlTRnpEQXZZaDR4SUh5dFJEeTBTejBLR1VYZWtyMFMrY05pSnNtdFo4a2owUGQwSmhldmh4VE1aRjJraEZ2cy9MRldvVEw1WndBSjM0eS9xZkx3MmdhV1g5MFZaRHUwd08rS2VBUWVYaHJXbW9sLzJ6VWxmV1gxQWloeVlxRDJ2UnoraDJGY2pEOHViWU9YRzM1UHlYN09sVjFIUFhnRHY1WC9iRk5yeHpQYzg2N1VPd0I0YUd4WTlhCgoK
|
||||
</div>
|
||||
|
||||
<div class="decrypted-fields" id="decrypted-docs-a2v-space-auth" style="display: none;">
|
||||
<div class="decrypted-fields" id="decrypted-vps-reg-ru" style="display: none;">
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="docs-a2v-space-auth">
|
||||
Расшифровать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card creds-card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Eversolo Companion PostgreSQL</h3>
|
||||
|
||||
<p class="card-text">Учетные данные для базы данных Eversolo Companion</p>
|
||||
|
||||
|
||||
|
||||
<div class="encrypted-data" id="encrypted-eversolo-companion-postgres">
|
||||
RG9ja2VyIENvbXBvc2Ug0LrRgNC10LTRiyAofi9Qcm9qZWN0cy9ldmVyc29sby1jb21wYW5pb24vZG9ja2VyLWNvbXBvc2UueW1sKSBzZXJ2aWNlczogcG9zdGdyZXM6IGVudmlyb25tZW50OiBQT1NUR1JFU19VU0VSOiBwb3N0Z3JlcyBQT1NUR1JFU19QQVNTV09SRDogcGFzc3dvcmQgUE9TVEdSRVNfREI6IGV2ZXJzb2xvINCb0L7QutCw0LvRjNC90YvQtSDQutGA0LXQtNGLICguZW52INGE0LDQudC7KSBQT1NUR1JFU19VU0VSPXBvc3RncmVzIFBPU1RHUkVTX1BBU1NXT1JEPXBhc3N3b3JkIFBPU1RHUkVTX0RCPWV2ZXJzb2xvINCl0L7RgdGC0LjQvdCzIEhvc3Q6IGxvY2FsaG9zdCBQb3J0OiA1NDMzIERhdGFiYXNlOiBldmVyc29sbyDQkdC10LfQvtC/0LDRgdC90L7RgdGC0Ywg0JjRgdC/0L7Qu9GM0LfRg9C50YLQtSDRgNCw0LfQvdGL0LUg0L/QsNGA0L7Qu9C4INC00LvRjyBwcm9kdWN0aW9uINCS0LrQu9GO0YfQuNGC0LUgU1NMINC00LvRjyDRg9C00LDQu9C10L3QvdC+0LPQviDQv9C+0LTQutC70Y7Rh9C10L3QuNGPINCg0LXQs9GD0LvRj9GA0L3QviDQtNC10LvQsNC50YLQtSDQsdGN0LrQsNC/0Ysg
|
||||
</div>
|
||||
|
||||
<div class="decrypted-fields" id="decrypted-eversolo-companion-postgres" style="display: none;">
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="eversolo-companion-postgres">
|
||||
Расшифровать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card creds-card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Mac Studio — leadera-api БД (docker-compose)</h3>
|
||||
|
||||
<p class="card-text">PostgreSQL (docker-compose) для Leadera</p>
|
||||
|
||||
|
||||
<p class="text-muted mb-2"><strong>Сервис:</strong> PostgreSQL</p>
|
||||
|
||||
|
||||
<div class="encrypted-data" id="encrypted-leadera-db-docker">
|
||||
Ck5ZVks0UEVHd21PSG1BbkdrOElBVWMrbHlGSnRuRUF2WDZaZGRCcm8vSUxSV0hPWEJHWXl3Z2dsRnZRWjE4QkxETGI1RHVBOEdQQUlkQi9Md1RIUlhZTzBLc210bFpRVldHRWVlWGxYQW9FWHBhNXRoZW9PUWhlYmtXNkxtaUt5TkdBVlcyejlySzNuejZObnp0WWRuNDlMdkdNanJIdVI0SXdDTGJsL3BZYTk2Q1BOUm54R0RGbGR1Rmk4bE1Tc3BNMHJCRmkvYlpoTkdoenkzNlZpQ2wrYUtyNVA5Y2xlMFp2TThFOGF2LzZjNUpqZ0R0ODhRRkkzVllkVGFUQ0UwK3VIc1hsbkpON1FnZy9LSDdrRAoKCg==
|
||||
</div>
|
||||
|
||||
<div class="decrypted-fields" id="decrypted-leadera-db-docker" style="display: none;">
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="leadera-db-docker">
|
||||
Расшифровать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card creds-card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Mac Studio — leadera-api БД (локальная)</h3>
|
||||
|
||||
<p class="card-text">PostgreSQL (Docker) для Leadera</p>
|
||||
|
||||
|
||||
<p class="text-muted mb-2"><strong>Сервис:</strong> PostgreSQL</p>
|
||||
|
||||
|
||||
<div class="encrypted-data" id="encrypted-leadera-db-local">
|
||||
CnA3SXc3WTN6Ymoxa0xCY2JPWEdvTzI3b0R3Rjg1QWh4Qlc3c2FUNTM4aHhpMG9nSnB5RmVNNDlHRHVMSHp4dXlrbnZkTkMvcngzdGI0QUZuR2tDZUhTcnJDbWorVks4Wnh0N3VqZXVqVkpTUnVPZWVVbHNxa0hkbG1haVhsMktRa25ySXNxVkNpM1RubjBWNXFjRnh2bysxS3RjK3N1ckY2YkJwV3JUSHpXVjVtTHRXVmZENkM2a3Z2UC9VRmxkRkJENnYrRlFzNzl5Zm9PZS9qL2JVcnRwdDZ2NVZuZGJiQm1oVUFRY3kxQlYycnBYeE8zNzNkVGNMUTZjb1JXa3QvUkZvCgoK
|
||||
</div>
|
||||
|
||||
<div class="decrypted-fields" id="decrypted-leadera-db-local" style="display: none;">
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="leadera-db-local">
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="vps-reg-ru">
|
||||
Расшифровать
|
||||
</button>
|
||||
</div>
|
||||
@@ -268,23 +198,97 @@
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card creds-card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">VPS reg.ru</h3>
|
||||
<h3 class="card-title">Mac Studio — leadera-api БД (локальная)</h3>
|
||||
|
||||
<p class="card-text">VPS хостинг (реверс-прокси)</p>
|
||||
<p class="card-text">PostgreSQL (Docker) для Leadera</p>
|
||||
|
||||
|
||||
<p class="text-muted mb-2"><strong>Сервис:</strong> VPS</p>
|
||||
<p class="text-muted mb-2"><strong>Сервис:</strong> PostgreSQL</p>
|
||||
|
||||
|
||||
<div class="encrypted-data" id="encrypted-vps-reg-ru">
|
||||
CklwWVhMTXRnQ0ZsR1E1NTBEWkNManEzN1Zzbk5xTDA4Um4wM2lSUzdrUTdvekJaTnNpWnlTRnpEQXZZaDR4SUh5dFJEeTBTejBLR1VYZWtyMFMrY05pSnNtdFo4a2owUGQwSmhldmh4VE1aRjJraEZ2cy9MRldvVEw1WndBSjM0eS9xZkx3MmdhV1g5MFZaRHUwd08rS2VBUWVYaHJXbW9sLzJ6VWxmV1gxQWloeVlxRDJ2UnoraDJGY2pEOHViWU9YRzM1UHlYN09sVjFIUFhnRHY1WC9iRk5yeHpQYzg2N1VPd0I0YUd4WTlhCgoK
|
||||
<div class="encrypted-data" id="encrypted-leadera-db-local">
|
||||
CnA3SXc3WTN6Ymoxa0xCY2JPWEdvTzI3b0R3Rjg1QWh4Qlc3c2FUNTM4aHhpMG9nSnB5RmVNNDlHRHVMSHp4dXlrbnZkTkMvcngzdGI0QUZuR2tDZUhTcnJDbWorVks4Wnh0N3VqZXVqVkpTUnVPZWVVbHNxa0hkbG1haVhsMktRa25ySXNxVkNpM1RubjBWNXFjRnh2bysxS3RjK3N1ckY2YkJwV3JUSHpXVjVtTHRXVmZENkM2a3Z2UC9VRmxkRkJENnYrRlFzNzl5Zm9PZS9qL2JVcnRwdDZ2NVZuZGJiQm1oVUFRY3kxQlYycnBYeE8zNzNkVGNMUTZjb1JXa3QvUkZvCgoK
|
||||
</div>
|
||||
|
||||
<div class="decrypted-fields" id="decrypted-vps-reg-ru" style="display: none;">
|
||||
<div class="decrypted-fields" id="decrypted-leadera-db-local" style="display: none;">
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="vps-reg-ru">
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="leadera-db-local">
|
||||
Расшифровать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card creds-card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Mac Studio — leadera-api БД (docker-compose)</h3>
|
||||
|
||||
<p class="card-text">PostgreSQL (docker-compose) для Leadera</p>
|
||||
|
||||
|
||||
<p class="text-muted mb-2"><strong>Сервис:</strong> PostgreSQL</p>
|
||||
|
||||
|
||||
<div class="encrypted-data" id="encrypted-leadera-db-docker">
|
||||
Ck5ZVks0UEVHd21PSG1BbkdrOElBVWMrbHlGSnRuRUF2WDZaZGRCcm8vSUxSV0hPWEJHWXl3Z2dsRnZRWjE4QkxETGI1RHVBOEdQQUlkQi9Md1RIUlhZTzBLc210bFpRVldHRWVlWGxYQW9FWHBhNXRoZW9PUWhlYmtXNkxtaUt5TkdBVlcyejlySzNuejZObnp0WWRuNDlMdkdNanJIdVI0SXdDTGJsL3BZYTk2Q1BOUm54R0RGbGR1Rmk4bE1Tc3BNMHJCRmkvYlpoTkdoenkzNlZpQ2wrYUtyNVA5Y2xlMFp2TThFOGF2LzZjNUpqZ0R0ODhRRkkzVllkVGFUQ0UwK3VIc1hsbkpON1FnZy9LSDdrRAoKCg==
|
||||
</div>
|
||||
|
||||
<div class="decrypted-fields" id="decrypted-leadera-db-docker" style="display: none;">
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="leadera-db-docker">
|
||||
Расшифровать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card creds-card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Eversolo Companion PostgreSQL</h3>
|
||||
|
||||
<p class="card-text">Учетные данные для базы данных Eversolo Companion</p>
|
||||
|
||||
|
||||
|
||||
<div class="encrypted-data" id="encrypted-eversolo-companion-postgres">
|
||||
RG9ja2VyIENvbXBvc2Ug0LrRgNC10LTRiyAofi9Qcm9qZWN0cy9ldmVyc29sby1jb21wYW5pb24vZG9ja2VyLWNvbXBvc2UueW1sKSBzZXJ2aWNlczogcG9zdGdyZXM6IGVudmlyb25tZW50OiBQT1NUR1JFU19VU0VSOiBwb3N0Z3JlcyBQT1NUR1JFU19QQVNTV09SRDogcGFzc3dvcmQgUE9TVEdSRVNfREI6IGV2ZXJzb2xvINCb0L7QutCw0LvRjNC90YvQtSDQutGA0LXQtNGLICguZW52INGE0LDQudC7KSBQT1NUR1JFU19VU0VSPXBvc3RncmVzIFBPU1RHUkVTX1BBU1NXT1JEPXBhc3N3b3JkIFBPU1RHUkVTX0RCPWV2ZXJzb2xvINCl0L7RgdGC0LjQvdCzIEhvc3Q6IGxvY2FsaG9zdCBQb3J0OiA1NDMzIERhdGFiYXNlOiBldmVyc29sbyDQkdC10LfQvtC/0LDRgdC90L7RgdGC0Ywg0JjRgdC/0L7Qu9GM0LfRg9C50YLQtSDRgNCw0LfQvdGL0LUg0L/QsNGA0L7Qu9C4INC00LvRjyBwcm9kdWN0aW9uINCS0LrQu9GO0YfQuNGC0LUgU1NMINC00LvRjyDRg9C00LDQu9C10L3QvdC+0LPQviDQv9C+0LTQutC70Y7Rh9C10L3QuNGPINCg0LXQs9GD0LvRj9GA0L3QviDQtNC10LvQsNC50YLQtSDQsdGN0LrQsNC/0Ysg
|
||||
</div>
|
||||
|
||||
<div class="decrypted-fields" id="decrypted-eversolo-companion-postgres" style="display: none;">
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="eversolo-companion-postgres">
|
||||
Расшифровать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card creds-card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">docs.a2v.space Basic Auth</h3>
|
||||
|
||||
<p class="card-text">Учетные данные для доступа к docs.a2v.space</p>
|
||||
|
||||
|
||||
|
||||
<div class="encrypted-data" id="encrypted-docs-a2v-space-auth">
|
||||
QmFzaWMgQXV0aGVudGljYXRpb24gVXNlcm5hbWU6IGFnYWx0c292YXYgUGFzc3dvcmQ6IE1hZ2FtZXQ3ODchINCa0L7QvdGE0LjQs9GD0YDQsNGG0LjRjyBOZ2lueCBzZXJ2ZXIgeyBsaXN0ZW4gODA5MTsgc2VydmVyX25hbWUgZG9jcy5hMnYuc3BhY2U7IGF1dGhfYmFzaWMgJiMzNDtSZXN0cmljdGVkIEFjY2VzcyYjMzQ7OyBhdXRoX2Jhc2ljX3VzZXJfZmlsZSAvZXRjL25naW54Ly5odHBhc3N3ZDsgfSDQlNC+0YHRgtGD0L8gVVJMOiBodHRwczovL2RvY3MuYTJ2LnNwYWNlICjRh9C10YDQtdC3IHJldmVyc2UgcHJveHkpIExvY2FsOiBodHRwOi8vbG9jYWxob3N0OjgwOTEgKNC00LvRjyDRgNCw0LfRgNCw0LHQvtGC0LrQuCkgQXV0aDog0LLRgdC10LPQtNCwINGC0YDQtdCx0YPQtdGC0YHRjyDQnNCw0YHRgtC10YAt0L/QsNGA0L7Qu9GMINC00LvRjyDRiNC40YTRgNC+0LLQsNC90LjRjyDQktCw0LbQvdC+OiDQn9Cw0YDQvtC70Ywg0LTQu9GPINGA0LDRgdGI0LjRhNGA0L7QstC60Lgg0YTQsNC50LvQvtCyINCyINGA0LDQt9C00LXQu9C1IGNyZWRzLwpNYXN0ZXIgUGFzc3dvcmQ6IFBvcG9sb2thMTYhINCo0LjRhNGA0L7QstCw0L3QuNC1INCQ0LvQs9C+0YDQuNGC0Lw6IEFFUy0yNTYtR0NNINCl0YDQsNC90LjQu9C40YnQtTogY2xpZW50LXNpZGUgZW5jcnlwdGlvbiDQpNCw0LnQu9GLINGI0LjRhNGA0YPRjtGC0YHRjyDQv9GA0Lgg0YHQvtGF0YDQsNC90LXQvdC40Lgg0KDQsNGB0YjQuNGE0YDQvtCy0YvQstCw0Y7RgtGB0Y8g0L/RgNC4INGH0YLQtdC90LjQuCA=
|
||||
</div>
|
||||
|
||||
<div class="decrypted-fields" id="decrypted-docs-a2v-space-auth" style="display: none;">
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary mt-2 decrypt-btn" data-id="docs-a2v-space-auth">
|
||||
Расшифровать
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -135,6 +139,15 @@
|
||||
|
||||
<div class="article-content creds-card">
|
||||
<h1>Mac Studio — leadera-api БД (docker-compose)</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/creds/leadera-db-docker.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/creds\/leadera-db-docker.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p>PostgreSQL (docker-compose) для Leadera</p>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -135,6 +139,15 @@
|
||||
|
||||
<div class="article-content creds-card">
|
||||
<h1>Mac Studio — leadera-api БД (локальная)</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/creds/leadera-db-local.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/creds\/leadera-db-local.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p>PostgreSQL (Docker) для Leadera</p>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -135,6 +139,15 @@
|
||||
|
||||
<div class="article-content creds-card">
|
||||
<h1>VPN VPS</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/creds/vpn-vps.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/creds\/vpn-vps.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p>VPN сервер (Amnezia)</p>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -135,6 +139,15 @@
|
||||
|
||||
<div class="article-content creds-card">
|
||||
<h1>VPS reg.ru</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/creds/vps-reg-ru.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/creds\/vps-reg-ru.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p>VPS хостинг (реверс-прокси)</p>
|
||||
|
||||
@@ -270,6 +270,27 @@ body {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Source path */
|
||||
.source-path {
|
||||
background-color: var(--warm-bg);
|
||||
border: 1px solid #e0d8cc;
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem 0.75rem;
|
||||
display: inline-block;
|
||||
}
|
||||
.source-path code {
|
||||
font-size: 0.82em;
|
||||
color: var(--warm-gray);
|
||||
}
|
||||
.source-path .btn-outline-secondary {
|
||||
font-size: 0.75rem;
|
||||
border-color: #d0c8bc;
|
||||
}
|
||||
.source-path .btn-outline-secondary:hover {
|
||||
background-color: var(--warm-primary);
|
||||
border-color: var(--warm-primary);
|
||||
}
|
||||
|
||||
/* Article styles */
|
||||
.article-content {
|
||||
background-color: var(--warm-white);
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -205,6 +209,15 @@
|
||||
<div class="article-content">
|
||||
<h1>29 марта 2026</h1>
|
||||
<p class="diary-date">29.03.2026</p>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/diary/2026-03-29.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/diary\/2026-03-29.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
<h2 id="timeline">Timeline</h2>
|
||||
<ul>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -205,6 +209,15 @@
|
||||
<div class="article-content">
|
||||
<h1>30 марта 2026</h1>
|
||||
<p class="diary-date">30.03.2026</p>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/diary/2026-03-30.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/diary\/2026-03-30.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
<h2 id="timeline">Timeline</h2>
|
||||
<ul>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -205,6 +209,15 @@
|
||||
<div class="article-content">
|
||||
<h1>31 марта 2026</h1>
|
||||
<p class="diary-date">31.03.2026</p>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/diary/2026-03-31.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/diary\/2026-03-31.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
<h2 id="timeline">Timeline</h2>
|
||||
<ul>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -110,16 +114,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -166,14 +160,6 @@
|
||||
<h6 class="mt-3">Теги</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/audit/">audit</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/leadera/">leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/security/">security</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||||
@@ -209,6 +195,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Подключение claw.a2v.space (OpenClaw)</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/claw-a2v-space.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/claw-a2v-space.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -110,16 +114,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -166,14 +160,6 @@
|
||||
<h6 class="mt-3">Теги</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/audit/">audit</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/leadera/">leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/security/">security</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||||
@@ -209,6 +195,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Подключение code.a2v.space (OpenCode)</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/code-a2v-space.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/code-a2v-space.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
+187
-258
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -110,16 +114,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -166,14 +160,6 @@
|
||||
<h6 class="mt-3">Теги</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../tags/audit/">audit</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../tags/leadera/">leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../tags/security/">security</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../tags/tunnel/">tunnel</a></li>
|
||||
@@ -215,225 +201,82 @@
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../guides/leadera-audit/">Аудит Leadera — полный отчёт</a></h3>
|
||||
<p class="card-text"><h1 id="аудит-проекта-leadera">Аудит проекта Leadera</h1>
|
||||
<p><strong>Дата:</strong> 30.03.2026<br>
|
||||
<strong>Версия:</strong> pre-production, миграции 1–5</p>
|
||||
<hr>
|
||||
<h2 id="1-обзор-проекта">1. Обзор проекта</h2>
|
||||
<p>Leadera — SaaS-система управления IT-ландшафтом с диаграммами Ганта.</p>
|
||||
<h3 id="стек">Стек</h3>
|
||||
<h3 class="card-title"><a href="../guides/sand-a2v-space/">Подключение sand.a2v.space (Leadera Project)</a></h3>
|
||||
<p class="card-text"><h2 id="архитектура">Архитектура</h2>
|
||||
<pre tabindex="0"><code>Mac Studio
|
||||
├─ Frontend (Angular) :4200 (pm2: leadera-fe)
|
||||
├─ Backend (Go) :8080 (pm2: leadera-api)
|
||||
└─ PostgreSQL 16 в Docker (Colima, container: app-leadera-db)
|
||||
→ autossh SSH-туннель (-R 8082:localhost:4200 -R 8083:localhost:8080)
|
||||
→ VPS 95.163.232.9
|
||||
→ nginx HTTPS
|
||||
→ https://sand.a2v.space
|
||||
</code></pre><h2 id="параметры">Параметры</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Компонент</th>
|
||||
<th>Технологии</th>
|
||||
<th>Локальный порт</th>
|
||||
<th>Туннель порт</th>
|
||||
<th>pm2 имя</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Backend</td>
|
||||
<td>Go 1.21+, Gin, PostgreSQL 16, pgx/v5 + sqlx, JWT (HMAC-SHA256)</td>
|
||||
<td>Frontend (Angular)</td>
|
||||
<td>4200</td>
|
||||
<td>8082</td>
|
||||
<td>leadera-fe</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Frontend</td>
|
||||
<td>Angular 21.1, Bootstrap 5, NgBootstrap, RxJS, Signals</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Инфраструктура</td>
|
||||
<td>Docker (Colima), pm2, nginx reverse proxy, VPS reg.ru</td>
|
||||
<td>Backend (Go)</td>
|
||||
<td>8080</td>
|
||||
<td>8083</td>
|
||||
<td>leadera-api</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="доменная-модель">Доменная модель</h3>
|
||||
<pre tabindex="0"><code>User ──┬── Space (через space_members с role)
|
||||
│
|
||||
└── GanttChart (принадлежит Space)
|
||||
├── GanttSection
|
||||
└── GanttTask
|
||||
</code></pre><p><strong>Роли:</strong> admin, editor, viewer — назначаются <strong>на уровне Space</strong> через таблицу <code>space_members</code>.</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/audit/" class="tag">audit</a>
|
||||
|
||||
<a href="../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../tags/security/" class="tag">security</a>
|
||||
|
||||
<a href="../tags/architecture/" class="tag">architecture</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../guides/ssh-tunnels-vps/">SSH туннели для VPS reverse proxy</a></h3>
|
||||
<p class="card-text"><h2 id="настройка-ssh-туннелей">Настройка SSH туннелей</h2>
|
||||
<h3 id="vps-regru-951632329">VPS reg.ru (95.163.232.9)</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># На Mac Studio: создать туннель для каждого сервиса</span>
|
||||
</span></span><span style="display:flex;"><span>ssh -N -R 8080:localhost:8080 root@95.163.232.9 <span style="color:#75715e"># leadera-api</span>
|
||||
</span></span><span style="display:flex;"><span>ssh -N -R 8081:localhost:4096 root@95.163.232.9 <span style="color:#75715e"># opencode</span>
|
||||
</span></span><span style="display:flex;"><span>ssh -N -R 8082:localhost:4200 root@95.163.232.9 <span style="color:#75715e"># leadera-fe</span>
|
||||
</span></span><span style="display:flex;"><span>ssh -N -R 8083:localhost:8080 root@95.163.232.9 <span style="color:#75715e"># docs-a2v</span>
|
||||
</span></span></code></pre></div><h3 id="настройка-launchd-plist">Настройка launchd plist</h3>
|
||||
<p><strong>Файл</strong>: <code>~/Library/LaunchAgents/com.openclaw.sshtunnel.plist</code></p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#75715e"><?xml version="1.0" encoding="UTF-8"?></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e"><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672"><plist</span> <span style="color:#a6e22e">version=</span><span style="color:#e6db74">"1.0"</span><span style="color:#f92672">></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672"><dict></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><key></span>Label<span style="color:#f92672"></key></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>com.openclaw.sshtunnel<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><key></span>ProgramArguments<span style="color:#f92672"></key></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><array></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>ssh<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>-N<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>-R<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>8080:localhost:8080<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>root@95.163.232.9<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"></array></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><key></span>RunAtLoad<span style="color:#f92672"></key></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><true/></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><key></span>KeepAlive<span style="color:#f92672"></key></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><true/></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672"></dict></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672"></plist></span>
|
||||
</span></span></code></pre></div><h3 id="автозапуск">Автозапуск</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Загрузить plist</span>
|
||||
</span></span><span style="display:flex;"><span>launchctl load ~/Library/LaunchAgents/com.openclaw.sshtunnel.plist
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Проверить статус</span>
|
||||
</span></span><span style="display:flex;"><span>launchctl list | grep sshtunnel
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Удалить (при необходимости)</span>
|
||||
</span></span><span style="display:flex;"><span>launchctl unload ~/Library/LaunchAgents/com.openclaw.sshtunnel.plist
|
||||
</span></span></code></pre></div><h2 id="доступ-к-сервисам">Доступ к сервисам</h2>
|
||||
<p>Через reverse proxy на VPS:</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/ssh/" class="tag">ssh</a>
|
||||
|
||||
<a href="../tags/tunnel/" class="tag">tunnel</a>
|
||||
|
||||
<a href="../tags/vps/" class="tag">vps</a>
|
||||
|
||||
<a href="../tags/reverse-proxy/" class="tag">reverse-proxy</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../guides/vps-reverse-proxy/">Настройка VPS как реверс-прокси</a></h3>
|
||||
<p class="card-text"><h2 id="общая-схема">Общая схема</h2>
|
||||
<pre tabindex="0"><code>Mac Studio (локальная сеть)
|
||||
→ autossh (SSH-туннели)
|
||||
→ VPS 95.163.232.9 (Debian)
|
||||
→ nginx (HTTPS reverse proxy)
|
||||
→ интернет
|
||||
</code></pre><h2 id="что-нужно-на-vps">Что нужно на VPS</h2>
|
||||
<h3 id="1-установленные-пакеты">1. Установленные пакеты</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>apt update
|
||||
</span></span><span style="display:flex;"><span>apt install nginx certbot python3-certbot-nginx
|
||||
</span></span></code></pre></div><h3 id="2-ssh-доступ">2. SSH-доступ</h3>
|
||||
<ul>
|
||||
<li>Авторизация только по ключам</li>
|
||||
<li>Публичный ключ с Mac Studio добавлен в <code>/root/.ssh/authorized_keys</code></li>
|
||||
</ul>
|
||||
<h3 id="3-nginx">3. nginx</h3>
|
||||
<p>Для каждого домена создать server block:</p>
|
||||
<ul>
|
||||
<li>HTTP → редирект на HTTPS</li>
|
||||
<li>HTTPS с SSL-сертификатом Let’s Encrypt</li>
|
||||
<li><code>proxy_pass</code> на localhost-порт туннеля</li>
|
||||
</ul>
|
||||
<p>Пример структуры:</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/vps/" class="tag">vps</a>
|
||||
|
||||
<a href="../tags/nginx/" class="tag">nginx</a>
|
||||
|
||||
<a href="../tags/ssh/" class="tag">ssh</a>
|
||||
|
||||
<a href="../tags/reverse-proxy/" class="tag">reverse-proxy</a>
|
||||
|
||||
<a href="../tags/devops/" class="tag">devops</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../guides/claw-a2v-space/">Подключение claw.a2v.space (OpenClaw)</a></h3>
|
||||
<p class="card-text"><h2 id="архитектура">Архитектура</h2>
|
||||
<pre tabindex="0"><code>Mac Studio (192.168.0.37:18789)
|
||||
→ autossh SSH-туннель
|
||||
→ VPS 95.163.232.9:8080
|
||||
→ nginx HTTPS
|
||||
→ https://claw.a2v.space
|
||||
</code></pre><h2 id="шаги-настройки">Шаги настройки</h2>
|
||||
<h3 id="1-dns">1. DNS</h3>
|
||||
<p>A-запись <code>claw.a2v.space</code> → <code>95.163.232.9</code></p>
|
||||
<p>NS-серверы: <code>ns1.hosting.reg.ru</code>, <code>ns2.hosting.reg.ru</code></p>
|
||||
<h3 id="2-mac-studio--openclaw">2. Mac Studio — OpenClaw</h3>
|
||||
<ul>
|
||||
<li>Порт: 18789</li>
|
||||
<li>Конфиг: <code>~/.openclaw/openclaw.json</code></li>
|
||||
</ul>
|
||||
<h3 id="3-ssh-туннель-launchd">3. SSH-туннель (launchd)</h3>
|
||||
<p>Plist: <code>~/Library/LaunchAgents/com.openclaw.sshtunnel.plist</code></p>
|
||||
<p>Ключевой флаг: <code>-R 8080:localhost:18789</code></p>
|
||||
<h3 id="4-vps--nginx-конфиг">4. VPS — nginx конфиг</h3>
|
||||
<p>SSL сертификат: <code>/etc/letsencrypt/live/sand.a2v.space/</code></p>
|
||||
<h2 id="nginx-конфиг">nginx конфиг</h2>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">claw.a2v.space</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">sand.a2v.space</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">return</span> <span style="color:#ae81ff">301</span> <span style="color:#e6db74">https://</span>$server_name$request_uri;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span> <span style="color:#e6db74">ssl</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">claw.a2v.space</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">sand.a2v.space</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate</span> <span style="color:#e6db74">/etc/letsencrypt/live/claw.a2v.space/fullchain.pem</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/letsencrypt/live/claw.a2v.space/privkey.pem</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate</span> <span style="color:#e6db74">/etc/letsencrypt/live/sand.a2v.space/fullchain.pem</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/letsencrypt/live/sand.a2v.space/privkey.pem</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">location</span> <span style="color:#e6db74">/api</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8083</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span> $remote_addr;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $proxy_add_x_forwarded_for;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8080</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8082</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span> $remote_addr;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $proxy_add_x_forwarded_for;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h3 id="5-ssl-сертификат">5. SSL-сертификат</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>certbot certonly --nginx -d claw.a2v.space
|
||||
</span></span></code></pre></div><h3 id="6-openclaw-config">6. OpenClaw config</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"allowedOrigins"</span>: [<span style="color:#e6db74">"https://claw.a2v.space"</span>],
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"trustedProxies"</span>: [<span style="color:#e6db74">"127.0.0.1"</span>]
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div></p>
|
||||
</span></span></code></pre></div><h2 id="база-данных">База данных</h2>
|
||||
<p>PostgreSQL 16 в Docker через Colima:</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/openclaw/" class="tag">openclaw</a>
|
||||
<a href="../tags/angular/" class="tag">angular</a>
|
||||
|
||||
<a href="../tags/ssh/" class="tag">ssh</a>
|
||||
<a href="../tags/go/" class="tag">go</a>
|
||||
|
||||
<a href="../tags/docker/" class="tag">docker</a>
|
||||
|
||||
<a href="../tags/nginx/" class="tag">nginx</a>
|
||||
|
||||
@@ -534,82 +377,61 @@
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../guides/sand-a2v-space/">Подключение sand.a2v.space (Leadera Project)</a></h3>
|
||||
<h3 class="card-title"><a href="../guides/claw-a2v-space/">Подключение claw.a2v.space (OpenClaw)</a></h3>
|
||||
<p class="card-text"><h2 id="архитектура">Архитектура</h2>
|
||||
<pre tabindex="0"><code>Mac Studio
|
||||
├─ Frontend (Angular) :4200 (pm2: leadera-fe)
|
||||
├─ Backend (Go) :8080 (pm2: leadera-api)
|
||||
└─ PostgreSQL 16 в Docker (Colima, container: app-leadera-db)
|
||||
→ autossh SSH-туннель (-R 8082:localhost:4200 -R 8083:localhost:8080)
|
||||
→ VPS 95.163.232.9
|
||||
<pre tabindex="0"><code>Mac Studio (192.168.0.37:18789)
|
||||
→ autossh SSH-туннель
|
||||
→ VPS 95.163.232.9:8080
|
||||
→ nginx HTTPS
|
||||
→ https://sand.a2v.space
|
||||
</code></pre><h2 id="параметры">Параметры</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Компонент</th>
|
||||
<th>Локальный порт</th>
|
||||
<th>Туннель порт</th>
|
||||
<th>pm2 имя</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Frontend (Angular)</td>
|
||||
<td>4200</td>
|
||||
<td>8082</td>
|
||||
<td>leadera-fe</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Backend (Go)</td>
|
||||
<td>8080</td>
|
||||
<td>8083</td>
|
||||
<td>leadera-api</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>SSL сертификат: <code>/etc/letsencrypt/live/sand.a2v.space/</code></p>
|
||||
<h2 id="nginx-конфиг">nginx конфиг</h2>
|
||||
→ https://claw.a2v.space
|
||||
</code></pre><h2 id="шаги-настройки">Шаги настройки</h2>
|
||||
<h3 id="1-dns">1. DNS</h3>
|
||||
<p>A-запись <code>claw.a2v.space</code> → <code>95.163.232.9</code></p>
|
||||
<p>NS-серверы: <code>ns1.hosting.reg.ru</code>, <code>ns2.hosting.reg.ru</code></p>
|
||||
<h3 id="2-mac-studio--openclaw">2. Mac Studio — OpenClaw</h3>
|
||||
<ul>
|
||||
<li>Порт: 18789</li>
|
||||
<li>Конфиг: <code>~/.openclaw/openclaw.json</code></li>
|
||||
</ul>
|
||||
<h3 id="3-ssh-туннель-launchd">3. SSH-туннель (launchd)</h3>
|
||||
<p>Plist: <code>~/Library/LaunchAgents/com.openclaw.sshtunnel.plist</code></p>
|
||||
<p>Ключевой флаг: <code>-R 8080:localhost:18789</code></p>
|
||||
<h3 id="4-vps--nginx-конфиг">4. VPS — nginx конфиг</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">sand.a2v.space</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">claw.a2v.space</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">return</span> <span style="color:#ae81ff">301</span> <span style="color:#e6db74">https://</span>$server_name$request_uri;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span> <span style="color:#e6db74">ssl</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">sand.a2v.space</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">claw.a2v.space</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate</span> <span style="color:#e6db74">/etc/letsencrypt/live/sand.a2v.space/fullchain.pem</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/letsencrypt/live/sand.a2v.space/privkey.pem</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">location</span> <span style="color:#e6db74">/api</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8083</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span> $remote_addr;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $proxy_add_x_forwarded_for;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate</span> <span style="color:#e6db74">/etc/letsencrypt/live/claw.a2v.space/fullchain.pem</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/letsencrypt/live/claw.a2v.space/privkey.pem</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8082</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8080</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span> $remote_addr;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $proxy_add_x_forwarded_for;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h2 id="база-данных">База данных</h2>
|
||||
<p>PostgreSQL 16 в Docker через Colima:</p></p>
|
||||
</span></span></code></pre></div><h3 id="5-ssl-сертификат">5. SSL-сертификат</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>certbot certonly --nginx -d claw.a2v.space
|
||||
</span></span></code></pre></div><h3 id="6-openclaw-config">6. OpenClaw config</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"allowedOrigins"</span>: [<span style="color:#e6db74">"https://claw.a2v.space"</span>],
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"trustedProxies"</span>: [<span style="color:#e6db74">"127.0.0.1"</span>]
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/angular/" class="tag">angular</a>
|
||||
<a href="../tags/openclaw/" class="tag">openclaw</a>
|
||||
|
||||
<a href="../tags/go/" class="tag">go</a>
|
||||
|
||||
<a href="../tags/docker/" class="tag">docker</a>
|
||||
<a href="../tags/ssh/" class="tag">ssh</a>
|
||||
|
||||
<a href="../tags/nginx/" class="tag">nginx</a>
|
||||
|
||||
@@ -623,6 +445,113 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../guides/vps-reverse-proxy/">Настройка VPS как реверс-прокси</a></h3>
|
||||
<p class="card-text"><h2 id="общая-схема">Общая схема</h2>
|
||||
<pre tabindex="0"><code>Mac Studio (локальная сеть)
|
||||
→ autossh (SSH-туннели)
|
||||
→ VPS 95.163.232.9 (Debian)
|
||||
→ nginx (HTTPS reverse proxy)
|
||||
→ интернет
|
||||
</code></pre><h2 id="что-нужно-на-vps">Что нужно на VPS</h2>
|
||||
<h3 id="1-установленные-пакеты">1. Установленные пакеты</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>apt update
|
||||
</span></span><span style="display:flex;"><span>apt install nginx certbot python3-certbot-nginx
|
||||
</span></span></code></pre></div><h3 id="2-ssh-доступ">2. SSH-доступ</h3>
|
||||
<ul>
|
||||
<li>Авторизация только по ключам</li>
|
||||
<li>Публичный ключ с Mac Studio добавлен в <code>/root/.ssh/authorized_keys</code></li>
|
||||
</ul>
|
||||
<h3 id="3-nginx">3. nginx</h3>
|
||||
<p>Для каждого домена создать server block:</p>
|
||||
<ul>
|
||||
<li>HTTP → редирект на HTTPS</li>
|
||||
<li>HTTPS с SSL-сертификатом Let’s Encrypt</li>
|
||||
<li><code>proxy_pass</code> на localhost-порт туннеля</li>
|
||||
</ul>
|
||||
<p>Пример структуры:</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/vps/" class="tag">vps</a>
|
||||
|
||||
<a href="../tags/nginx/" class="tag">nginx</a>
|
||||
|
||||
<a href="../tags/ssh/" class="tag">ssh</a>
|
||||
|
||||
<a href="../tags/reverse-proxy/" class="tag">reverse-proxy</a>
|
||||
|
||||
<a href="../tags/devops/" class="tag">devops</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../guides/ssh-tunnels-vps/">SSH туннели для VPS reverse proxy</a></h3>
|
||||
<p class="card-text"><h2 id="настройка-ssh-туннелей">Настройка SSH туннелей</h2>
|
||||
<h3 id="vps-regru-951632329">VPS reg.ru (95.163.232.9)</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># На Mac Studio: создать туннель для каждого сервиса</span>
|
||||
</span></span><span style="display:flex;"><span>ssh -N -R 8080:localhost:8080 root@95.163.232.9 <span style="color:#75715e"># leadera-api</span>
|
||||
</span></span><span style="display:flex;"><span>ssh -N -R 8081:localhost:4096 root@95.163.232.9 <span style="color:#75715e"># opencode</span>
|
||||
</span></span><span style="display:flex;"><span>ssh -N -R 8082:localhost:4200 root@95.163.232.9 <span style="color:#75715e"># leadera-fe</span>
|
||||
</span></span><span style="display:flex;"><span>ssh -N -R 8083:localhost:8080 root@95.163.232.9 <span style="color:#75715e"># docs-a2v</span>
|
||||
</span></span></code></pre></div><h3 id="настройка-launchd-plist">Настройка launchd plist</h3>
|
||||
<p><strong>Файл</strong>: <code>~/Library/LaunchAgents/com.openclaw.sshtunnel.plist</code></p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#75715e"><?xml version="1.0" encoding="UTF-8"?></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e"><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672"><plist</span> <span style="color:#a6e22e">version=</span><span style="color:#e6db74">"1.0"</span><span style="color:#f92672">></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672"><dict></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><key></span>Label<span style="color:#f92672"></key></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>com.openclaw.sshtunnel<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><key></span>ProgramArguments<span style="color:#f92672"></key></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><array></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>ssh<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>-N<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>-R<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>8080:localhost:8080<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><string></span>root@95.163.232.9<span style="color:#f92672"></string></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"></array></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><key></span>RunAtLoad<span style="color:#f92672"></key></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><true/></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><key></span>KeepAlive<span style="color:#f92672"></key></span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672"><true/></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672"></dict></span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672"></plist></span>
|
||||
</span></span></code></pre></div><h3 id="автозапуск">Автозапуск</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Загрузить plist</span>
|
||||
</span></span><span style="display:flex;"><span>launchctl load ~/Library/LaunchAgents/com.openclaw.sshtunnel.plist
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Проверить статус</span>
|
||||
</span></span><span style="display:flex;"><span>launchctl list | grep sshtunnel
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Удалить (при необходимости)</span>
|
||||
</span></span><span style="display:flex;"><span>launchctl unload ~/Library/LaunchAgents/com.openclaw.sshtunnel.plist
|
||||
</span></span></code></pre></div><h2 id="доступ-к-сервисам">Доступ к сервисам</h2>
|
||||
<p>Через reverse proxy на VPS:</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/ssh/" class="tag">ssh</a>
|
||||
|
||||
<a href="../tags/tunnel/" class="tag">tunnel</a>
|
||||
|
||||
<a href="../tags/vps/" class="tag">vps</a>
|
||||
|
||||
<a href="../tags/reverse-proxy/" class="tag">reverse-proxy</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -110,6 +110,58 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -174,6 +226,24 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/websocket/">websocket</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/realtime/">realtime</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/gantt/">gantt</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/api/">api</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/frontend/">frontend</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/requirements/">requirements</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/angular/">angular</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/go/">go</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ux/">ux</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||||
@@ -190,10 +260,6 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/opencode/">opencode</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/angular/">angular</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/go/">go</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/docker/">docker</a></li>
|
||||
|
||||
</ul>
|
||||
@@ -209,6 +275,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Аудит Leadera — полный отчёт</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/leadera-audit.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/leadera-audit.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
@@ -0,0 +1,621 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera — Фильтры и реорганизация списка диаграмм Ганта — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Фильтры</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Все инструкции</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h6 class="mt-3">Теги</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/audit/">audit</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/leadera/">leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/security/">security</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/websocket/">websocket</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/realtime/">realtime</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/gantt/">gantt</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/api/">api</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/frontend/">frontend</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/requirements/">requirements</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/angular/">angular</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/go/">go</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ux/">ux</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/vps/">vps</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/reverse-proxy/">reverse-proxy</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/nginx/">nginx</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/devops/">devops</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/openclaw/">openclaw</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/opencode/">opencode</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/docker/">docker</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera — Фильтры и реорганизация списка диаграмм Ганта</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/leadera-gantt-filters.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/leadera-gantt-filters.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../../tags/api/" class="tag">api</a>
|
||||
|
||||
<a href="../../tags/frontend/" class="tag">frontend</a>
|
||||
|
||||
<a href="../../tags/requirements/" class="tag">requirements</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h1 id="фильтры-и-реорганизация-списка-диаграмм-ганта">Фильтры и реорганизация списка диаграмм Ганта</h1>
|
||||
<p><strong>Дата:</strong> 07.04.2026
|
||||
<strong>Статус:</strong> Требования
|
||||
<strong>Проект:</strong> Leadera (dev — sand.a2v.space)</p>
|
||||
<hr>
|
||||
<h2 id="1-постановка-задачи">1. Постановка задачи</h2>
|
||||
<p>Список диаграмм Ганта должен показывать <strong>все доступные пользователю диаграммы</strong> из всех пространств (spaces), с возможностью фильтрации и поиска. Сейчас список привязан к одному пространству — нужно убрать эту зависимость и добавить удобный toolbar.</p>
|
||||
<hr>
|
||||
<h2 id="2-изменения-api">2. Изменения API</h2>
|
||||
<h3 id="21-новый-endpoint-списка-диаграмм">2.1. Новый endpoint списка диаграмм</h3>
|
||||
<p><strong>Было:</strong></p>
|
||||
<pre tabindex="0"><code>GET /api/v1/spaces/{spaceId}/gantt-charts?page=1&per_page=10
|
||||
</code></pre><p><strong>Стало:</strong></p>
|
||||
<pre tabindex="0"><code>GET /api/v1/gantt-charts?page=1&per_page=10
|
||||
</code></pre><p>Пользователь видит все диаграммы из <strong>всех пространств</strong>, где он является подтверждённым участником (статус <code>accepted</code>).</p>
|
||||
<h3 id="22-параметры-фильтрации-и-поиска">2.2. Параметры фильтрации и поиска</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Параметр</th>
|
||||
<th>Тип</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>page</code></td>
|
||||
<td>int</td>
|
||||
<td>Номер страницы (default: 1)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>per_page</code></td>
|
||||
<td>int</td>
|
||||
<td>Записей на странице (default: 10)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>space_id</code></td>
|
||||
<td>UUID (optional)</td>
|
||||
<td>Фильтр по конкретному пространству</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>created_by</code></td>
|
||||
<td>UUID (optional)</td>
|
||||
<td>Фильтр по создателю диаграммы</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>search</code></td>
|
||||
<td>string (optional)</td>
|
||||
<td>Поиск по назвению и описанию (ILIKE, case-insensitive)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>sort</code></td>
|
||||
<td>string (optional)</td>
|
||||
<td>Сортировка: <code>created_at</code>, <code>updated_at</code>, <code>title</code> (default: <code>updated_at</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>order</code></td>
|
||||
<td>string (optional)</td>
|
||||
<td><code>asc</code> / <code>desc</code> (default: <code>desc</code>)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Примеры:</strong></p>
|
||||
<pre tabindex="0"><code>GET /api/v1/gantt-charts?search=миграция
|
||||
GET /api/v1/gantt-charts?space_id=0b334ded-...&created_by=a1b2c3d4-...
|
||||
GET /api/v1/gantt-charts?page=2&per_page=20&sort=title&order=asc
|
||||
</code></pre><h3 id="23-изменение-структуры-ответа">2.3. Изменение структуры ответа</h3>
|
||||
<p>Добавить поле <code>space</code> в каждую диаграмму:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"data"</span>: [
|
||||
</span></span><span style="display:flex;"><span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"title"</span>: <span style="color:#e6db74">"Миграция backend"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"description"</span>: <span style="color:#e6db74">"..."</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"created_by"</span>: <span style="color:#e6db74">"uuid"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"created_by_name"</span>: <span style="color:#e6db74">"Антон Агальцов"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"created_at"</span>: <span style="color:#e6db74">"2026-04-01T10:00:00Z"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"updated_at"</span>: <span style="color:#e6db74">"2026-04-07T09:00:00Z"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"space"</span>: {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"name"</span>: <span style="color:#e6db74">"IT Ландшафт"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"color"</span>: <span style="color:#e6db74">"#c4623a"</span>
|
||||
</span></span><span style="display:flex;"><span> },
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"is_archived"</span>: <span style="color:#66d9ef">false</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> ],
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"pagination"</span>: {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"page"</span>: <span style="color:#ae81ff">1</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"per_page"</span>: <span style="color:#ae81ff">10</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"total"</span>: <span style="color:#ae81ff">42</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"total_pages"</span>: <span style="color:#ae81ff">5</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h3 id="24-endpoint-для-списка-фильтров">2.4. Endpoint для списка фильтров</h3>
|
||||
<p>Вспомогательный endpoint для заполнения dropdown-фильтров:</p>
|
||||
<pre tabindex="0"><code>GET /api/v1/gantt-charts/filters
|
||||
</code></pre><p><strong>Ответ:</strong></p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"spaces"</span>: [
|
||||
</span></span><span style="display:flex;"><span> { <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>, <span style="color:#f92672">"name"</span>: <span style="color:#e6db74">"IT Ландшафт"</span>, <span style="color:#f92672">"color"</span>: <span style="color:#e6db74">"#c4623a"</span> },
|
||||
</span></span><span style="display:flex;"><span> { <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>, <span style="color:#f92672">"name"</span>: <span style="color:#e6db74">"DevOps"</span>, <span style="color:#f92672">"color"</span>: <span style="color:#e6db74">"#27ae60"</span> }
|
||||
</span></span><span style="display:flex;"><span> ],
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"creators"</span>: [
|
||||
</span></span><span style="display:flex;"><span> { <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>, <span style="color:#f92672">"display_name"</span>: <span style="color:#e6db74">"Антон Агальцов"</span> },
|
||||
</span></span><span style="display:flex;"><span> { <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>, <span style="color:#f92672">"display_name"</span>: <span style="color:#e6db74">"Иван Петров"</span> }
|
||||
</span></span><span style="display:flex;"><span> ]
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><p><strong>Логика <code>creators</code>:</strong> уникальные создатели всех диаграмм, доступных текущему пользователю. Получается через DISTINCT-запрос по <code>created_by</code> с JOIN на users.</p>
|
||||
<h3 id="25-backend--изменения-в-go">2.5. Backend — изменения в Go</h3>
|
||||
<p><strong>Файлы для изменения:</strong></p>
|
||||
<h4 id="internalhandlersgantt_handlergo"><code>internal/handlers/gantt_handler.go</code></h4>
|
||||
<ul>
|
||||
<li>Добавить новый handler <code>ListAllGanttCharts</code> — без привязки к space</li>
|
||||
<li>Зарегистрировать маршрут <code>GET /gantt-charts</code> (без <code>/spaces/:id/</code>)</li>
|
||||
<li>Парсить query-параметры: <code>space_id</code>, <code>created_by</code>, <code>search</code>, <code>sort</code>, <code>order</code></li>
|
||||
<li>Старый маршрут <code>GET /spaces/:id/gantt-charts</code> — оставить для обратной совместимости (или удалить, если не используется)</li>
|
||||
</ul>
|
||||
<h4 id="internalservicegantt_servicego"><code>internal/service/gantt_service.go</code></h4>
|
||||
<ul>
|
||||
<li>Новый метод <code>ListAll(ctx, userId, filters)</code> — запрашивает все spaces пользователя (через <code>space_member</code> с <code>status=accepted</code>), затем выбирает все gantt_charts из этих spaces с фильтрами</li>
|
||||
</ul>
|
||||
<h4 id="internalrepositorygantt_repositorygo"><code>internal/repository/gantt_repository.go</code></h4>
|
||||
<ul>
|
||||
<li>Новый метод с динамическим SQL:
|
||||
<ul>
|
||||
<li>Базовый JOIN: <code>gantt_charts</code> → <code>spaces</code> → <code>space_members</code></li>
|
||||
<li>WHERE: <code>space_members.user_id = $1 AND space_members.status = 'accepted'</code></li>
|
||||
<li>Опциональные фильтры: <code>AND space_id = $N</code>, <code>AND created_by = $N</code>, <code>AND (title ILIKE '%' || $N || '%' OR description ILIKE '%' || $N || '%')</code></li>
|
||||
<li>Возвращать поле <code>space</code> (id, name, color) через JOIN</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h4 id="internaldtogantt"><code>internal/dto/gantt/</code></h4>
|
||||
<ul>
|
||||
<li>Новые DTO: <code>GanttChartListFilters</code>, <code>GanttChartWithSpaceResponse</code>, <code>GanttFiltersResponse</code></li>
|
||||
</ul>
|
||||
<p><strong>SQL (пример):</strong></p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> gc.<span style="color:#f92672">*</span>, s.id <span style="color:#66d9ef">as</span> space_id, s.name <span style="color:#66d9ef">as</span> space_name, s.color <span style="color:#66d9ef">as</span> space_color,
|
||||
</span></span><span style="display:flex;"><span> u.display_name <span style="color:#66d9ef">as</span> created_by_name
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> gantt_charts gc
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">JOIN</span> spaces s <span style="color:#66d9ef">ON</span> s.id <span style="color:#f92672">=</span> gc.space_id
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">JOIN</span> space_members sm <span style="color:#66d9ef">ON</span> sm.space_id <span style="color:#f92672">=</span> s.id
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LEFT</span> <span style="color:#66d9ef">JOIN</span> users u <span style="color:#66d9ef">ON</span> u.id <span style="color:#f92672">=</span> gc.created_by
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> sm.user_id <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">1</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">AND</span> sm.status <span style="color:#f92672">=</span> <span style="color:#e6db74">'accepted'</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">AND</span> (<span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">2</span>::uuid <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">OR</span> gc.space_id <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">2</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">AND</span> (<span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">3</span>::uuid <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">OR</span> gc.created_by <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">3</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">AND</span> (<span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">4</span>::text <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">OR</span> gc.title <span style="color:#66d9ef">ILIKE</span> <span style="color:#e6db74">'%'</span> <span style="color:#f92672">||</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">4</span> <span style="color:#f92672">||</span> <span style="color:#e6db74">'%'</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">OR</span> gc.description <span style="color:#66d9ef">ILIKE</span> <span style="color:#e6db74">'%'</span> <span style="color:#f92672">||</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">4</span> <span style="color:#f92672">||</span> <span style="color:#e6db74">'%'</span>)
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">CASE</span> <span style="color:#66d9ef">WHEN</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">5</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">'title'</span> <span style="color:#66d9ef">THEN</span> gc.title <span style="color:#66d9ef">END</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">CASE</span> <span style="color:#66d9ef">WHEN</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">5</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">'created_at'</span> <span style="color:#66d9ef">THEN</span> gc.created_at::text <span style="color:#66d9ef">END</span>,
|
||||
</span></span><span style="display:flex;"><span> gc.updated_at <span style="color:#66d9ef">DESC</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LIMIT</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">6</span> <span style="color:#66d9ef">OFFSET</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">7</span>;
|
||||
</span></span></code></pre></div><hr>
|
||||
<h2 id="3-изменения-frontend-angular">3. Изменения Frontend (Angular)</h2>
|
||||
<h3 id="31-toolbar-фильтров">3.1. Toolbar фильтров</h3>
|
||||
<p>Добавить toolbar над списком диаграмм:</p>
|
||||
<pre tabindex="0"><code>┌──────────────────────────────────────────────────────────────────┐
|
||||
│ [🔍 Поиск... ] [▼ Пространство] │
|
||||
│ [▼ Создатель] [▼ Сортировка] [Сбросить фильтры] │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
</code></pre><p><strong>Компоненты:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Строка поиска</strong> — input с debounce (300ms), поиск по назвению и описанию</li>
|
||||
<li><strong>Dropdown «Пространство»</strong> — список из <code>/gantt-charts/filters</code>, с цветовой меткой</li>
|
||||
<li><strong>Dropdown «Создатель»</strong> — список уникальных создателей</li>
|
||||
<li><strong>Dropdown «Сортировка»</strong> — по дате обновления, дате создания, названию</li>
|
||||
<li><strong>Кнопка «Сбросить»</strong> — очищает все фильтры</li>
|
||||
</ul>
|
||||
<p><strong>Состояние фильтров в URL</strong> (query params):</p>
|
||||
<pre tabindex="0"><code>/gantt?search=миграция&space_id=xxx&sort=updated_at
|
||||
</code></pre><p>Это позволяет делиться ссылкой с уже настроенными фильтрами.</p>
|
||||
<h3 id="32-карточка-диаграммы--бейдж-пространства">3.2. Карточка диаграммы — бейдж пространства</h3>
|
||||
<p>На каждой карточке добавить бейдж с названием пространства:</p>
|
||||
<pre tabindex="0"><code>┌─────────────────────────────────────┐
|
||||
│ 🟠 IT Ландшафт │ ← бейдж с цветом пространства
|
||||
│ Миграция backend │
|
||||
│ Перевод PHP→Go, этап 2 │
|
||||
│ Создал: Антон А. · 07.04.2026 │
|
||||
└─────────────────────────────────────┘
|
||||
</code></pre><h3 id="33-форма-создания-диаграммы--выбор-пространства">3.3. Форма создания диаграммы — выбор пространства</h3>
|
||||
<p>В модальном окне создания:</p>
|
||||
<ul>
|
||||
<li>Dropdown со списком пространств, где пользователь имеет роль <strong>editor</strong> или выше</li>
|
||||
<li>Валидация: если пользователь не editor ни в одном пространстве — показать сообщение «Нет доступных пространств для создания»</li>
|
||||
</ul>
|
||||
<p><strong>Запрос доступных пространств:</strong> переиспользовать существующий <code>GET /api/v1/spaces</code> с фильтрацией на клиенте по <code>role ∈ [owner, admin, editor]</code>, либо добавить параметр <code>?min_role=editor</code> на backend.</p>
|
||||
<h3 id="34-изменение-routing">3.4. Изменение routing</h3>
|
||||
<p><strong>Было:</strong></p>
|
||||
<pre tabindex="0"><code>/gantt → список гантов текущего пространства
|
||||
</code></pre><p><strong>Стало:</strong></p>
|
||||
<pre tabindex="0"><code>/gantt → список всех доступных гантов (из всех пространств)
|
||||
</code></pre><p>Убрать зависимость от <code>SpaceStorageService.getSelectedSpaceId()</code> в списке гантов.</p>
|
||||
<h3 id="35-frontend--файлы-для-изменения">3.5. Frontend — файлы для изменения</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Файл</th>
|
||||
<th>Изменение</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>pages/gantt/services/gantt.service.ts</code></td>
|
||||
<td>Новый метод <code>listAll(filters)</code>, <code>getFilters()</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pages/gantt/gantt.component.ts</code></td>
|
||||
<td>Подключить toolbar, парсинг URL-параметров</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pages/gantt/gantt.component.html</code></td>
|
||||
<td>Добавить toolbar, бейджи на карточках</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pages/gantt/components/gantt-card/</code></td>
|
||||
<td>Добавить бейдж пространства</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pages/gantt/components/create-chart-modal/</code></td>
|
||||
<td>Dropdown выбора пространства</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>core/models/</code></td>
|
||||
<td>Обновить модели: добавить <code>space</code> в GanttChart</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="4-обратная-совместимость">4. Обратная совместимость</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Endpoint</th>
|
||||
<th>Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>GET /api/v1/spaces/{id}/gantt-charts</code></td>
|
||||
<td>Сохранить (для детального view внутри пространства)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GET /api/v1/gantt-charts</code></td>
|
||||
<td><strong>Новый</strong> — глобальный список</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GET /api/v1/gantt-charts/filters</code></td>
|
||||
<td><strong>Новый</strong> — данные для dropdown’ов</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>POST /api/v1/spaces/{id}/gantt-charts</code></td>
|
||||
<td>Сохранить (создание в контексте пространства)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Все CRUD для sections/tasks</td>
|
||||
<td>Без изменений</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="5-критерии-приёмки">5. Критерии приёмки</h2>
|
||||
<ul>
|
||||
<li><input disabled="" type="checkbox"> Endpoint <code>GET /api/v1/gantt-charts</code> возвращает все доступные диаграммы</li>
|
||||
<li><input disabled="" type="checkbox"> Фильтр по <code>space_id</code> — показывает только диаграммы конкретного пространства</li>
|
||||
<li><input disabled="" type="checkbox"> Фильтр по <code>created_by</code> — показывает только диаграммы выбранного создателя</li>
|
||||
<li><input disabled="" type="checkbox"> Поиск <code>search</code> — ищет по title и description (case-insensitive)</li>
|
||||
<li><input disabled="" type="checkbox"> Dropdown фильтров заполняется реальными данными из <code>/filters</code></li>
|
||||
<li><input disabled="" type="checkbox"> Карточка диаграммы показывает бейдж пространства с цветом</li>
|
||||
<li><input disabled="" type="checkbox"> Форма создания предлагает только пространства с ролью ≥ editor</li>
|
||||
<li><input disabled="" type="checkbox"> Состояние фильтров сохраняется в URL (можно поделиться ссылкой)</li>
|
||||
<li><input disabled="" type="checkbox"> Старые endpoints (<code>/spaces/{id}/gantt-charts</code>) продолжают работать</li>
|
||||
<li><input disabled="" type="checkbox"> Swagger документация обновлена</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="6-порядок-реализации">6. Порядок реализации</h2>
|
||||
<ol>
|
||||
<li>Backend: DTO для новых запросов/ответов</li>
|
||||
<li>Backend: Repository — SQL-запрос с фильтрами + JOIN на spaces</li>
|
||||
<li>Backend: Service — <code>ListAll</code>, <code>GetFilters</code></li>
|
||||
<li>Backend: Handler — <code>ListAllGanttCharts</code>, <code>GetGanttFilters</code>, регистрация маршрутов</li>
|
||||
<li>Backend: Swagger — обновить документацию</li>
|
||||
<li>Frontend: Модели — обновить GanttChart, добавить Space в ответ</li>
|
||||
<li>Frontend: Service — <code>listAll()</code>, <code>getFilters()</code></li>
|
||||
<li>Frontend: Toolbar компонент — поиск + dropdown’ы</li>
|
||||
<li>Frontend: Карточка — бейдж пространства</li>
|
||||
<li>Frontend: Модальное окно создания — dropdown пространства</li>
|
||||
<li>Frontend: URL-параметры — синхронизация фильтров с router</li>
|
||||
<li>Тестирование</li>
|
||||
</ol>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,776 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera: Вехи (Milestones) на диаграмме Ганта — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Фильтры</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Все инструкции</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h6 class="mt-3">Теги</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/audit/">audit</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/leadera/">leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/security/">security</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/websocket/">websocket</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/realtime/">realtime</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/gantt/">gantt</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/api/">api</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/frontend/">frontend</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/requirements/">requirements</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/angular/">angular</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/go/">go</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ux/">ux</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/vps/">vps</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/reverse-proxy/">reverse-proxy</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/nginx/">nginx</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/devops/">devops</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/openclaw/">openclaw</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/opencode/">opencode</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/docker/">docker</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera: Вехи (Milestones) на диаграмме Ганта</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/leadera-milestones.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/leadera-milestones.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../../tags/angular/" class="tag">angular</a>
|
||||
|
||||
<a href="../../tags/go/" class="tag">go</a>
|
||||
|
||||
<a href="../../tags/api/" class="tag">api</a>
|
||||
|
||||
<a href="../../tags/ux/" class="tag">ux</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h2 id="бизнес-требования">Бизнес-требования</h2>
|
||||
<h3 id="br-1-сущность-веха-milestone">BR-1: Сущность «Веха» (Milestone)</h3>
|
||||
<p>Веха — это маркер на таймлайне диаграммы Ганта, обозначающий важную дату в проекте. В отличие от задач, веха не имеет длительности и привязана к конкретной дате.</p>
|
||||
<p><strong>Поля вехи:</strong></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Поле</th>
|
||||
<th>Тип</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>id</td>
|
||||
<td>UUID</td>
|
||||
<td>Идентификатор</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>gantt_chart_id</td>
|
||||
<td>UUID</td>
|
||||
<td>Принадлежность к диаграмме (не к секции!)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>title</td>
|
||||
<td>string</td>
|
||||
<td>Название вехи (обязательное, 1–255 символов)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>date</td>
|
||||
<td>date</td>
|
||||
<td>Дата вехи (без времени, обязательная)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>color</td>
|
||||
<td>string</td>
|
||||
<td>Цвет маркера (HEX, по умолчанию <code>#dc3545</code> — красный)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>created_by</td>
|
||||
<td>UUID</td>
|
||||
<td>Кто создал (не отображается в UI, хранится для аудита)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>updated_by</td>
|
||||
<td>UUID</td>
|
||||
<td>Кто обновил (не отображается в UI)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>created_at</td>
|
||||
<td>timestamp</td>
|
||||
<td>Дата создания</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>updated_at</td>
|
||||
<td>timestamp</td>
|
||||
<td>Дата обновления</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>deleted_at</td>
|
||||
<td>timestamp</td>
|
||||
<td>Soft delete</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Права доступа:</strong> Создание, редактирование и удаление вех доступно участникам с правами выше «только чтение» (аналогично задачам).</p>
|
||||
<p><strong>Особенности:</strong></p>
|
||||
<ul>
|
||||
<li>Вех может быть несколько в проекте</li>
|
||||
<li>Несколько вех могут иметь одну и ту же дату</li>
|
||||
<li>Вехи сортируются строго по дате (по возрастанию)</li>
|
||||
<li>Вехи не привязаны к секциям — они отображаются <strong>сквозь все секции</strong></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3 id="br-2-управление-вехами-модальное-окно">BR-2: Управление вехами (модальное окно)</h3>
|
||||
<p><strong>Вызов:</strong> Кнопка «Вехи» рядом с кнопкой «Создать секцию» на странице диаграммы.</p>
|
||||
<p><strong>Модальное окно содержит:</strong></p>
|
||||
<ul>
|
||||
<li>Заголовок: «Вехи проекта»</li>
|
||||
<li>Список всех вех текущей диаграммы (сортировка по дате по возрастанию)</li>
|
||||
<li>Каждая веха в списке показывает: цвет (индикатор), название, дату, кнопки «Редактировать» и «Удалить»</li>
|
||||
<li>Кнопка «Добавить веху»</li>
|
||||
</ul>
|
||||
<p><strong>Форма добавления/редактирования вехи:</strong></p>
|
||||
<ul>
|
||||
<li>Название — текстовый ввод (обязательное, 1–255 символов)</li>
|
||||
<li>Дата — datepicker (обязательное)</li>
|
||||
<li>Цвет — выбор цвета (по аналогии с палитрой цветов в задачах)</li>
|
||||
<li>По умолчанию цвет = красный (<code>#dc3545</code>)</li>
|
||||
</ul>
|
||||
<p><strong>Удаление:</strong> с подтверждением (confirm dialog).</p>
|
||||
<hr>
|
||||
<h3 id="br-3-отображение-вех-на-таймлайне">BR-3: Отображение вех на таймлайне</h3>
|
||||
<p><strong>Визуальное представление:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Маркер-флажок</strong> (flag marker) на шкале таймлайна</li>
|
||||
<li>От маркера вниз идёт <strong>пунктирная вертикальная линия</strong> через все секции (на всю высоту диаграммы)</li>
|
||||
<li>Цвет линии и маркера = цвет вехи</li>
|
||||
</ul>
|
||||
<p><strong>Позиционирование:</strong></p>
|
||||
<ul>
|
||||
<li>Веха ставится ровно на свою дату на шкале времени</li>
|
||||
<li>Если дата вехи выходит за рамки текущего диапазона задач — таймлайн <strong>расширяется</strong> (аналогично задачам)</li>
|
||||
<li>При изменении масштаба (days/weeks/months/quarters) позиция вехи пересчитывается</li>
|
||||
</ul>
|
||||
<p><strong>Tooltip при наведении:</strong></p>
|
||||
<ul>
|
||||
<li>При наведении на маркер/линию — показывается tooltip с названием вехи</li>
|
||||
<li>Если на одну дату приходится несколько вех — в tooltip показываются <strong>все</strong> вехи этой даты с их названиями (каждая с индикацией своего цвета)</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="технические-требования">Технические требования</h2>
|
||||
<h3 id="база-данных">База данных</h3>
|
||||
<h4 id="новая-таблица-gantt_milestones">Новая таблица: gantt_milestones</h4>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> gantt_milestones (
|
||||
</span></span><span style="display:flex;"><span> id UUID <span style="color:#66d9ef">PRIMARY</span> <span style="color:#66d9ef">KEY</span> <span style="color:#66d9ef">DEFAULT</span> gen_random_uuid(),
|
||||
</span></span><span style="display:flex;"><span> gantt_chart_id UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> gantt_charts(id) <span style="color:#66d9ef">ON</span> <span style="color:#66d9ef">DELETE</span> <span style="color:#66d9ef">CASCADE</span>,
|
||||
</span></span><span style="display:flex;"><span> title VARCHAR(<span style="color:#ae81ff">255</span>) <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> date DATE <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> color VARCHAR(<span style="color:#ae81ff">20</span>) <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">DEFAULT</span> <span style="color:#e6db74">'#dc3545'</span>,
|
||||
</span></span><span style="display:flex;"><span> created_by UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> users(id),
|
||||
</span></span><span style="display:flex;"><span> updated_by UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> users(id),
|
||||
</span></span><span style="display:flex;"><span> created_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span> <span style="color:#66d9ef">DEFAULT</span> NOW(),
|
||||
</span></span><span style="display:flex;"><span> updated_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span> <span style="color:#66d9ef">DEFAULT</span> NOW(),
|
||||
</span></span><span style="display:flex;"><span> deleted_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span>
|
||||
</span></span><span style="display:flex;"><span>);
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_milestones_chart <span style="color:#66d9ef">ON</span> gantt_milestones(gantt_chart_id);
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_milestones_date <span style="color:#66d9ef">ON</span> gantt_milestones(date);
|
||||
</span></span></code></pre></div><hr>
|
||||
<h3 id="backend-go-api">Backend (Go API)</h3>
|
||||
<h4 id="этап-1-миграция--домен">Этап 1: Миграция + Домен</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Добавь в проект новую таблицу <code>gantt_milestones</code>.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Создай миграцию в <code>cmd/migrate/</code>:</p>
|
||||
<ul>
|
||||
<li>Новая таблица <code>gantt_milestones</code> с полями: id (UUID PK), gantt_chart_id (UUID FK → gantt_charts, ON DELETE CASCADE), title (VARCHAR 255 NOT NULL), date (DATE NOT NULL), color (VARCHAR 20 NOT NULL DEFAULT ‘#dc3545’), created_by (UUID FK → users), updated_by (UUID FK → users), created_at, updated_at, deleted_at. Индексы по gantt_chart_id и date.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Создай доменную модель <code>GanttMilestone</code> в <code>internal/domain/gantt.go</code>:</p>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GanttMilestone</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ID</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"id"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">GanttChartID</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"gantt_chart_id"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"title"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"date"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"color"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedBy</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"created_by"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedBy</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"updated_by"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"created_at"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"updated_at"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">DeletedAt</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"deleted_at,omitempty"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div></blockquote>
|
||||
<h4 id="этап-2-crud-для-ganttmilestones">Этап 2: CRUD для GanttMilestones</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Реализуй полный CRUD для сущности <code>GanttMilestone</code> по аналогии с существующими handlers/services/repositories в проекте.</p>
|
||||
<p><strong>Роуты:</strong></p>
|
||||
<ul>
|
||||
<li><code>GET /api/gantt-charts/:chartId/milestones</code> — список вех диаграммы</li>
|
||||
<li><code>POST /api/gantt-charts/:chartId/milestones</code> — создать веху</li>
|
||||
<li><code>PUT /api/gantt-charts/:chartId/milestones/:milestoneId</code> — обновить веху</li>
|
||||
<li><code>DELETE /api/gantt-charts/:chartId/milestones/:milestoneId</code> — удалить веху (soft delete)</li>
|
||||
</ul>
|
||||
<p><strong>Repository:</strong> <code>internal/repository/gantt_milestone_repository.go</code></p>
|
||||
<ul>
|
||||
<li><code>GetByChartID(ctx, chartID) ([]domain.GanttMilestone, error)</code> — все неудалённые вехи диаграммы, отсортированные по date ASC</li>
|
||||
<li><code>GetByID(ctx, id) (*domain.GanttMilestone, error)</code></li>
|
||||
<li><code>Create(ctx, milestone) error</code></li>
|
||||
<li><code>Update(ctx, milestone) error</code></li>
|
||||
<li><code>Delete(ctx, id) error</code> — soft delete (set deleted_at)</li>
|
||||
<li>Все запросы — raw SQL через pgx, как в остальных репозиториях проекта</li>
|
||||
</ul>
|
||||
<p><strong>Service:</strong> <code>internal/service/gantt_milestone_service.go</code></p>
|
||||
<ul>
|
||||
<li>Валидация: title (1–255 символов), date (обязательное), color (валидный HEX, default ‘#dc3545’)</li>
|
||||
<li>Проверка прав доступа: пользователь должен иметь права выше «только чтение» в пространстве (space) диаграммы</li>
|
||||
<li>Проверка что chartId существует и не архивирован</li>
|
||||
</ul>
|
||||
<p><strong>Handler:</strong> <code>internal/handler/gantt_milestone_handler.go</code></p>
|
||||
<ul>
|
||||
<li>Swagger-аннотации для всех эндпоинтов</li>
|
||||
<li>Валидация входных данных</li>
|
||||
<li>Ошибки в формате проекта</li>
|
||||
</ul>
|
||||
<p><strong>DTO:</strong> <code>internal/dto/gantt/milestones_dto.go</code></p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">CreateGanttMilestoneRequest</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"title" binding:"required,min=1,max=255"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"date" binding:"required"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"color" binding:"omitempty,max=20"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">UpdateGanttMilestoneRequest</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"title" binding:"omitempty,min=1,max=255"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"date" binding:"omitempty"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"color" binding:"omitempty,max=20"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GanttMilestoneResponse</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ID</span> <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"id"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">GanttChartID</span> <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"gantt_chart_id"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"title"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"date"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"color"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedByUser</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CreatedByUpdatedUser</span> <span style="color:#e6db74">`json:"created_by_user"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedByUser</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CreatedByUpdatedUser</span> <span style="color:#e6db74">`json:"updated_by_user"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"created_at"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"updated_at"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GanttMilestonesListResponse</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Milestones</span> []<span style="color:#a6e22e">GanttMilestoneResponse</span> <span style="color:#e6db74">`json:"milestones"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><p><strong>Регистрация роутов:</strong> Добавь группу milestone-роутов в роутер рядом с существующими роутами секций/задач. Проверка прав через существующий middleware.</p>
|
||||
</blockquote>
|
||||
<h4 id="этап-3-включение-вех-в-ответ-диаграммы">Этап 3: Включение вех в ответ диаграммы</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>При запросе данных диаграммы Ганта (полная загрузка) включай связанные вехи в ответ.</p>
|
||||
<ol>
|
||||
<li>Добавь эндпоинт или расширь существующий <code>GET /api/gantt-charts/:chartId</code> — включить поле <code>milestones</code> с массивом <code>GanttMilestoneResponse</code></li>
|
||||
<li>Вехи с <code>deleted_at IS NOT NULL</code> не возвращаются</li>
|
||||
<li>Вехи сортируются по <code>date</code> ASC</li>
|
||||
<li>Вехи загружаются вместе с данными диаграммы (tasks, sections) — один запрос при открытии Ганта</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<h3 id="frontend-angular">Frontend (Angular)</h3>
|
||||
<h4 id="этап-fe-1-модели--сервис">Этап FE-1: Модели + Сервис</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Добавь в проект поддержку вех (milestones) на диаграмме Ганта.</p>
|
||||
<ol>
|
||||
<li>В <code>pages/gantt/models/</code> (или соответствующем файле моделей) добавь:</li>
|
||||
</ol>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttMilestone</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">gantt_chart_id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">color</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_by_user</span><span style="color:#f92672">?:</span> { <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>; <span style="color:#a6e22e">full_name</span>: <span style="color:#66d9ef">string</span> };
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_by_user</span><span style="color:#f92672">?:</span> { <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>; <span style="color:#a6e22e">full_name</span>: <span style="color:#66d9ef">string</span> };
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_at</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_at</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttMilestoneCreateRequest</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">color?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttMilestoneUpdateRequest</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">color?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><ol start="2">
|
||||
<li>
|
||||
<p>Добавь <code>milestones: GanttMilestone[]</code> в интерфейс/модель полной загрузки данных диаграммы</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Создай сервис <code>pages/gantt/services/gantt-milestone.service.ts</code>:</p>
|
||||
</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>Inject HttpClient</li>
|
||||
<li>Методы:
|
||||
<ul>
|
||||
<li><code>getMilestones(chartId: string): Observable<GanttMilestone[]></code></li>
|
||||
<li><code>createMilestone(chartId: string, data: GanttMilestoneCreateRequest): Observable<GanttMilestone></code></li>
|
||||
<li><code>updateMilestone(chartId: string, milestoneId: string, data: GanttMilestoneUpdateRequest): Observable<GanttMilestone></code></li>
|
||||
<li><code>deleteMilestone(chartId: string, milestoneId: string): Observable<void></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Все методы возвращают Observable с типизированными ответами (по аналогии с GanttChartService)</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-2-модальное-окно-управления-вехами">Этап FE-2: Модальное окно управления вехами</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Создай компонент модального окна для управления вехами диаграммы Ганта.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Создай компонент <code>pages/gantt/components/milestones-modal/</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Кнопка вызова:</strong> Добавь кнопку «Вехи» рядом с кнопкой «Создать секцию» в toolbar диаграммы. По клику — открытие модального окна через ng-bootstrap NgbModal.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Модальное окно</strong> (ng-bootstrap modal):</p>
|
||||
<ul>
|
||||
<li>Заголовок: «Вехи проекта»</li>
|
||||
<li>Список вех (сортировка по date ASC):
|
||||
<ul>
|
||||
<li>Каждый элемент: цветной кружок (color), название, дата (форматированная), кнопки ✏️ (редактировать) и 🗑️ (удалить с confirm)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Кнопка «Добавить веху» внизу списка</li>
|
||||
<li>Если вех нет — сообщение «Вех пока нет. Добавьте первую!»</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Форма добавления/редактирования</strong> (inline в модальном окне):</p>
|
||||
<ul>
|
||||
<li>Название — текстовый ввод (обязательное)</li>
|
||||
<li>Дата — ng-bootstrap datepicker (обязательное)</li>
|
||||
<li>Цвет — палитра цветов (использовать тот же компонент/подход, что и для задач)</li>
|
||||
<li>По умолчанию цвет = <code>#dc3545</code> (красный)</li>
|
||||
<li>Кнопки «Сохранить» и «Отмена»</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>При сохранении — вызов соответствующего метода сервиса (create/update)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>При удалении — confirm dialog → вызов deleteMilestone</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>После любого изменения — обновить список вех и обновить данные на канвасе (через сервис/GanttStore)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Инжектировать GanttMilestoneService и NgbModal</p>
|
||||
</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-3-отображение-вех-на-таймлайне-konva-canvas">Этап FE-3: Отображение вех на таймлайне (Konva canvas)</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Отобрази вехи (milestones) на canvas-диаграмме Ганта.</p>
|
||||
<p>Проект использует Konva.js для рендеринга.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>При загрузке данных диаграммы — извлечь массив <code>milestones</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Для каждой вехи нарисовать на canvas:</p>
|
||||
<ul>
|
||||
<li><strong>Маркер-флажок</strong> (flag marker) на позиции даты вехи на шкале времени
|
||||
<ul>
|
||||
<li>Стиль: маленький треугольник или флаг на верхней границе таймлайна</li>
|
||||
<li>Цвет = milestone.color</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Пунктирная вертикальная линия</strong> от маркера вниз через все секции (на всю высоту области секций)
|
||||
<ul>
|
||||
<li>Стиль: Konva.Line, dash: [5, 5], strokeWidth: 1.5, color = milestone.color, opacity: 0.7</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Позиционирование:</strong></p>
|
||||
<ul>
|
||||
<li>Рассчитать X-позицию по дате вехи с использованием текущей шкалы времени (как для задач)</li>
|
||||
<li>Если дата вехи выходит за текущий диапазон — расширить диапазон (влияет на общую ширину таймлайна)</li>
|
||||
<li>При изменении масштаба (days/weeks/months/quarters) — пересчитать позиции всех вех</li>
|
||||
<li>При скролле — вехи скроллятся вместе с задачами</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Tooltip при наведении:</strong></p>
|
||||
<ul>
|
||||
<li>При наведении на маркер или пунктирную линию — показать Konva.Tooltip или HTML tooltip:
|
||||
<ul>
|
||||
<li>Если веха одна на эту дату — показать название</li>
|
||||
<li>Если несколько вех на одну дату — показать список: цветной кружок + название для каждой</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Z-index:</strong></p>
|
||||
<ul>
|
||||
<li>Линии вех рисуются <strong>поверх</strong> фона секций, но <strong>под</strong> задачами</li>
|
||||
<li>Маркеры-флажки — поверх всего (самый высокий z-index)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Реактивность:</strong></p>
|
||||
<ul>
|
||||
<li>При добавлении/удалении/редактировании вехи — перерисовать только вехи (без полного ре рендера задач)</li>
|
||||
<li>Подписаться на изменения массива milestones через сервис/store</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<h2 id="порядок-реализации">Порядок реализации</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Этап</th>
|
||||
<th>Сторона</th>
|
||||
<th>Зависимости</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>Миграция + Домен</td>
|
||||
<td>Backend</td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>CRUD вех</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>Включение вех в ответ диаграммы</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>Модели + Сервис</td>
|
||||
<td>Frontend</td>
|
||||
<td>— (параллельно с backend)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>Модальное окно управления вехами</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 4 + backend 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td>Отображение вех на таймлайне</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 4 + backend 3</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,839 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera: Ограничения секций в диаграмме Ганта — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Фильтры</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Все инструкции</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h6 class="mt-3">Теги</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/audit/">audit</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/leadera/">leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/security/">security</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/websocket/">websocket</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/realtime/">realtime</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/gantt/">gantt</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/api/">api</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/frontend/">frontend</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/requirements/">requirements</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/angular/">angular</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/go/">go</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ux/">ux</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/vps/">vps</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/reverse-proxy/">reverse-proxy</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/nginx/">nginx</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/devops/">devops</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/openclaw/">openclaw</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/opencode/">opencode</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/docker/">docker</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera: Ограничения секций в диаграмме Ганта</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/leadera-section-constraints.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/leadera-section-constraints.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../../tags/angular/" class="tag">angular</a>
|
||||
|
||||
<a href="../../tags/go/" class="tag">go</a>
|
||||
|
||||
<a href="../../tags/api/" class="tag">api</a>
|
||||
|
||||
<a href="../../tags/ux/" class="tag">ux</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h2 id="бизнес-требования">Бизнес-требования</h2>
|
||||
<h3 id="br-1-лимит-одновременных-задач-concurrency-limit">BR-1: Лимит одновременных задач (Concurrency Limit)</h3>
|
||||
<p>У каждой секции появляется параметр <strong>max_concurrent_tasks</strong> (целое число ≥ 0, по умолчанию 0).</p>
|
||||
<p><strong>Поведение:</strong></p>
|
||||
<ul>
|
||||
<li>Если <code>max_concurrent_tasks = 3</code> — в любой данный день в этой секции не может быть больше 3 активных задач одновременно</li>
|
||||
<li>Если <code>max_concurrent_tasks = 0</code> — ограничение отключено, лимита нет</li>
|
||||
<li>Пересечение считается по дате: задача занимает дни с <code>start_date</code> по <code>end_date</code> включительно. Две задачи «пересекаются», если у них есть хотя бы один общий день</li>
|
||||
<li>Ограничение работает <strong>только в рамках одной секции</strong>. Задачи из разных секций не влияют друг на друга</li>
|
||||
</ul>
|
||||
<p><strong>Срабатывание при:</strong></p>
|
||||
<ol>
|
||||
<li>Создании задачи — если добавление нарушает лимит → Alert</li>
|
||||
<li>Перетаскивании задачи (drag) — если новая позиция нарушает лимит → Alert</li>
|
||||
<li>Растягивании задачи (resize) — если новый размер нарушает лимит → Alert</li>
|
||||
<li>Редактировании задачи (форма) — если изменённые даты нарушают лимит → Alert</li>
|
||||
</ol>
|
||||
<p><strong>Важно:</strong> Действие <strong>НЕ отменяется</strong>. Задача создаётся/перемещается в любом случае, но пользователь видит предупреждение.</p>
|
||||
<p><strong>Текст Alert:</strong> «В секции “[название секции]” на [дата/диапазон] превышен лимит одновременных задач (максимум: N)»</p>
|
||||
<hr>
|
||||
<h3 id="br-2-блоки-секций-section-blocks">BR-2: Блоки секций (Section Blocks)</h3>
|
||||
<p>У каждой секции появляется набор <strong>блоков</strong> — особый вид записей, отображаемых на таймлайне как заштрихованные области.</p>
|
||||
<p><strong>Сущность GanttSectionBlock:</strong></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Поле</th>
|
||||
<th>Тип</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>id</td>
|
||||
<td>UUID</td>
|
||||
<td>Идентификатор</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>section_id</td>
|
||||
<td>UUID</td>
|
||||
<td>Принадлежность к секции</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>title</td>
|
||||
<td>string</td>
|
||||
<td>Название (например: «Отпуск», «Командировка»)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>start_date</td>
|
||||
<td>date</td>
|
||||
<td>Начало интервала (минимум 1 день)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>end_date</td>
|
||||
<td>date</td>
|
||||
<td>Конец интервала</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>block_concurrency</td>
|
||||
<td>boolean</td>
|
||||
<td>Запрещать ли пересечение обычных задач с данным блоком</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Валидация:</strong></p>
|
||||
<ul>
|
||||
<li><code>end_date</code> не может быть раньше <code>start_date</code></li>
|
||||
<li>Все поля обязательны</li>
|
||||
<li>Блок может быть в прошлом</li>
|
||||
</ul>
|
||||
<p><strong>Визуальное отображение на Ганте:</strong></p>
|
||||
<ul>
|
||||
<li>Заштрихованная область на <strong>всю высоту секции</strong> (включая все строки задач в секции)</li>
|
||||
<li>Название блока отображается <strong>поверх штриховки с подложкой</strong> (для читаемости)</li>
|
||||
</ul>
|
||||
<p><strong>Поведение block_concurrency:</strong></p>
|
||||
<ul>
|
||||
<li>Если <code>block_concurrency = true</code> — при пересечении задачи с данным блоком показывается Alert (аналогично BR-1), но задача <strong>всё равно создаётся/перемещается</strong></li>
|
||||
<li>Если <code>block_concurrency = false</code> — блок отображается визуально, но не влияет на задачи</li>
|
||||
</ul>
|
||||
<p><strong>Срабатывание при:</strong></p>
|
||||
<ol>
|
||||
<li>Создании задачи — пересекает блок с <code>block_concurrency = true</code> → Alert</li>
|
||||
<li>Перетаскивании задачи (drag) → Alert</li>
|
||||
<li>Растягивании задачи (resize) → Alert</li>
|
||||
<li>Редактировании задачи (форма) → Alert</li>
|
||||
</ol>
|
||||
<p><strong>Текст Alert:</strong> «В секции “[название секции]” задача пересекает блок “[название блока]” ([дата] — [дата])»</p>
|
||||
<hr>
|
||||
<h3 id="br-3-система-alert-ов">BR-3: Система Alert-ов</h3>
|
||||
<p>Все предупреждения (от BR-1 и BR-2) выводятся в <strong>единую область</strong> над диаграммой Ганта.</p>
|
||||
<p><strong>Поведение:</strong></p>
|
||||
<ul>
|
||||
<li>Alert-ы — Bootstrap Alert (warning), <strong>постоянные</strong></li>
|
||||
<li>Пользователь <strong>НЕ может</strong> закрыть Alert вручную</li>
|
||||
<li>Alert исчезает <strong>автоматически</strong>, когда текущая дата прошла дату конфликта (конфликт больше не актуален)</li>
|
||||
<li>Проверка конфликтов — <strong>только на фронтенде</strong> (на основе загруженных данных: задачи секции, блоки, max_concurrent_tasks)</li>
|
||||
<li>Если Alert-ов много — область <strong>сворачиваемая</strong> с счётчиком в бейдже (например: «⚠️ 3 предупреждения»)</li>
|
||||
<li>По умолчанию — развёрнуто</li>
|
||||
<li>Сворачивание/разворачивание — клик по заголовку области</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3 id="br-4-настройки-секции">BR-4: Настройки секции</h3>
|
||||
<ul>
|
||||
<li>Настройки совмещены с <strong>существующим модальным окном редактирования секции</strong> (не отдельное окно)</li>
|
||||
</ul>
|
||||
<p><strong>Окно создания секции</strong> — добавляется:</p>
|
||||
<ul>
|
||||
<li>Поле «Макс. одновременных задач» (числовой ввод, 0 = без лимита, по умолчанию 0)</li>
|
||||
</ul>
|
||||
<p><strong>Окно редактирования секции</strong> — добавляется:</p>
|
||||
<ul>
|
||||
<li>Поле «Макс. одновременных задач» (числовой ввод, 0 = без лимита)</li>
|
||||
<li>Список блоков (таблица/список с CRUD: добавить, редактировать, удалить)
|
||||
<ul>
|
||||
<li><strong>Сортировка:</strong> по <code>start_date</code> по убыванию (будущее сверху)</li>
|
||||
<li>Каждый блок — форма: title, start_date, end_date, block_concurrency (чекбокс: «Запретить пересечение задач»)</li>
|
||||
<li>Валидация: end_date >= start_date, все поля заполнены</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3 id="br-5-наследование">BR-5: Наследование</h3>
|
||||
<ul>
|
||||
<li>При появлении функционала копирования/дублирования диаграммы — настройки секций (лимит + блоки) наследуются</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="технические-требования">Технические требования</h2>
|
||||
<h3 id="база-данных">База данных</h3>
|
||||
<h4 id="новая-таблица-gantt_section_blocks">Новая таблица: gantt_section_blocks</h4>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> gantt_section_blocks (
|
||||
</span></span><span style="display:flex;"><span> id UUID <span style="color:#66d9ef">PRIMARY</span> <span style="color:#66d9ef">KEY</span> <span style="color:#66d9ef">DEFAULT</span> gen_random_uuid(),
|
||||
</span></span><span style="display:flex;"><span> section_id UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> gantt_sections(id) <span style="color:#66d9ef">ON</span> <span style="color:#66d9ef">DELETE</span> <span style="color:#66d9ef">CASCADE</span>,
|
||||
</span></span><span style="display:flex;"><span> title VARCHAR(<span style="color:#ae81ff">255</span>) <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> start_date DATE <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> end_date DATE <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> block_concurrency BOOLEAN <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">DEFAULT</span> <span style="color:#66d9ef">false</span>,
|
||||
</span></span><span style="display:flex;"><span> created_by UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> users(id),
|
||||
</span></span><span style="display:flex;"><span> updated_by UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> users(id),
|
||||
</span></span><span style="display:flex;"><span> created_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span> <span style="color:#66d9ef">DEFAULT</span> NOW(),
|
||||
</span></span><span style="display:flex;"><span> updated_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span> <span style="color:#66d9ef">DEFAULT</span> NOW(),
|
||||
</span></span><span style="display:flex;"><span> deleted_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">CONSTRAINT</span> chk_block_dates <span style="color:#66d9ef">CHECK</span> (end_date <span style="color:#f92672">>=</span> start_date)
|
||||
</span></span><span style="display:flex;"><span>);
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_section_blocks_section <span style="color:#66d9ef">ON</span> gantt_section_blocks(section_id);
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_section_blocks_dates <span style="color:#66d9ef">ON</span> gantt_section_blocks(start_date, end_date);
|
||||
</span></span></code></pre></div><h4 id="изменение-таблицы-gantt_sections">Изменение таблицы: gantt_sections</h4>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> gantt_sections <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> max_concurrent_tasks INTEGER <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">DEFAULT</span> <span style="color:#ae81ff">0</span>;
|
||||
</span></span></code></pre></div><hr>
|
||||
<h3 id="backend-go-api">Backend (Go API)</h3>
|
||||
<h4 id="этап-1-миграция--домен">Этап 1: Миграция + Домен</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Добавь в проект новую таблицу <code>gantt_section_blocks</code> и колонку <code>max_concurrent_tasks</code> в <code>gantt_sections</code>.</p>
|
||||
<ol>
|
||||
<li>Создай миграцию в <code>cmd/migrate/</code>:
|
||||
<ul>
|
||||
<li>Новая таблица <code>gantt_section_blocks</code> с полями: id (UUID PK), section_id (UUID FK → gantt_sections, ON DELETE CASCADE), title (VARCHAR 255 NOT NULL), start_date (DATE NOT NULL), end_date (DATE NOT NULL), block_concurrency (BOOLEAN DEFAULT false), created_by (UUID FK → users), updated_by (UUID FK → users), created_at, updated_at, deleted_at. CHECK constraint: end_date >= start_date. Индексы по section_id и (start_date, end_date).</li>
|
||||
<li>ALTER TABLE gantt_sections ADD COLUMN max_concurrent_tasks INTEGER NOT NULL DEFAULT 0.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Создай доменную модель <code>GanttSectionBlock</code> в <code>internal/domain/gantt.go</code> со всеми полями таблицы.</li>
|
||||
<li>Добавь поле <code>MaxConcurrentTasks int</code> в структуру <code>GanttSection</code>.</li>
|
||||
<li>Обнови маппинг в relevant repository файлах, чтобы новое поле <code>max_concurrent_tasks</code> читалось/писалось.</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-2-crud-для-ganttsectionblocks">Этап 2: CRUD для GanttSectionBlocks</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Реализуй полный CRUD для сущности <code>GanttSectionBlock</code> по аналогии с существующими handlers/services/repositories в проекте.</p>
|
||||
<p><strong>Роуты (внутри группы секции):</strong></p>
|
||||
<ul>
|
||||
<li><code>GET /api/gantt-charts/:chartId/sections/:sectionId/blocks</code> — список блоков секции</li>
|
||||
<li><code>POST /api/gantt-charts/:chartId/sections/:sectionId/blocks</code> — создать блок</li>
|
||||
<li><code>PUT /api/gantt-charts/:chartId/sections/:sectionId/blocks/:blockId</code> — обновить блок</li>
|
||||
<li><code>DELETE /api/gantt-charts/:chartId/sections/:sectionId/blocks/:blockId</code> — удалить блок (soft delete)</li>
|
||||
</ul>
|
||||
<p><strong>Repository:</strong> <code>internal/repository/gantt_section_block_repository.go</code></p>
|
||||
<ul>
|
||||
<li><code>GetBySectionID(ctx, sectionID) ([]domain.GanttSectionBlock, error)</code></li>
|
||||
<li><code>GetByID(ctx, id) (*domain.GanttSectionBlock, error)</code></li>
|
||||
<li><code>Create(ctx, block) error</code></li>
|
||||
<li><code>Update(ctx, block) error</code></li>
|
||||
<li><code>Delete(ctx, id) error</code> — soft delete (set deleted_at)</li>
|
||||
<li>Все запросы — raw SQL через pgx, как в остальных репозиториях проекта</li>
|
||||
</ul>
|
||||
<p><strong>Service:</strong> <code>internal/service/gantt_section_block_service.go</code></p>
|
||||
<ul>
|
||||
<li>Бизнес-логика: валидация dates (end >= start), проверка прав доступа к секции</li>
|
||||
<li>Inject repository</li>
|
||||
</ul>
|
||||
<p><strong>Handler:</strong> <code>internal/handler/gantt_section_block_handler.go</code></p>
|
||||
<ul>
|
||||
<li>Swagger-аннотации для всех эндпоинтов</li>
|
||||
<li>Валидация входных данных</li>
|
||||
<li>Ошибки в формате проекта (GanttErrorResponse)</li>
|
||||
</ul>
|
||||
<p><strong>Модели запросов:</strong></p>
|
||||
<ul>
|
||||
<li>Create: title (required), start_date (required), end_date (required), block_concurrency (bool, default false)</li>
|
||||
<li>Update: title, start_date, end_date, block_concurrency — все опциональны</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
<h4 id="этап-3-обновление-настроек-секции">Этап 3: Обновление настроек секции</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Обнови CRUD секций (gantt_sections) для поддержки нового поля <code>max_concurrent_tasks</code>.</p>
|
||||
<ol>
|
||||
<li>Добавь <code>max_concurrent_tasks</code> в <code>GanttSectionCreateRequest</code> (опциональное, int ≥ 0, default 0)</li>
|
||||
<li>Добавь <code>max_concurrent_tasks</code> в <code>GanttSectionUpdateRequest</code> (опциональное, int ≥ 0)</li>
|
||||
<li>Обнови SQL-запросы в repository: INSERT и UPDATE должны включать <code>max_concurrent_tasks</code></li>
|
||||
<li>При GET секции (или списке секций) — возвращать <code>max_concurrent_tasks</code> в ответе</li>
|
||||
<li>Валидация: значение должно быть ≥ 0</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-4-включение-блоков-в-ответ-секций">Этап 4: Включение блоков в ответ секций</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>При запросе списка секций или одной секции включай связанные блоки в ответ.</p>
|
||||
<ol>
|
||||
<li>Обнови <code>GET /api/gantt-charts/:chartId/sections/:sectionId</code> — добавить поле <code>blocks</code> с массивом GanttSectionBlock</li>
|
||||
<li>Обнови <code>GET /api/gantt-charts/:chartId/sections</code> — добавить поле <code>blocks</code> для каждой секции</li>
|
||||
<li>Блоки с <code>deleted_at IS NOT NULL</code> не возвращаются</li>
|
||||
<li>Блоки сортируются по start_date по убыванию (будущее сверху)</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<h3 id="frontend-angular">Frontend (Angular)</h3>
|
||||
<h4 id="этап-fe-1-модели--сервис">Этап FE-1: Модели + Сервис</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Добавь в проект поддержку ограничений секций Ганта.</p>
|
||||
<ol>
|
||||
<li>В <code>pages/gantt/models/gantt-chart.model.ts</code> добавь:</li>
|
||||
</ol>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttSectionBlock</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">section_id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">start_date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_concurrency</span>: <span style="color:#66d9ef">boolean</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_by_user</span>: <span style="color:#66d9ef">UserInfo</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_by_user</span>: <span style="color:#66d9ef">UserInfo</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_at</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_at</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttSectionBlockCreateRequest</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">start_date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_concurrency?</span>: <span style="color:#66d9ef">boolean</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttSectionBlockUpdateRequest</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">start_date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_concurrency?</span>: <span style="color:#66d9ef">boolean</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ConflictCheck</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">has_conflicts</span>: <span style="color:#66d9ef">boolean</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">conflicts</span>: <span style="color:#66d9ef">ConflictItem</span>[];
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ConflictItem</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'concurrency'</span> <span style="color:#f92672">|</span> <span style="color:#e6db74">'block'</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">section_id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">section_title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_title?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">start_date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">message</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">dates?</span>: <span style="color:#66d9ef">string</span>[];
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><ol start="2">
|
||||
<li>
|
||||
<p>Добавь <code>max_concurrent_tasks: number</code> в интерфейс <code>GanttSection</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Добавь <code>blocks: GanttSectionBlock[]</code> в интерфейс <code>GanttSection</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Добавь <code>max_concurrent_tasks?: number</code> в <code>GanttSectionCreateRequest</code> и <code>GanttSectionUpdateRequest</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Создай сервис <code>pages/gantt/services/gantt-section-block.service.ts</code>:</p>
|
||||
</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>Inject HttpClient</li>
|
||||
<li>Методы: getBlocks(chartId, sectionId), createBlock(chartId, sectionId, data), updateBlock(chartId, sectionId, blockId, data), deleteBlock(chartId, sectionId, blockId)</li>
|
||||
<li>Все методы возвращают Observable с типизированными ответами (по аналогии с GanttChartService)</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-2-интеграция-в-модальные-окна-секции">Этап FE-2: Интеграция в модальные окна секции</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Интегрируй настройки ограничений в существующие модальные окна создания и редактирования секции Ганта.</p>
|
||||
<ol>
|
||||
<li>Найди существующие компоненты модальных окон секции (создание/редактирование).</li>
|
||||
</ol>
|
||||
<p><strong>Окно создания секции</strong> — добавить:</p>
|
||||
<ul>
|
||||
<li>Числовой ввод: «Максимум одновременных задач»</li>
|
||||
<li>Подсказка: «0 = без ограничений»</li>
|
||||
<li>Валидация: число ≥ 0</li>
|
||||
<li>Значение по умолчанию: 0</li>
|
||||
<li>Отправляется в <code>GanttSectionCreateRequest</code> как <code>max_concurrent_tasks</code></li>
|
||||
</ul>
|
||||
<p><strong>Окно редактирования секции</strong> — добавить:</p>
|
||||
<ul>
|
||||
<li>Числовой ввод: «Максимум одновременных задач» (аналогично созданию)</li>
|
||||
<li>Секция «Блоки (запретные интервалы)»:
|
||||
<ul>
|
||||
<li>Таблица/список существующих блоков с колонками: Название, Начало, Конец, Блокировка (чекбокс/иконка), Действия (редактировать/удалить)</li>
|
||||
<li><strong>Сортировка:</strong> по <code>start_date</code> по убыванию (будущее сверху)</li>
|
||||
<li>Кнопка «Добавить блок» — inline-форма:
|
||||
<ul>
|
||||
<li>title (текстовый ввод, обязательный)</li>
|
||||
<li>start_date (datepicker, обязательный)</li>
|
||||
<li>end_date (datepicker, обязательный)</li>
|
||||
<li>block_concurrency (чекбокс: «Запретить пересечение задач»)</li>
|
||||
<li>Валидация: end_date >= start_date, все поля заполнены</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Редактирование — inline или через ту же форму</li>
|
||||
<li>Удаление — с подтверждением</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<ol start="2">
|
||||
<li>При открытии окна редактирования — загрузить текущие настройки секции + блоки</li>
|
||||
<li>При сохранении — обновить max_concurrent_tasks и сохранить изменения блоков</li>
|
||||
<li>Инжектировать GanttChartService и GanttSectionBlockService</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-3-визуализация-блоков-на-ганте">Этап FE-3: Визуализация блоков на Ганте</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Отобрази блоки секций (GanttSectionBlock) на canvas-диаграмме Ганта.</p>
|
||||
<p>Проект использует Konva.js для рендеринга. Основной компонент: <code>gantt-chart-canvas</code>.</p>
|
||||
<ol>
|
||||
<li>При загрузке данных диаграммы — извлечь блоки из каждой секции</li>
|
||||
<li>В рендерере секций (canvas/timeline-renderer.ts или аналогичный) — для каждого блока:
|
||||
<ul>
|
||||
<li>Нарисовать заштрихованную область (diagonal hatch pattern) на <strong>всю высоту секции</strong> (все строки задач в этой секции)</li>
|
||||
<li>Область ограничена по горизонтали: от start_date до end_date (в пикселях по шкале времени)</li>
|
||||
<li>Цвет штриховки: полупрозрачный серый или configurable</li>
|
||||
<li>Поверх штриховки — текст с названием блока (block.title) на подложке (прямоугольник с фоном, чтобы текст читался)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Блоки рендерятся <strong>до</strong> задач (z-index ниже), чтобы задачи были поверх</li>
|
||||
<li>Блоки с <code>block_concurrency = true</code> и <code>false</code> визуально не отличаются (штриховка одинаковая)</li>
|
||||
<li>При скролле/зуме — блоки пересчитываются как обычные элементы таймлайна</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-4-система-alert-ов">Этап FE-4: Система Alert-ов</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Создай компонент для отображения Alert-ов о конфликтах на диаграмме Ганта.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Создай компонент <code>pages/gantt/components/gantt-conflict-alerts/</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Компонент размещается <strong>над</strong> canvas-диаграммой Ганта</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Входные данные: массив <code>ConflictItem[]</code> (из модели)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Визуал:</p>
|
||||
<ul>
|
||||
<li>Bootstrap Alert (warning) для каждого конфликта</li>
|
||||
<li>Каждый Alert показывает message из ConflictItem</li>
|
||||
<li><strong>Без кнопки закрытия</strong> — Alert нельзя закрыть вручную</li>
|
||||
<li>Alert исчезает автоматически, когда текущая дата прошла дату конфликта</li>
|
||||
<li>Область <strong>сворачиваемая</strong>: заголовок «⚠️ Предупреждения (N)» — клик сворачивает/разворачивает список</li>
|
||||
<li>По умолчанию — развёрнуто</li>
|
||||
<li>При 0 конфликтов — область скрыта</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Состояние хранится в сервисе или в компоненте диаграммы:</p>
|
||||
<ul>
|
||||
<li>Добавление нового конфликта → area auto-expands</li>
|
||||
<li>Alert удаляется из массива <strong>автоматически</strong>, когда текущая дата прошла дату конфликта</li>
|
||||
<li>Проверка актуальности конфликтов — при каждой отрисовке / изменении данных</li>
|
||||
<li>Не использовать toast — Alert-ы постоянные</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-5-проверка-конфликтов-при-операциях-с-задачами">Этап FE-5: Проверка конфликтов при операциях с задачами</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Интегрируй проверку конфликтов во все операции с задачами на диаграмме Ганта.</p>
|
||||
<p>Проект использует interaction-handler.ts для drag/resize и формы для создания/редактирования.</p>
|
||||
<p><strong>Важно:</strong> Проверка конфликтов — <strong>только на фронтенде</strong>, без вызова API. Используются загруженные данные секции (задачи, блоки, max_concurrent_tasks).</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Вынеси логику проверки конфликтов <code>checkConflicts()</code> в отдельный метод в gantt-canvas.service.ts:</p>
|
||||
<ul>
|
||||
<li>Принимает: section (GanttSection — содержит max_concurrent_tasks и blocks), tasks (GanttTask[] — задачи секции), task_id (nullable — исключить текущую задачу), start_date, end_date</li>
|
||||
<li>Логика:
|
||||
a. Если section.max_concurrent_tasks > 0: посчитать кол-во задач (исключая task_id), пересекающихся с [start_date, end_date] на каждый день. Если в любой день count > max_concurrent_tasks — добавить конфликт concurrency
|
||||
b. Для каждого блока секции с block_concurrency = true, пересекающегося с [start_date, end_date] — добавить конфликт block</li>
|
||||
<li>Возвращает ConflictCheck</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>После <strong>каждой</strong> успешной операции с задачей (создание, обновление, drag, resize):</p>
|
||||
<ul>
|
||||
<li>Вызвать checkConflicts с параметрами задачи</li>
|
||||
<li>Если has_conflicts = true — передать конфликты в GanttConflictAlertsComponent</li>
|
||||
<li>Alert-ы показываются, действие <strong>НЕ отменяется</strong></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Точки интеграции:</p>
|
||||
<ul>
|
||||
<li><strong>Создание задачи</strong> (форма): после успешного POST → checkConflicts → show alerts</li>
|
||||
<li><strong>Редактирование задачи</strong> (форма): после успешного PUT → checkConflicts → show alerts</li>
|
||||
<li><strong>Drag задачи</strong> (interaction-handler.ts): после успешного обновления позиции → checkConflicts → show alerts</li>
|
||||
<li><strong>Resize задачи</strong> (interaction-handler.ts): после успешного обновления размера → checkConflicts → show alerts</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>При переключении диаграммы или секции — очищать Alert-ы</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Alert-ы автоматически удаляются, когда текущая дата прошла дату конфликта — область скрывается, когда конфликтов не осталось</p>
|
||||
</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<h2 id="порядок-реализации">Порядок реализации</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Этап</th>
|
||||
<th>Сторона</th>
|
||||
<th>Зависимости</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>Миграция + Домен</td>
|
||||
<td>Backend</td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>CRUD блоков</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>Обновление настроек секции</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>Включение блоков в ответ секций</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>Модели + Сервис</td>
|
||||
<td>Frontend</td>
|
||||
<td>— (можно параллельно с backend)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td>Интеграция в модальные окна</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 5 + backend 2,3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>7</td>
|
||||
<td>Визуализация блоков на Ганте</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 5 + backend 4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>8</td>
|
||||
<td>Система Alert-ов</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этап 5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>9</td>
|
||||
<td>Проверка конфликтов при операциях</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 5,7,8</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -110,16 +114,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -166,14 +160,6 @@
|
||||
<h6 class="mt-3">Теги</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/audit/">audit</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/leadera/">leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/security/">security</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||||
@@ -209,6 +195,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Подключение sand.a2v.space (Leadera Project)</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/sand-a2v-space.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/sand-a2v-space.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -110,16 +114,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -166,14 +160,6 @@
|
||||
<h6 class="mt-3">Теги</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/audit/">audit</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/leadera/">leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/security/">security</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||||
@@ -209,6 +195,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>SSH туннели для VPS reverse proxy</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/ssh-tunnels-vps.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/ssh-tunnels-vps.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -110,16 +114,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -166,14 +160,6 @@
|
||||
<h6 class="mt-3">Теги</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/audit/">audit</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/leadera/">leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/security/">security</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/architecture/">architecture</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/ssh/">ssh</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../tags/tunnel/">tunnel</a></li>
|
||||
@@ -209,6 +195,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Настройка VPS как реверс-прокси</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/guides/vps-reverse-proxy.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/guides\/vps-reverse-proxy.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
<a class="nav-link" href="./plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="./leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="./guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -95,6 +99,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="./plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="./leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="./guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="./creds/">Креды</a></li>
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -127,6 +131,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Eversolo T8 API</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/kb/eversolo-api.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/kb\/eversolo-api.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
@@ -0,0 +1,536 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera</h1>
|
||||
|
||||
<h1 id="leadera">Leadera</h1>
|
||||
<p>SaaS-система управления IT-ландшафтом с диаграммами Ганта.</p>
|
||||
<ul>
|
||||
<li><strong>Прод:</strong> <a href="https://leadera.pro">leadera.pro</a></li>
|
||||
<li><strong>Дев:</strong> <a href="https://sand.a2v.space">sand.a2v.space</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../leadera/leadera-gantt-public-access/">Leadera — Публичный доступ к диаграмме Ганта</a></h3>
|
||||
<p class="card-text"><h1 id="публичный-доступ-к-диаграмме-ганта-share">Публичный доступ к диаграмме Ганта (Share)</h1>
|
||||
<p><strong>Дата:</strong> 09.04.2026
|
||||
<strong>Статус:</strong> Требования
|
||||
<strong>Проект:</strong> Leadera</p>
|
||||
<hr>
|
||||
<h2 id="общее-описание">Общее описание</h2>
|
||||
<p>Пользователь может создать публичную ссылку на фрагмент диаграммы Ганта, доступную для просмотра без авторизации. Открывший ссылку видит диаграмму в read-only режиме с возможностью масштабирования таймлайна, но без каких-либо элементов управления (создание, редактирование, удаление задач/секций/вех).</p>
|
||||
<hr>
|
||||
<h2 id="пользовательский-сценарий-создатель-ссылки">Пользовательский сценарий (создатель ссылки)</h2>
|
||||
<ol>
|
||||
<li>Пользователь открывает диаграмму Ганта проекта</li>
|
||||
<li>Нажимает кнопку «Экспорт» / «Общий доступ»</li>
|
||||
<li>Указывает диапазон дат, который должен отображаться на диаграмме</li>
|
||||
<li>Система генерирует уникальную ссылку (UUID) и показывает её пользователю</li>
|
||||
<li>Пользователь копирует ссылку и передаёт заинтересованным лицам</li>
|
||||
</ol>
|
||||
<h3 id="управление-ссылками">Управление ссылками</h3>
|
||||
<ul>
|
||||
<li>Вызывается через меню экспорта → модальное окно «Общий доступ»</li>
|
||||
<li>В модальном окне отображается список созданных ссылок с указанием диапазона дат</li>
|
||||
<li>Каждую ссылку можно <strong>удалить</strong> (полное удаление из БД, не soft delete)</li>
|
||||
<li>Ссылки <strong>нельзя редактировать</strong> — только создавать и удалять</li>
|
||||
<li>Ограничений на количество активных ссылок нет</li>
|
||||
<li>Статистика просмотров (сколько раз открывали) — <strong>не нужна</strong></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="пользовательский-сценарий-зритель-по-ссылке">Пользовательский сценарий (зритель по ссылке)</h2>
|
||||
<ol>
|
||||
<li>Зритель переходит по ссылке (без авторизации)</li>
|
||||
<li>Открывается отдельная страница с диаграммой Ганта</li>
|
||||
<li>Зритель видит:
|
||||
<ul>
|
||||
<li>Задачи и вехи, попадающие в заданный диапазон</li>
|
||||
<li>Секции (заголовки группировок)</li>
|
||||
<li>Таймлайн с цветовой раскраской задач</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Зритель может:
|
||||
<ul>
|
||||
<li>Масштабировать таймлайн</li>
|
||||
<li>Переключать отображение «по ячейкам» / «по дням»</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Зритель <strong>не может</strong>:
|
||||
<ul>
|
||||
<li>Создавать, редактировать, удалять задачи / секции / вехи</li>
|
||||
<li>Видеть любые контролы управления (кнопки создания, контекстные меню, drag-and-drop и т.д.)</li>
|
||||
<li>Авторизоваться через эту страницу</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<hr>
|
||||
<h2 id="данные-и-фильтрация">Данные и фильтрация</h2>
|
||||
<h3 id="что-попадает-в-представление">Что попадает в представление</h3>
|
||||
<p>Задачи и вехи, у которых хотя бы одно из условий верно:</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../tags/public-access/" class="tag">public-access</a>
|
||||
|
||||
<a href="../tags/sharing/" class="tag">sharing</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../leadera/leadera-gantt-excel-export/">Leadera — Экспорт диаграммы Ганта в Excel</a></h3>
|
||||
<p class="card-text"><h1 id="экспорт-диаграммы-ганта-в-excel">Экспорт диаграммы Ганта в Excel</h1>
|
||||
<p><strong>Дата:</strong> 09.04.2026
|
||||
<strong>Статус:</strong> Требования
|
||||
<strong>Проект:</strong> Leadera</p>
|
||||
<hr>
|
||||
<h2 id="общее-описание">Общее описание</h2>
|
||||
<p>Пользователь нажимает кнопку экспорта в интерфейсе диаграммы Ганта. Система запрашивает диапазон дат выгрузки (по умолчанию — текущий видимый диапазон из интерфейса). После указания диапазона скачивается Excel-файл с визуальным представлением диаграммы Ганта.</p>
|
||||
<hr>
|
||||
<h2 id="вводные-данные">Вводные данные</h2>
|
||||
<ul>
|
||||
<li><strong>Источник:</strong> текущая диаграмма Ганта проекта (один проект = одна диаграмма)</li>
|
||||
<li><strong>Диапазон дат:</strong> пользователь указывает начальную и конечную дату. По умолчанию подставляется видимый в момент запроса диапазон из интерфейса</li>
|
||||
<li><strong>Масштаб таймлайна:</strong> берётся тот, который выбран пользователем в интерфейсе в момент запроса (не сохранённый в системе, а текущий). Варианты: по ячейкам или по дням</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="структура-excel-файла">Структура Excel-файла</h2>
|
||||
<h3 id="шапка-первые-строки">Шапка (первые строки)</h3>
|
||||
<ul>
|
||||
<li>Название диаграммы (название проекта)</li>
|
||||
<li>Диапазон дат, в рамках которого сделана выгрузка</li>
|
||||
</ul>
|
||||
<h3 id="лист--один">Лист — один</h3>
|
||||
<p>Вся диаграмма размещается на одном листе.</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../tags/excel/" class="tag">excel</a>
|
||||
|
||||
<a href="../tags/export/" class="tag">export</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../leadera/leadera-milestones/">Leadera: Вехи (Milestones) на диаграмме Ганта</a></h3>
|
||||
<p class="card-text"><h2 id="бизнес-требования">Бизнес-требования</h2>
|
||||
<h3 id="br-1-сущность-веха-milestone">BR-1: Сущность «Веха» (Milestone)</h3>
|
||||
<p>Веха — это маркер на таймлайне диаграммы Ганта, обозначающий важную дату в проекте. В отличие от задач, веха не имеет длительности и привязана к конкретной дате.</p>
|
||||
<p><strong>Поля вехи:</strong></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Поле</th>
|
||||
<th>Тип</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>id</td>
|
||||
<td>UUID</td>
|
||||
<td>Идентификатор</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>gantt_chart_id</td>
|
||||
<td>UUID</td>
|
||||
<td>Принадлежность к диаграмме (не к секции!)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>title</td>
|
||||
<td>string</td>
|
||||
<td>Название вехи (обязательное, 1–255 символов)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>date</td>
|
||||
<td>date</td>
|
||||
<td>Дата вехи (без времени, обязательная)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>color</td>
|
||||
<td>string</td>
|
||||
<td>Цвет маркера (HEX, по умолчанию <code>#dc3545</code> — красный)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>created_by</td>
|
||||
<td>UUID</td>
|
||||
<td>Кто создал (не отображается в UI, хранится для аудита)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>updated_by</td>
|
||||
<td>UUID</td>
|
||||
<td>Кто обновил (не отображается в UI)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>created_at</td>
|
||||
<td>timestamp</td>
|
||||
<td>Дата создания</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>updated_at</td>
|
||||
<td>timestamp</td>
|
||||
<td>Дата обновления</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>deleted_at</td>
|
||||
<td>timestamp</td>
|
||||
<td>Soft delete</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Права доступа:</strong> Создание, редактирование и удаление вех доступно участникам с правами выше «только чтение» (аналогично задачам).</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../tags/angular/" class="tag">angular</a>
|
||||
|
||||
<a href="../tags/go/" class="tag">go</a>
|
||||
|
||||
<a href="../tags/api/" class="tag">api</a>
|
||||
|
||||
<a href="../tags/ux/" class="tag">ux</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../leadera/leadera-gantt-filters/">Leadera — Фильтры и реорганизация списка диаграмм Ганта</a></h3>
|
||||
<p class="card-text"><h1 id="фильтры-и-реорганизация-списка-диаграмм-ганта">Фильтры и реорганизация списка диаграмм Ганта</h1>
|
||||
<p><strong>Дата:</strong> 07.04.2026
|
||||
<strong>Статус:</strong> Требования
|
||||
<strong>Проект:</strong> Leadera (dev — sand.a2v.space)</p>
|
||||
<hr>
|
||||
<h2 id="1-постановка-задачи">1. Постановка задачи</h2>
|
||||
<p>Список диаграмм Ганта должен показывать <strong>все доступные пользователю диаграммы</strong> из всех пространств (spaces), с возможностью фильтрации и поиска. Сейчас список привязан к одному пространству — нужно убрать эту зависимость и добавить удобный toolbar.</p>
|
||||
<hr>
|
||||
<h2 id="2-изменения-api">2. Изменения API</h2>
|
||||
<h3 id="21-новый-endpoint-списка-диаграмм">2.1. Новый endpoint списка диаграмм</h3>
|
||||
<p><strong>Было:</strong></p>
|
||||
<pre tabindex="0"><code>GET /api/v1/spaces/{spaceId}/gantt-charts?page=1&per_page=10
|
||||
</code></pre><p><strong>Стало:</strong></p>
|
||||
<pre tabindex="0"><code>GET /api/v1/gantt-charts?page=1&per_page=10
|
||||
</code></pre><p>Пользователь видит все диаграммы из <strong>всех пространств</strong>, где он является подтверждённым участником (статус <code>accepted</code>).</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../tags/api/" class="tag">api</a>
|
||||
|
||||
<a href="../tags/frontend/" class="tag">frontend</a>
|
||||
|
||||
<a href="../tags/requirements/" class="tag">requirements</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../leadera/leadera-websocket-gantt/">Leadera — WebSocket для диаграмм Ганта</a></h3>
|
||||
<p class="card-text"><h1 id="websocket-для-диаграмм-ганта--realtime-совместное-редактирование">WebSocket для диаграмм Ганта — realtime-совместное редактирование</h1>
|
||||
<p><strong>Дата:</strong> 07.04.2026
|
||||
<strong>Статус:</strong> Планирование
|
||||
<strong>Проект:</strong> Leadera (dev — sand.a2v.space)</p>
|
||||
<hr>
|
||||
<h2 id="1-постановка-задачи">1. Постановка задачи</h2>
|
||||
<p>Диаграммы Ганта должны обновляться в реальном времени при совместной работе: если два или более пользователей редактируют один Гант, изменения каждого мгновенно отображаются у остальных — без перезагрузки страницы и ручного обновления.</p>
|
||||
<hr>
|
||||
<h2 id="2-архитектура">2. Архитектура</h2>
|
||||
<pre tabindex="0"><code>Angular frontend ←── WebSocket ──→ Go backend (Hub) ──→ PostgreSQL
|
||||
│ │
|
||||
│ HTTP (мутации) │ Broadcast
|
||||
└──→ REST API ──→ Service ──→ DB ──→ Hub ──→ все клиенты комнаты
|
||||
</code></pre><p><strong>Ключевой принцип:</strong> мутации данных идут через HTTP (REST), WebSocket используется <strong>только для доставки уведомлений</strong>. Сервер — источник истины, клиент не может обойти проверки через WS.</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../tags/websocket/" class="tag">websocket</a>
|
||||
|
||||
<a href="../tags/realtime/" class="tag">realtime</a>
|
||||
|
||||
<a href="../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../tags/architecture/" class="tag">architecture</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../leadera/leadera-section-constraints/">Leadera: Ограничения секций в диаграмме Ганта</a></h3>
|
||||
<p class="card-text"><h2 id="бизнес-требования">Бизнес-требования</h2>
|
||||
<h3 id="br-1-лимит-одновременных-задач-concurrency-limit">BR-1: Лимит одновременных задач (Concurrency Limit)</h3>
|
||||
<p>У каждой секции появляется параметр <strong>max_concurrent_tasks</strong> (целое число ≥ 0, по умолчанию 0).</p>
|
||||
<p><strong>Поведение:</strong></p>
|
||||
<ul>
|
||||
<li>Если <code>max_concurrent_tasks = 3</code> — в любой данный день в этой секции не может быть больше 3 активных задач одновременно</li>
|
||||
<li>Если <code>max_concurrent_tasks = 0</code> — ограничение отключено, лимита нет</li>
|
||||
<li>Пересечение считается по дате: задача занимает дни с <code>start_date</code> по <code>end_date</code> включительно. Две задачи «пересекаются», если у них есть хотя бы один общий день</li>
|
||||
<li>Ограничение работает <strong>только в рамках одной секции</strong>. Задачи из разных секций не влияют друг на друга</li>
|
||||
</ul>
|
||||
<p><strong>Срабатывание при:</strong></p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../tags/angular/" class="tag">angular</a>
|
||||
|
||||
<a href="../tags/go/" class="tag">go</a>
|
||||
|
||||
<a href="../tags/api/" class="tag">api</a>
|
||||
|
||||
<a href="../tags/ux/" class="tag">ux</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title"><a href="../leadera/leadera-audit/">Аудит Leadera — полный отчёт</a></h3>
|
||||
<p class="card-text"><h1 id="аудит-проекта-leadera">Аудит проекта Leadera</h1>
|
||||
<p><strong>Дата:</strong> 30.03.2026<br>
|
||||
<strong>Версия:</strong> pre-production, миграции 1–5</p>
|
||||
<hr>
|
||||
<h2 id="1-обзор-проекта">1. Обзор проекта</h2>
|
||||
<p>Leadera — SaaS-система управления IT-ландшафтом с диаграммами Ганта.</p>
|
||||
<h3 id="стек">Стек</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Компонент</th>
|
||||
<th>Технологии</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Backend</td>
|
||||
<td>Go 1.21+, Gin, PostgreSQL 16, pgx/v5 + sqlx, JWT (HMAC-SHA256)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Frontend</td>
|
||||
<td>Angular 21.1, Bootstrap 5, NgBootstrap, RxJS, Signals</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Инфраструктура</td>
|
||||
<td>Docker (Colima), pm2, nginx reverse proxy, VPS reg.ru</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="доменная-модель">Доменная модель</h3>
|
||||
<pre tabindex="0"><code>User ──┬── Space (через space_members с role)
|
||||
│
|
||||
└── GanttChart (принадлежит Space)
|
||||
├── GanttSection
|
||||
└── GanttTask
|
||||
</code></pre><p><strong>Роли:</strong> admin, editor, viewer — назначаются <strong>на уровне Space</strong> через таблицу <code>space_members</code>.</p></p>
|
||||
|
||||
<div class="tags">
|
||||
|
||||
<a href="../tags/audit/" class="tag">audit</a>
|
||||
|
||||
<a href="../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../tags/security/" class="tag">security</a>
|
||||
|
||||
<a href="../tags/architecture/" class="tag">architecture</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../js/search.js"></script>
|
||||
<script src="../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,732 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Аудит Leadera — полный отчёт — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Аудит Leadera — полный отчёт</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/leadera/leadera-audit.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/leadera\/leadera-audit.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/audit/" class="tag">audit</a>
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/security/" class="tag">security</a>
|
||||
|
||||
<a href="../../tags/architecture/" class="tag">architecture</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h1 id="аудит-проекта-leadera">Аудит проекта Leadera</h1>
|
||||
<p><strong>Дата:</strong> 30.03.2026<br>
|
||||
<strong>Версия:</strong> pre-production, миграции 1–5</p>
|
||||
<hr>
|
||||
<h2 id="1-обзор-проекта">1. Обзор проекта</h2>
|
||||
<p>Leadera — SaaS-система управления IT-ландшафтом с диаграммами Ганта.</p>
|
||||
<h3 id="стек">Стек</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Компонент</th>
|
||||
<th>Технологии</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Backend</td>
|
||||
<td>Go 1.21+, Gin, PostgreSQL 16, pgx/v5 + sqlx, JWT (HMAC-SHA256)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Frontend</td>
|
||||
<td>Angular 21.1, Bootstrap 5, NgBootstrap, RxJS, Signals</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Инфраструктура</td>
|
||||
<td>Docker (Colima), pm2, nginx reverse proxy, VPS reg.ru</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="доменная-модель">Доменная модель</h3>
|
||||
<pre tabindex="0"><code>User ──┬── Space (через space_members с role)
|
||||
│
|
||||
└── GanttChart (принадлежит Space)
|
||||
├── GanttSection
|
||||
└── GanttTask
|
||||
</code></pre><p><strong>Роли:</strong> admin, editor, viewer — назначаются <strong>на уровне Space</strong> через таблицу <code>space_members</code>.</p>
|
||||
<h3 id="что-реализовано-endpoints">Что реализовано (endpoints)</h3>
|
||||
<p><strong>Auth (5):</strong> register, login, refresh, change-password, logout<br>
|
||||
<strong>Spaces (9):</strong> CRUD + members CRUD<br>
|
||||
<strong>Gantt Charts (9):</strong> CRUD + archive + members (добавлен, но удалён в миграции 5)<br>
|
||||
<strong>Gantt Sections (5):</strong> CRUD + reorder<br>
|
||||
<strong>Gantt Tasks (4):</strong> CRUD</p>
|
||||
<p><strong>Итого: ~32 endpoint</strong></p>
|
||||
<hr>
|
||||
<h2 id="2-архитектура-и-качество-кода">2. Архитектура и качество кода</h2>
|
||||
<h3 id="21-backend--структура">2.1. Backend — структура</h3>
|
||||
<pre tabindex="0"><code>cmd/server/main.go — точка входа
|
||||
internal/
|
||||
domain/ — модели (User, Space, GanttChart, etc.)
|
||||
dto/ — request/response DTO (auth, space, gantt)
|
||||
handlers/ — HTTP handlers (auth, space, gantt)
|
||||
middleware/ — auth, cors, space, gantt, role, logging, recovery
|
||||
repository/ — слой доступа к данным
|
||||
service/ — бизнес-логика
|
||||
config/ — конфигурация (.env)
|
||||
utils/ — jwt, password, response, validation
|
||||
migrations/ — SQL миграции (1-5)
|
||||
</code></pre><p><strong>Оценка:</strong> ✅ Чистая layered архитектура. Разделение ответственности чёткое.</p>
|
||||
<h3 id="22-frontend--структура">2.2. Frontend — структура</h3>
|
||||
<pre tabindex="0"><code>src/app/
|
||||
core/auth/ — auth service, interceptor
|
||||
core/models/ — app.model, space.model
|
||||
core/services/ — space.service, space-storage.service
|
||||
pages/auth/ — sign-in, sign-up, reset-pw, etc.
|
||||
pages/gantt/ — компоненты, модалки, сервисы, модели
|
||||
pages/dashboards/ — spaces list, dashboard
|
||||
layouts/ — navbar, topbar, main-layout
|
||||
</code></pre><p><strong>Оценка:</strong> ✅ Стандартная Angular-структура. Signals для state management.</p>
|
||||
<h3 id="23-проблемы-кода">2.3. Проблемы кода</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Проблема</th>
|
||||
<th>Критичность</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>C1</td>
|
||||
<td><code>\r</code> в service-файлах</td>
|
||||
<td>Medium</td>
|
||||
<td>gantt_service.go, gantt_section_service.go содержат CRLF</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>C2</td>
|
||||
<td><code>main.go:238</code> — мёртвый код <code>getUserHandler</code></td>
|
||||
<td>Low</td>
|
||||
<td>Неиспользуемая функция</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>C3</td>
|
||||
<td>Swagger не сгенерирован</td>
|
||||
<td>Medium</td>
|
||||
<td>Аннотации есть, но <code>api/</code> директории нет</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>C4</td>
|
||||
<td><code>RegisterProtectedRoutes</code> не вызывается в main.go</td>
|
||||
<td><strong>Critical</strong></td>
|
||||
<td>GET/PUT/DELETE <code>/spaces/:id</code> не зарегистрированы</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="3-права-доступа--главный-вопрос">3. Права доступа — главный вопрос</h2>
|
||||
<h3 id="31-текущая-реализация">3.1. Текущая реализация</h3>
|
||||
<p>Права назначаются <strong>исключительно через Space</strong>:</p>
|
||||
<ol>
|
||||
<li>Таблица <code>space_members</code> — связка user ↔ space с role (admin/editor/viewer)</li>
|
||||
<li><code>SpaceMiddleware.RequireSpace()</code> — проверяет членство и записывает <code>ContextSpaceRole</code> в context</li>
|
||||
<li><code>RoleMiddleware.RequireMinRole()</code> — проверяет иерархию ролей (admin > editor > viewer)</li>
|
||||
</ol>
|
||||
<p><strong>Диаграммы Ганта НЕ имеют собственной системы ролей.</strong> Таблица <code>gantt_chart_members</code> была создана (миграция 4) и тут же удалена (миграция 5). Все проверки доступа к диаграммам идут через space membership.</p>
|
||||
<h3 id="32-как-это-работает">3.2. Как это работает</h3>
|
||||
<pre tabindex="0"><code>Запрос → AuthMiddleware (JWT → userID)
|
||||
→ SpaceMiddleware.RequireSpace() (проверяет space_members → ContextSpaceRole)
|
||||
→ GanttMiddleware.RequireGanttChart() (проверяет chart.space_id == spaceID)
|
||||
→ RoleMiddleware.RequireMinRole("editor") (проверяет ContextSpaceRole)
|
||||
</code></pre><p><strong>Это правильно.</strong> Любой член Space с ролью editor+ может создавать/редактировать диаграммы. Viewers — только читать.</p>
|
||||
<h3 id="33--критическая-проблема-frontend-не-соответствует-backend">3.3. 🔴 Критическая проблема: Frontend не соответствует Backend</h3>
|
||||
<p><strong>Frontend <code>MembersModalComponent</code></strong> пытается управлять участниками <strong>диаграммы</strong>:</p>
|
||||
<ul>
|
||||
<li><code>ganttService.addMember(chartId, dto)</code> → <code>/api/v1/spaces/{spaceId}/gantt-charts/{chartId}/members</code></li>
|
||||
<li><code>ganttService.updateMemberRole(chartId, userId, dto)</code> → <code>.../members/{userId}</code></li>
|
||||
<li><code>ganttService.removeMember(chartId, userId)</code> → <code>.../members/{userId}</code></li>
|
||||
</ul>
|
||||
<p><strong>Backend <code>GanttHandler.RegisterRoutes()</code> НЕ регистрирует маршруты для members диаграмм.</strong> После удаления <code>gantt_chart_members</code> (миграция 5) эти endpoints не существуют.</p>
|
||||
<p><strong>Результат:</strong> модальное окно «Участники диаграммы» на фронтенде <strong>не работает</strong> — все вызовы вернут 404.</p>
|
||||
<h3 id="34--проблема-role-fallback">3.4. 🔴 Проблема: Role fallback</h3>
|
||||
<p>В <code>role_middleware.go:28-31</code>:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">role</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">GetString</span>(<span style="color:#a6e22e">ContextSpaceRole</span>)
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#a6e22e">role</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">""</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">role</span> = <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">GetString</span>(<span style="color:#a6e22e">ContextRole</span>) <span style="color:#75715e">// fallback на глобальную роль User</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><p>Если <code>ContextSpaceRole</code> не установлен, используется <strong>глобальная роль пользователя</strong> (из JWT). Это значит:</p>
|
||||
<ul>
|
||||
<li>Пользователь с <code>role=admin</code> в таблице <code>users</code> получит admin-доступ к <strong>любому</strong> space, даже если он не член этого space</li>
|
||||
<li>Но <code>SpaceMiddleware</code> проверяет членство раньше, так что в цепочке <code>RequireSpace() → RequireMinRole()</code> это не эксплуатируется</li>
|
||||
<li>Однако если <code>RequireMinRole()</code> используется <strong>без</strong> <code>RequireSpace()</code> — это дыра</li>
|
||||
</ul>
|
||||
<p><strong>Рекомендация:</strong> Удалить fallback на <code>ContextRole</code>. Если <code>ContextSpaceRole</code> пуст — отказывать.</p>
|
||||
<h3 id="35-рекомендации-по-правам-доступа">3.5. Рекомендации по правам доступа</h3>
|
||||
<ol>
|
||||
<li><strong>Удалить модалку участников диаграммы</strong> на фронтенде — она мертва</li>
|
||||
<li><strong>Или</strong> добавить раздел «Участники Space» с возможностью управления ролями через <code>/spaces/:space_id/members</code> (эти endpoints работают)</li>
|
||||
<li>Удалить fallback на <code>ContextRole</code> в <code>role_middleware.go</code></li>
|
||||
<li>Добавить проверку: editor не может повысить кого-то до admin (сейчас может — <code>AddMember</code> разрешает editor+ назначать любую роль)</li>
|
||||
</ol>
|
||||
<hr>
|
||||
<h2 id="4-безопасность">4. Безопасность</h2>
|
||||
<h3 id="41-критические">4.1. Критические</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Проблема</th>
|
||||
<th>Файл</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>S1</td>
|
||||
<td>JWT Secret = test</td>
|
||||
<td><code>.env</code></td>
|
||||
<td><code>JWT_SECRET=test_secret_key_change_in_production</code> — <strong>это прод-конфиг!</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>S2</td>
|
||||
<td>CORS: <code>*</code></td>
|
||||
<td><code>.env</code></td>
|
||||
<td><code>CORS_ALLOWED_ORIGINS=*</code> — любой домен может обращаться к API</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>S3</td>
|
||||
<td>Пароль БД в .env</td>
|
||||
<td><code>.env</code></td>
|
||||
<td><code>DB_PASSWORD=popoloka16</code> — creds в открытом виде</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>S4</td>
|
||||
<td>Refresh tokens: SHA256</td>
|
||||
<td><code>utils/password.go</code></td>
|
||||
<td>SHA256 вместо bcrypt для refresh token hash — быстро брутфорсится</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="42-высокие">4.2. Высокие</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Проблема</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>S5</td>
|
||||
<td>Нет rate limiting</td>
|
||||
<td>Brute-force на <code>/auth/login</code> без ограничений</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>S6</td>
|
||||
<td>Нет CSRF protection</td>
|
||||
<td>API принимает POST/PUT/DELETE без CSRF-токена</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>S7</td>
|
||||
<td>Нет CSP заголовков</td>
|
||||
<td>X-Frame-Options, Content-Security-Policy не настроены</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>S8</td>
|
||||
<td>JWT в localStorage</td>
|
||||
<td>XSS может украсть access + refresh tokens</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="43-средние">4.3. Средние</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Проблема</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>S9</td>
|
||||
<td>Нет password policy</td>
|
||||
<td>Минимальные требования к паролю не enforced</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>S10</td>
|
||||
<td>Нет audit log</td>
|
||||
<td>Действия с spaces/charts не логируются для аудита</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>S11</td>
|
||||
<td>Нет блокировки аккаунта</td>
|
||||
<td>Неограниченные попытки входа</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="5-инфраструктура">5. Инфраструктура</h2>
|
||||
<h3 id="51-текущее-состояние">5.1. Текущее состояние</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Компонент</th>
|
||||
<th>Статус</th>
|
||||
<th>Примечание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Go API (pm2: leadera-api)</td>
|
||||
<td>⚠️</td>
|
||||
<td>Порт 8080, хост 0.0.0.0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Angular FE (pm2: leadera-fe)</td>
|
||||
<td>⚠️</td>
|
||||
<td>Порт 4200, хост 0.0.0.0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PostgreSQL (Docker)</td>
|
||||
<td>✅</td>
|
||||
<td>Порт 5432, контейнер app-leadera-db</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VPS reverse proxy</td>
|
||||
<td>⚠️</td>
|
||||
<td>sand.a2v.space → 8082/8083, но 502 если сервисы не запущены</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SSL</td>
|
||||
<td>✅</td>
|
||||
<td>Let’s Encrypt на VPS</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="52-проблемы">5.2. Проблемы</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Проблема</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>I1</td>
|
||||
<td>Docker для БД, нативно для API</td>
|
||||
<td>Несогласованность — API и FE не в Docker</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>I2</td>
|
||||
<td>Нет health-check в pm2</td>
|
||||
<td>API может упасть без перезапуска</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>I3</td>
|
||||
<td>Нет CI/CD</td>
|
||||
<td>Ручной деплой через <code>make run</code> + pm2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>I4</td>
|
||||
<td>Нет nginx на Mac Studio</td>
|
||||
<td>FE и API торчат наружу напрямую</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="6-функциональная-оценка">6. Функциональная оценка</h2>
|
||||
<h3 id="61-что-работает">6.1. Что работает</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Функция</th>
|
||||
<th>Backend</th>
|
||||
<th>Frontend</th>
|
||||
<th>Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Регистрация/логин</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
<td>Работает</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>JWT auth + refresh</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
<td>Работает</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Spaces CRUD</td>
|
||||
<td>⚠️</td>
|
||||
<td>✅</td>
|
||||
<td><code>RegisterProtectedRoutes</code> не вызван (C4)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Space members</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
<td>Работает</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gantt charts CRUD</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
<td>Работает</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gantt sections</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
<td>Работает</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gantt tasks</td>
|
||||
<td>✅</td>
|
||||
<td>⚠️</td>
|
||||
<td>Нет UI для задач внутри диаграммы</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Архивация диаграмм</td>
|
||||
<td>✅</td>
|
||||
<td>✅</td>
|
||||
<td>Работает</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Members диаграммы</td>
|
||||
<td>❌</td>
|
||||
<td>✅ (мёртвый UI)</td>
|
||||
<td>Backend endpoints не существуют</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="62-что-не-работает-или-отсутствует">6.2. Что НЕ работает или отсутствует</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Функция</th>
|
||||
<th>Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Визуализация Ганта (таймлайн)</td>
|
||||
<td>❌ Нет компонента отрисовки</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Drag & Drop задач</td>
|
||||
<td>❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Real-time collaboration</td>
|
||||
<td>❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Уведомления</td>
|
||||
<td>❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Поиск</td>
|
||||
<td>❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Экспорт (PDF/Excel)</td>
|
||||
<td>❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Email verification</td>
|
||||
<td>❌</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Password reset</td>
|
||||
<td>Backend есть, UI есть — не проверено</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="63-оценка-готовности">6.3. Оценка готовности</h3>
|
||||
<p><strong>Система на стадии каркаса.</strong> Backend API функционален для CRUD операций, но:</p>
|
||||
<ul>
|
||||
<li>Визуализация Ганта — основная фича — не реализована (нет canvas/SVG рендеринга)</li>
|
||||
<li>Управление участниками только на уровне Space</li>
|
||||
<li>Нет CI/CD, тестов, мониторинга</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="7-приоритизированный-план-исправлений">7. Приоритизированный план исправлений</h2>
|
||||
<h3 id="неделя-1--критическое-безопасность">Неделя 1 — Критическое (безопасность)</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Приоритет</th>
|
||||
<th>Задача</th>
|
||||
<th>Трудоёмкость</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>P0</td>
|
||||
<td>Заменить JWT secret на криптостойкий</td>
|
||||
<td>1 час</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P0</td>
|
||||
<td>Настроить CORS (только sand.a2v.space)</td>
|
||||
<td>1 час</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P0</td>
|
||||
<td>bcrypt для refresh tokens вместо SHA256</td>
|
||||
<td>2 часа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P0</td>
|
||||
<td>Удалить fallback на ContextRole в role_middleware</td>
|
||||
<td>30 мин</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P0</td>
|
||||
<td>Исправить <code>RegisterProtectedRoutes</code> — зарегистрировать маршруты</td>
|
||||
<td>1 час</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P1</td>
|
||||
<td>Rate limiting на auth endpoints</td>
|
||||
<td>4 часа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P1</td>
|
||||
<td>Editor не может назначать admin роль</td>
|
||||
<td>2 часа</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="неделя-2--архитектура-прав">Неделя 2 — Архитектура прав</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Приоритет</th>
|
||||
<th>Задача</th>
|
||||
<th>Трудоёмкость</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>P1</td>
|
||||
<td>Удалить/переписать MembersModalComponent → Space members</td>
|
||||
<td>4 часа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P1</td>
|
||||
<td>Добавить страницу управления участниками Space</td>
|
||||
<td>8 часов</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P2</td>
|
||||
<td>HttpOnly cookies для JWT (вместо localStorage)</td>
|
||||
<td>8 часов</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P2</td>
|
||||
<td>CSRF protection</td>
|
||||
<td>4 часа</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="неделя-3--функциональность">Неделя 3 — Функциональность</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Приоритет</th>
|
||||
<th>Задача</th>
|
||||
<th>Трудоёмкость</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>P2</td>
|
||||
<td>Визуализация Ганта (canvas/d3)</td>
|
||||
<td>40+ часов</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P2</td>
|
||||
<td>Drag & Drop для задач</td>
|
||||
<td>16 часов</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P3</td>
|
||||
<td>Тесты (unit + integration)</td>
|
||||
<td>24 часа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P3</td>
|
||||
<td>CI/CD pipeline</td>
|
||||
<td>8 часов</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="8-резюме">8. Резюме</h2>
|
||||
<h3 id="ключевые-выводы">Ключевые выводы</h3>
|
||||
<ol>
|
||||
<li><strong>Права доступа</strong> — архитектурно верно: роль назначается через Space, не через диаграмму. Но фронтенд ещё не обновлён после удаления <code>gantt_chart_members</code>.</li>
|
||||
<li><strong>Безопасность</strong> — критические уязвимости в конфигурации (JWT secret, CORS, bcrypt). Требуют немедленного исправления до любого публичного деплоя.</li>
|
||||
<li><strong>Функциональность</strong> — CRUD работает, но основная ценность (визуализация Ганта) не реализована.</li>
|
||||
<li><strong>Код</strong> — чистый, хорошая архитектура, но есть мёртвый код и незарегистрированные маршруты.</li>
|
||||
</ol>
|
||||
<h3 id="общая-оценка--pre-alpha">Общая оценка: 🔴 Pre-alpha</h3>
|
||||
<p>Проект готов к внутренней демонстрации CRUD, но не готов к продакшену. Первоочередное — безопасность и исправление мёртвых endpoints.</p>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,262 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera — Экспорт диаграммы Ганта в Excel — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera — Экспорт диаграммы Ганта в Excel</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/leadera/leadera-gantt-excel-export.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/leadera\/leadera-gantt-excel-export.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../../tags/excel/" class="tag">excel</a>
|
||||
|
||||
<a href="../../tags/export/" class="tag">export</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h1 id="экспорт-диаграммы-ганта-в-excel">Экспорт диаграммы Ганта в Excel</h1>
|
||||
<p><strong>Дата:</strong> 09.04.2026
|
||||
<strong>Статус:</strong> Требования
|
||||
<strong>Проект:</strong> Leadera</p>
|
||||
<hr>
|
||||
<h2 id="общее-описание">Общее описание</h2>
|
||||
<p>Пользователь нажимает кнопку экспорта в интерфейсе диаграммы Ганта. Система запрашивает диапазон дат выгрузки (по умолчанию — текущий видимый диапазон из интерфейса). После указания диапазона скачивается Excel-файл с визуальным представлением диаграммы Ганта.</p>
|
||||
<hr>
|
||||
<h2 id="вводные-данные">Вводные данные</h2>
|
||||
<ul>
|
||||
<li><strong>Источник:</strong> текущая диаграмма Ганта проекта (один проект = одна диаграмма)</li>
|
||||
<li><strong>Диапазон дат:</strong> пользователь указывает начальную и конечную дату. По умолчанию подставляется видимый в момент запроса диапазон из интерфейса</li>
|
||||
<li><strong>Масштаб таймлайна:</strong> берётся тот, который выбран пользователем в интерфейсе в момент запроса (не сохранённый в системе, а текущий). Варианты: по ячейкам или по дням</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="структура-excel-файла">Структура Excel-файла</h2>
|
||||
<h3 id="шапка-первые-строки">Шапка (первые строки)</h3>
|
||||
<ul>
|
||||
<li>Название диаграммы (название проекта)</li>
|
||||
<li>Диапазон дат, в рамках которого сделана выгрузка</li>
|
||||
</ul>
|
||||
<h3 id="лист--один">Лист — один</h3>
|
||||
<p>Вся диаграмма размещается на одном листе.</p>
|
||||
<h3 id="секции">Секции</h3>
|
||||
<ul>
|
||||
<li>Секции отображаются как <strong>визуальная группировка строк</strong> (не отдельные листы)</li>
|
||||
<li>Название секции — строка-заголовок над группой задач</li>
|
||||
</ul>
|
||||
<h3 id="задачи">Задачи</h3>
|
||||
<p>Каждая задача — отдельная строка. Данные задачи:</p>
|
||||
<ul>
|
||||
<li><strong>Название</strong> (текстовая колонка слева)</li>
|
||||
<li><strong>Дата начала</strong> (текстовая колонка слева)</li>
|
||||
<li><strong>Дата конца</strong> (текстовая колонка слева)</li>
|
||||
<li><strong>Цвет</strong> — ячейки таймлайна, соответствующие задаче, закрашены в цвет задачи</li>
|
||||
</ul>
|
||||
<h3 id="таймлайн">Таймлайн</h3>
|
||||
<ul>
|
||||
<li>Таймлайн представлен колонками дат в верхней части (горизонтальная ось времени)</li>
|
||||
<li>Масштаб колонок соответствует выбранному пользователем в момент экспорта (по ячейкам / по дням)</li>
|
||||
<li>Задачи отображаются как <strong>merged cells</strong> по соответствующим колонкам дат — визуальная полоса диаграммы Ганта</li>
|
||||
</ul>
|
||||
<h3 id="вехи">Вехи</h3>
|
||||
<ul>
|
||||
<li>Отображаются <strong>отдельной строкой</strong> (как задача, но с маркировкой вехи)</li>
|
||||
<li>Позиционируются на соответствующей дате в таймлайне</li>
|
||||
</ul>
|
||||
<h3 id="зависимости">Зависимости</h3>
|
||||
<ul>
|
||||
<li><strong>Не отображаются</strong> (стрелочки/связи между задачами не нужны)</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="пользовательский-сценарий">Пользовательский сценарий</h2>
|
||||
<ol>
|
||||
<li>Пользователь открывает диаграмму Ганта проекта</li>
|
||||
<li>Настраивает масштаб таймлайна (по ячейкам / по дням)</li>
|
||||
<li>Нажимает кнопку «Экспорт в Excel»</li>
|
||||
<li>Система показывает диалог с выбором диапазона дат (предзаполнен текущим видимым диапазоном)</li>
|
||||
<li>Пользователь подтверждает / корректирует диапазон</li>
|
||||
<li>Система генерирует Excel-файл и инициирует скачивание</li>
|
||||
</ol>
|
||||
<hr>
|
||||
<h2 id="технические-требования">Технические требования</h2>
|
||||
<h3 id="backend-go-api">Backend (Go API)</h3>
|
||||
<ul>
|
||||
<li>Новый endpoint для генерации Excel (например, <code>GET /api/v1/gantt/:projectId/export</code>)</li>
|
||||
<li>Query-параметры: <code>dateFrom</code>, <code>dateTo</code>, <code>scale</code> (cell|day)</li>
|
||||
<li>Ответ: binary Excel-файл (<code>application/vnd.openxmlformats-officedocument.spreadsheetml.sheet</code>)</li>
|
||||
<li>Авторизация: обязательна (только участники проекта)</li>
|
||||
<li>Библиотека для генерации Excel: <code>excelize</code> (Go)</li>
|
||||
</ul>
|
||||
<h3 id="frontend-angular">Frontend (Angular)</h3>
|
||||
<ul>
|
||||
<li>Кнопка экспорта в тулбаре диаграммы Ганта</li>
|
||||
<li>Модальное окно выбора диапазона дат с datepicker’ами</li>
|
||||
<li>Предзаполнение диапазона из текущего видимого viewport’а</li>
|
||||
<li>Скачивание файла через <code>Blob</code> / <code>saveAs</code></li>
|
||||
</ul>
|
||||
<h3 id="фильтрация-данных">Фильтрация данных</h3>
|
||||
<ul>
|
||||
<li>Выгружаются все задачи и вехи, которые:
|
||||
<ul>
|
||||
<li>Начинаются в диапазоне, ИЛИ</li>
|
||||
<li>Заканчиваются в диапазоне, ИЛИ</li>
|
||||
<li>Начинаются до диапазона и заканчиваются после (перекрывают целиком)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Секции включаются, если в них есть хотя бы одна попавшая задача</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="открытые-вопросы">Открытые вопросы</h2>
|
||||
<p><em>(Нет)</em></p>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,463 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera — Фильтры и реорганизация списка диаграмм Ганта — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera — Фильтры и реорганизация списка диаграмм Ганта</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/leadera/leadera-gantt-filters.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/leadera\/leadera-gantt-filters.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../../tags/api/" class="tag">api</a>
|
||||
|
||||
<a href="../../tags/frontend/" class="tag">frontend</a>
|
||||
|
||||
<a href="../../tags/requirements/" class="tag">requirements</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h1 id="фильтры-и-реорганизация-списка-диаграмм-ганта">Фильтры и реорганизация списка диаграмм Ганта</h1>
|
||||
<p><strong>Дата:</strong> 07.04.2026
|
||||
<strong>Статус:</strong> Требования
|
||||
<strong>Проект:</strong> Leadera (dev — sand.a2v.space)</p>
|
||||
<hr>
|
||||
<h2 id="1-постановка-задачи">1. Постановка задачи</h2>
|
||||
<p>Список диаграмм Ганта должен показывать <strong>все доступные пользователю диаграммы</strong> из всех пространств (spaces), с возможностью фильтрации и поиска. Сейчас список привязан к одному пространству — нужно убрать эту зависимость и добавить удобный toolbar.</p>
|
||||
<hr>
|
||||
<h2 id="2-изменения-api">2. Изменения API</h2>
|
||||
<h3 id="21-новый-endpoint-списка-диаграмм">2.1. Новый endpoint списка диаграмм</h3>
|
||||
<p><strong>Было:</strong></p>
|
||||
<pre tabindex="0"><code>GET /api/v1/spaces/{spaceId}/gantt-charts?page=1&per_page=10
|
||||
</code></pre><p><strong>Стало:</strong></p>
|
||||
<pre tabindex="0"><code>GET /api/v1/gantt-charts?page=1&per_page=10
|
||||
</code></pre><p>Пользователь видит все диаграммы из <strong>всех пространств</strong>, где он является подтверждённым участником (статус <code>accepted</code>).</p>
|
||||
<h3 id="22-параметры-фильтрации-и-поиска">2.2. Параметры фильтрации и поиска</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Параметр</th>
|
||||
<th>Тип</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>page</code></td>
|
||||
<td>int</td>
|
||||
<td>Номер страницы (default: 1)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>per_page</code></td>
|
||||
<td>int</td>
|
||||
<td>Записей на странице (default: 10)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>space_id</code></td>
|
||||
<td>UUID (optional)</td>
|
||||
<td>Фильтр по конкретному пространству</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>created_by</code></td>
|
||||
<td>UUID (optional)</td>
|
||||
<td>Фильтр по создателю диаграммы</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>search</code></td>
|
||||
<td>string (optional)</td>
|
||||
<td>Поиск по назвению и описанию (ILIKE, case-insensitive)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>sort</code></td>
|
||||
<td>string (optional)</td>
|
||||
<td>Сортировка: <code>created_at</code>, <code>updated_at</code>, <code>title</code> (default: <code>updated_at</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>order</code></td>
|
||||
<td>string (optional)</td>
|
||||
<td><code>asc</code> / <code>desc</code> (default: <code>desc</code>)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Примеры:</strong></p>
|
||||
<pre tabindex="0"><code>GET /api/v1/gantt-charts?search=миграция
|
||||
GET /api/v1/gantt-charts?space_id=0b334ded-...&created_by=a1b2c3d4-...
|
||||
GET /api/v1/gantt-charts?page=2&per_page=20&sort=title&order=asc
|
||||
</code></pre><h3 id="23-изменение-структуры-ответа">2.3. Изменение структуры ответа</h3>
|
||||
<p>Добавить поле <code>space</code> в каждую диаграмму:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"data"</span>: [
|
||||
</span></span><span style="display:flex;"><span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"title"</span>: <span style="color:#e6db74">"Миграция backend"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"description"</span>: <span style="color:#e6db74">"..."</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"created_by"</span>: <span style="color:#e6db74">"uuid"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"created_by_name"</span>: <span style="color:#e6db74">"Антон Агальцов"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"created_at"</span>: <span style="color:#e6db74">"2026-04-01T10:00:00Z"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"updated_at"</span>: <span style="color:#e6db74">"2026-04-07T09:00:00Z"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"space"</span>: {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"name"</span>: <span style="color:#e6db74">"IT Ландшафт"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"color"</span>: <span style="color:#e6db74">"#c4623a"</span>
|
||||
</span></span><span style="display:flex;"><span> },
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"is_archived"</span>: <span style="color:#66d9ef">false</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> ],
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"pagination"</span>: {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"page"</span>: <span style="color:#ae81ff">1</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"per_page"</span>: <span style="color:#ae81ff">10</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"total"</span>: <span style="color:#ae81ff">42</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"total_pages"</span>: <span style="color:#ae81ff">5</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h3 id="24-endpoint-для-списка-фильтров">2.4. Endpoint для списка фильтров</h3>
|
||||
<p>Вспомогательный endpoint для заполнения dropdown-фильтров:</p>
|
||||
<pre tabindex="0"><code>GET /api/v1/gantt-charts/filters
|
||||
</code></pre><p><strong>Ответ:</strong></p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"spaces"</span>: [
|
||||
</span></span><span style="display:flex;"><span> { <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>, <span style="color:#f92672">"name"</span>: <span style="color:#e6db74">"IT Ландшафт"</span>, <span style="color:#f92672">"color"</span>: <span style="color:#e6db74">"#c4623a"</span> },
|
||||
</span></span><span style="display:flex;"><span> { <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>, <span style="color:#f92672">"name"</span>: <span style="color:#e6db74">"DevOps"</span>, <span style="color:#f92672">"color"</span>: <span style="color:#e6db74">"#27ae60"</span> }
|
||||
</span></span><span style="display:flex;"><span> ],
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"creators"</span>: [
|
||||
</span></span><span style="display:flex;"><span> { <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>, <span style="color:#f92672">"display_name"</span>: <span style="color:#e6db74">"Антон Агальцов"</span> },
|
||||
</span></span><span style="display:flex;"><span> { <span style="color:#f92672">"id"</span>: <span style="color:#e6db74">"uuid"</span>, <span style="color:#f92672">"display_name"</span>: <span style="color:#e6db74">"Иван Петров"</span> }
|
||||
</span></span><span style="display:flex;"><span> ]
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><p><strong>Логика <code>creators</code>:</strong> уникальные создатели всех диаграмм, доступных текущему пользователю. Получается через DISTINCT-запрос по <code>created_by</code> с JOIN на users.</p>
|
||||
<h3 id="25-backend--изменения-в-go">2.5. Backend — изменения в Go</h3>
|
||||
<p><strong>Файлы для изменения:</strong></p>
|
||||
<h4 id="internalhandlersgantt_handlergo"><code>internal/handlers/gantt_handler.go</code></h4>
|
||||
<ul>
|
||||
<li>Добавить новый handler <code>ListAllGanttCharts</code> — без привязки к space</li>
|
||||
<li>Зарегистрировать маршрут <code>GET /gantt-charts</code> (без <code>/spaces/:id/</code>)</li>
|
||||
<li>Парсить query-параметры: <code>space_id</code>, <code>created_by</code>, <code>search</code>, <code>sort</code>, <code>order</code></li>
|
||||
<li>Старый маршрут <code>GET /spaces/:id/gantt-charts</code> — оставить для обратной совместимости (или удалить, если не используется)</li>
|
||||
</ul>
|
||||
<h4 id="internalservicegantt_servicego"><code>internal/service/gantt_service.go</code></h4>
|
||||
<ul>
|
||||
<li>Новый метод <code>ListAll(ctx, userId, filters)</code> — запрашивает все spaces пользователя (через <code>space_member</code> с <code>status=accepted</code>), затем выбирает все gantt_charts из этих spaces с фильтрами</li>
|
||||
</ul>
|
||||
<h4 id="internalrepositorygantt_repositorygo"><code>internal/repository/gantt_repository.go</code></h4>
|
||||
<ul>
|
||||
<li>Новый метод с динамическим SQL:
|
||||
<ul>
|
||||
<li>Базовый JOIN: <code>gantt_charts</code> → <code>spaces</code> → <code>space_members</code></li>
|
||||
<li>WHERE: <code>space_members.user_id = $1 AND space_members.status = 'accepted'</code></li>
|
||||
<li>Опциональные фильтры: <code>AND space_id = $N</code>, <code>AND created_by = $N</code>, <code>AND (title ILIKE '%' || $N || '%' OR description ILIKE '%' || $N || '%')</code></li>
|
||||
<li>Возвращать поле <code>space</code> (id, name, color) через JOIN</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h4 id="internaldtogantt"><code>internal/dto/gantt/</code></h4>
|
||||
<ul>
|
||||
<li>Новые DTO: <code>GanttChartListFilters</code>, <code>GanttChartWithSpaceResponse</code>, <code>GanttFiltersResponse</code></li>
|
||||
</ul>
|
||||
<p><strong>SQL (пример):</strong></p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> gc.<span style="color:#f92672">*</span>, s.id <span style="color:#66d9ef">as</span> space_id, s.name <span style="color:#66d9ef">as</span> space_name, s.color <span style="color:#66d9ef">as</span> space_color,
|
||||
</span></span><span style="display:flex;"><span> u.display_name <span style="color:#66d9ef">as</span> created_by_name
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> gantt_charts gc
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">JOIN</span> spaces s <span style="color:#66d9ef">ON</span> s.id <span style="color:#f92672">=</span> gc.space_id
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">JOIN</span> space_members sm <span style="color:#66d9ef">ON</span> sm.space_id <span style="color:#f92672">=</span> s.id
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LEFT</span> <span style="color:#66d9ef">JOIN</span> users u <span style="color:#66d9ef">ON</span> u.id <span style="color:#f92672">=</span> gc.created_by
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> sm.user_id <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">1</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">AND</span> sm.status <span style="color:#f92672">=</span> <span style="color:#e6db74">'accepted'</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">AND</span> (<span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">2</span>::uuid <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">OR</span> gc.space_id <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">2</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">AND</span> (<span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">3</span>::uuid <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">OR</span> gc.created_by <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">3</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">AND</span> (<span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">4</span>::text <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">OR</span> gc.title <span style="color:#66d9ef">ILIKE</span> <span style="color:#e6db74">'%'</span> <span style="color:#f92672">||</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">4</span> <span style="color:#f92672">||</span> <span style="color:#e6db74">'%'</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">OR</span> gc.description <span style="color:#66d9ef">ILIKE</span> <span style="color:#e6db74">'%'</span> <span style="color:#f92672">||</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">4</span> <span style="color:#f92672">||</span> <span style="color:#e6db74">'%'</span>)
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">CASE</span> <span style="color:#66d9ef">WHEN</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">5</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">'title'</span> <span style="color:#66d9ef">THEN</span> gc.title <span style="color:#66d9ef">END</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">CASE</span> <span style="color:#66d9ef">WHEN</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">5</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">'created_at'</span> <span style="color:#66d9ef">THEN</span> gc.created_at::text <span style="color:#66d9ef">END</span>,
|
||||
</span></span><span style="display:flex;"><span> gc.updated_at <span style="color:#66d9ef">DESC</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LIMIT</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">6</span> <span style="color:#66d9ef">OFFSET</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">7</span>;
|
||||
</span></span></code></pre></div><hr>
|
||||
<h2 id="3-изменения-frontend-angular">3. Изменения Frontend (Angular)</h2>
|
||||
<h3 id="31-toolbar-фильтров">3.1. Toolbar фильтров</h3>
|
||||
<p>Добавить toolbar над списком диаграмм:</p>
|
||||
<pre tabindex="0"><code>┌──────────────────────────────────────────────────────────────────┐
|
||||
│ [🔍 Поиск... ] [▼ Пространство] │
|
||||
│ [▼ Создатель] [▼ Сортировка] [Сбросить фильтры] │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
</code></pre><p><strong>Компоненты:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Строка поиска</strong> — input с debounce (300ms), поиск по назвению и описанию</li>
|
||||
<li><strong>Dropdown «Пространство»</strong> — список из <code>/gantt-charts/filters</code>, с цветовой меткой</li>
|
||||
<li><strong>Dropdown «Создатель»</strong> — список уникальных создателей</li>
|
||||
<li><strong>Dropdown «Сортировка»</strong> — по дате обновления, дате создания, названию</li>
|
||||
<li><strong>Кнопка «Сбросить»</strong> — очищает все фильтры</li>
|
||||
</ul>
|
||||
<p><strong>Состояние фильтров в URL</strong> (query params):</p>
|
||||
<pre tabindex="0"><code>/gantt?search=миграция&space_id=xxx&sort=updated_at
|
||||
</code></pre><p>Это позволяет делиться ссылкой с уже настроенными фильтрами.</p>
|
||||
<h3 id="32-карточка-диаграммы--бейдж-пространства">3.2. Карточка диаграммы — бейдж пространства</h3>
|
||||
<p>На каждой карточке добавить бейдж с названием пространства:</p>
|
||||
<pre tabindex="0"><code>┌─────────────────────────────────────┐
|
||||
│ 🟠 IT Ландшафт │ ← бейдж с цветом пространства
|
||||
│ Миграция backend │
|
||||
│ Перевод PHP→Go, этап 2 │
|
||||
│ Создал: Антон А. · 07.04.2026 │
|
||||
└─────────────────────────────────────┘
|
||||
</code></pre><h3 id="33-форма-создания-диаграммы--выбор-пространства">3.3. Форма создания диаграммы — выбор пространства</h3>
|
||||
<p>В модальном окне создания:</p>
|
||||
<ul>
|
||||
<li>Dropdown со списком пространств, где пользователь имеет роль <strong>editor</strong> или выше</li>
|
||||
<li>Валидация: если пользователь не editor ни в одном пространстве — показать сообщение «Нет доступных пространств для создания»</li>
|
||||
</ul>
|
||||
<p><strong>Запрос доступных пространств:</strong> переиспользовать существующий <code>GET /api/v1/spaces</code> с фильтрацией на клиенте по <code>role ∈ [owner, admin, editor]</code>, либо добавить параметр <code>?min_role=editor</code> на backend.</p>
|
||||
<h3 id="34-изменение-routing">3.4. Изменение routing</h3>
|
||||
<p><strong>Было:</strong></p>
|
||||
<pre tabindex="0"><code>/gantt → список гантов текущего пространства
|
||||
</code></pre><p><strong>Стало:</strong></p>
|
||||
<pre tabindex="0"><code>/gantt → список всех доступных гантов (из всех пространств)
|
||||
</code></pre><p>Убрать зависимость от <code>SpaceStorageService.getSelectedSpaceId()</code> в списке гантов.</p>
|
||||
<h3 id="35-frontend--файлы-для-изменения">3.5. Frontend — файлы для изменения</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Файл</th>
|
||||
<th>Изменение</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>pages/gantt/services/gantt.service.ts</code></td>
|
||||
<td>Новый метод <code>listAll(filters)</code>, <code>getFilters()</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pages/gantt/gantt.component.ts</code></td>
|
||||
<td>Подключить toolbar, парсинг URL-параметров</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pages/gantt/gantt.component.html</code></td>
|
||||
<td>Добавить toolbar, бейджи на карточках</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pages/gantt/components/gantt-card/</code></td>
|
||||
<td>Добавить бейдж пространства</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pages/gantt/components/create-chart-modal/</code></td>
|
||||
<td>Dropdown выбора пространства</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>core/models/</code></td>
|
||||
<td>Обновить модели: добавить <code>space</code> в GanttChart</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="4-обратная-совместимость">4. Обратная совместимость</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Endpoint</th>
|
||||
<th>Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>GET /api/v1/spaces/{id}/gantt-charts</code></td>
|
||||
<td>Сохранить (для детального view внутри пространства)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GET /api/v1/gantt-charts</code></td>
|
||||
<td><strong>Новый</strong> — глобальный список</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GET /api/v1/gantt-charts/filters</code></td>
|
||||
<td><strong>Новый</strong> — данные для dropdown’ов</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>POST /api/v1/spaces/{id}/gantt-charts</code></td>
|
||||
<td>Сохранить (создание в контексте пространства)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Все CRUD для sections/tasks</td>
|
||||
<td>Без изменений</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="5-критерии-приёмки">5. Критерии приёмки</h2>
|
||||
<ul>
|
||||
<li><input disabled="" type="checkbox"> Endpoint <code>GET /api/v1/gantt-charts</code> возвращает все доступные диаграммы</li>
|
||||
<li><input disabled="" type="checkbox"> Фильтр по <code>space_id</code> — показывает только диаграммы конкретного пространства</li>
|
||||
<li><input disabled="" type="checkbox"> Фильтр по <code>created_by</code> — показывает только диаграммы выбранного создателя</li>
|
||||
<li><input disabled="" type="checkbox"> Поиск <code>search</code> — ищет по title и description (case-insensitive)</li>
|
||||
<li><input disabled="" type="checkbox"> Dropdown фильтров заполняется реальными данными из <code>/filters</code></li>
|
||||
<li><input disabled="" type="checkbox"> Карточка диаграммы показывает бейдж пространства с цветом</li>
|
||||
<li><input disabled="" type="checkbox"> Форма создания предлагает только пространства с ролью ≥ editor</li>
|
||||
<li><input disabled="" type="checkbox"> Состояние фильтров сохраняется в URL (можно поделиться ссылкой)</li>
|
||||
<li><input disabled="" type="checkbox"> Старые endpoints (<code>/spaces/{id}/gantt-charts</code>) продолжают работать</li>
|
||||
<li><input disabled="" type="checkbox"> Swagger документация обновлена</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="6-порядок-реализации">6. Порядок реализации</h2>
|
||||
<ol>
|
||||
<li>Backend: DTO для новых запросов/ответов</li>
|
||||
<li>Backend: Repository — SQL-запрос с фильтрами + JOIN на spaces</li>
|
||||
<li>Backend: Service — <code>ListAll</code>, <code>GetFilters</code></li>
|
||||
<li>Backend: Handler — <code>ListAllGanttCharts</code>, <code>GetGanttFilters</code>, регистрация маршрутов</li>
|
||||
<li>Backend: Swagger — обновить документацию</li>
|
||||
<li>Frontend: Модели — обновить GanttChart, добавить Space в ответ</li>
|
||||
<li>Frontend: Service — <code>listAll()</code>, <code>getFilters()</code></li>
|
||||
<li>Frontend: Toolbar компонент — поиск + dropdown’ы</li>
|
||||
<li>Frontend: Карточка — бейдж пространства</li>
|
||||
<li>Frontend: Модальное окно создания — dropdown пространства</li>
|
||||
<li>Frontend: URL-параметры — синхронизация фильтров с router</li>
|
||||
<li>Тестирование</li>
|
||||
</ol>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,298 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera — Публичный доступ к диаграмме Ганта — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera — Публичный доступ к диаграмме Ганта</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/leadera/leadera-gantt-public-access.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/leadera\/leadera-gantt-public-access.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../../tags/public-access/" class="tag">public-access</a>
|
||||
|
||||
<a href="../../tags/sharing/" class="tag">sharing</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h1 id="публичный-доступ-к-диаграмме-ганта-share">Публичный доступ к диаграмме Ганта (Share)</h1>
|
||||
<p><strong>Дата:</strong> 09.04.2026
|
||||
<strong>Статус:</strong> Требования
|
||||
<strong>Проект:</strong> Leadera</p>
|
||||
<hr>
|
||||
<h2 id="общее-описание">Общее описание</h2>
|
||||
<p>Пользователь может создать публичную ссылку на фрагмент диаграммы Ганта, доступную для просмотра без авторизации. Открывший ссылку видит диаграмму в read-only режиме с возможностью масштабирования таймлайна, но без каких-либо элементов управления (создание, редактирование, удаление задач/секций/вех).</p>
|
||||
<hr>
|
||||
<h2 id="пользовательский-сценарий-создатель-ссылки">Пользовательский сценарий (создатель ссылки)</h2>
|
||||
<ol>
|
||||
<li>Пользователь открывает диаграмму Ганта проекта</li>
|
||||
<li>Нажимает кнопку «Экспорт» / «Общий доступ»</li>
|
||||
<li>Указывает диапазон дат, который должен отображаться на диаграмме</li>
|
||||
<li>Система генерирует уникальную ссылку (UUID) и показывает её пользователю</li>
|
||||
<li>Пользователь копирует ссылку и передаёт заинтересованным лицам</li>
|
||||
</ol>
|
||||
<h3 id="управление-ссылками">Управление ссылками</h3>
|
||||
<ul>
|
||||
<li>Вызывается через меню экспорта → модальное окно «Общий доступ»</li>
|
||||
<li>В модальном окне отображается список созданных ссылок с указанием диапазона дат</li>
|
||||
<li>Каждую ссылку можно <strong>удалить</strong> (полное удаление из БД, не soft delete)</li>
|
||||
<li>Ссылки <strong>нельзя редактировать</strong> — только создавать и удалять</li>
|
||||
<li>Ограничений на количество активных ссылок нет</li>
|
||||
<li>Статистика просмотров (сколько раз открывали) — <strong>не нужна</strong></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="пользовательский-сценарий-зритель-по-ссылке">Пользовательский сценарий (зритель по ссылке)</h2>
|
||||
<ol>
|
||||
<li>Зритель переходит по ссылке (без авторизации)</li>
|
||||
<li>Открывается отдельная страница с диаграммой Ганта</li>
|
||||
<li>Зритель видит:
|
||||
<ul>
|
||||
<li>Задачи и вехи, попадающие в заданный диапазон</li>
|
||||
<li>Секции (заголовки группировок)</li>
|
||||
<li>Таймлайн с цветовой раскраской задач</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Зритель может:
|
||||
<ul>
|
||||
<li>Масштабировать таймлайн</li>
|
||||
<li>Переключать отображение «по ячейкам» / «по дням»</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Зритель <strong>не может</strong>:
|
||||
<ul>
|
||||
<li>Создавать, редактировать, удалять задачи / секции / вехи</li>
|
||||
<li>Видеть любые контролы управления (кнопки создания, контекстные меню, drag-and-drop и т.д.)</li>
|
||||
<li>Авторизоваться через эту страницу</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<hr>
|
||||
<h2 id="данные-и-фильтрация">Данные и фильтрация</h2>
|
||||
<h3 id="что-попадает-в-представление">Что попадает в представление</h3>
|
||||
<p>Задачи и вехи, у которых хотя бы одно из условий верно:</p>
|
||||
<ul>
|
||||
<li>Дата начала попадает в диапазон ссылки</li>
|
||||
<li>Дата конца попадает в диапазон ссылки</li>
|
||||
<li>Задача начинается до диапазона и заканчивается после (полностью перекрывает)</li>
|
||||
</ul>
|
||||
<p>Секции включаются, если в них есть хотя бы одна попавшая задача.</p>
|
||||
<h3 id="api-возвращает">API возвращает</h3>
|
||||
<ul>
|
||||
<li>Только задачи и вехи, попадающие в диапазон (не всю диаграмму)</li>
|
||||
<li>Информацию о секциях, содержащих попавшие задачи</li>
|
||||
<li>Минимальный набор полей, необходимый для отрисовки (без внутренних ID пользователей и т.п.)</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="технические-требования">Технические требования</h2>
|
||||
<h3 id="ссылки">Ссылки</h3>
|
||||
<ul>
|
||||
<li>Формат: UUID v4 (каждый раз новый)</li>
|
||||
<li>Срок действия: бессрочно (пока не удалена)</li>
|
||||
<li>Удаление: <strong>жёсткое</strong> (hard delete из БД)</li>
|
||||
</ul>
|
||||
<h3 id="backend-go-api">Backend (Go API)</h3>
|
||||
<h4 id="таблица-бд-gantt_public_links">Таблица БД: <code>gantt_public_links</code></h4>
|
||||
<ul>
|
||||
<li><code>id</code> (UUID, PK)</li>
|
||||
<li><code>project_id</code> (FK → projects)</li>
|
||||
<li><code>date_from</code> (date)</li>
|
||||
<li><code>date_to</code> (date)</li>
|
||||
<li><code>created_by</code> (FK → users)</li>
|
||||
<li><code>created_at</code> (timestamp)</li>
|
||||
</ul>
|
||||
<h4 id="endpoints">Endpoints</h4>
|
||||
<ul>
|
||||
<li><code>POST /api/v1/gantt/:projectId/public-links</code> — создать ссылку (авторизован, участник проекта)
|
||||
<ul>
|
||||
<li>Body: <code>{ dateFrom, dateTo }</code></li>
|
||||
<li>Response: <code>{ url: "https://leadera.pro/share/<uuid>" }</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><code>GET /api/v1/gantt/:projectId/public-links</code> — список ссылок проекта (авторизован, участник)</li>
|
||||
<li><code>DELETE /api/v1/gantt/public-links/:uuid</code> — удалить ссылку (авторизован, создатель)</li>
|
||||
<li><code>GET /api/v1/public/gantt/:uuid</code> — получить данные диаграммы по ссылке (без авторизации)
|
||||
<ul>
|
||||
<li>Response: задачи, вехи, секции в рамках диапазона</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h4 id="rate-limiting">Rate limiting</h4>
|
||||
<ul>
|
||||
<li>На публичный endpoint <code>GET /api/v1/public/gantt/:uuid</code> — rate limiting для защиты от перебора</li>
|
||||
<li>Рекомендация: 60 запросов в минуту на IP</li>
|
||||
</ul>
|
||||
<h3 id="frontend-angular">Frontend (Angular)</h3>
|
||||
<h4 id="модальное-окно-общий-доступ">Модальное окно «Общий доступ»</h4>
|
||||
<ul>
|
||||
<li>Вызывается из меню экспорта</li>
|
||||
<li>Содержит:
|
||||
<ul>
|
||||
<li>Форму создания ссылки (диапазон дат + кнопка «Создать»)</li>
|
||||
<li>Список существующих ссылок (диапазон + URL + кнопка «Копировать» + кнопка «Удалить»)</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h4 id="публичная-страница-shareuuid">Публичная страница <code>/share/:uuid</code></h4>
|
||||
<ul>
|
||||
<li>Отдельный route, <strong>не требует авторизации</strong></li>
|
||||
<li>Отображает диаграмму Ганта в read-only режиме</li>
|
||||
<li>Доступны только: масштабирование таймлайна, переключение ячейки/дни</li>
|
||||
<li>Все контролы управления скрыты (удалены из шаблона, не просто disabled)</li>
|
||||
<li>Минимальный layout — без навигации проекта, без сайдбара</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="открытые-вопросы">Открытые вопросы</h2>
|
||||
<p><em>(Нет)</em></p>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,618 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera: Вехи (Milestones) на диаграмме Ганта — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera: Вехи (Milestones) на диаграмме Ганта</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/leadera/leadera-milestones.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/leadera\/leadera-milestones.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../../tags/angular/" class="tag">angular</a>
|
||||
|
||||
<a href="../../tags/go/" class="tag">go</a>
|
||||
|
||||
<a href="../../tags/api/" class="tag">api</a>
|
||||
|
||||
<a href="../../tags/ux/" class="tag">ux</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h2 id="бизнес-требования">Бизнес-требования</h2>
|
||||
<h3 id="br-1-сущность-веха-milestone">BR-1: Сущность «Веха» (Milestone)</h3>
|
||||
<p>Веха — это маркер на таймлайне диаграммы Ганта, обозначающий важную дату в проекте. В отличие от задач, веха не имеет длительности и привязана к конкретной дате.</p>
|
||||
<p><strong>Поля вехи:</strong></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Поле</th>
|
||||
<th>Тип</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>id</td>
|
||||
<td>UUID</td>
|
||||
<td>Идентификатор</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>gantt_chart_id</td>
|
||||
<td>UUID</td>
|
||||
<td>Принадлежность к диаграмме (не к секции!)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>title</td>
|
||||
<td>string</td>
|
||||
<td>Название вехи (обязательное, 1–255 символов)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>date</td>
|
||||
<td>date</td>
|
||||
<td>Дата вехи (без времени, обязательная)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>color</td>
|
||||
<td>string</td>
|
||||
<td>Цвет маркера (HEX, по умолчанию <code>#dc3545</code> — красный)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>created_by</td>
|
||||
<td>UUID</td>
|
||||
<td>Кто создал (не отображается в UI, хранится для аудита)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>updated_by</td>
|
||||
<td>UUID</td>
|
||||
<td>Кто обновил (не отображается в UI)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>created_at</td>
|
||||
<td>timestamp</td>
|
||||
<td>Дата создания</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>updated_at</td>
|
||||
<td>timestamp</td>
|
||||
<td>Дата обновления</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>deleted_at</td>
|
||||
<td>timestamp</td>
|
||||
<td>Soft delete</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Права доступа:</strong> Создание, редактирование и удаление вех доступно участникам с правами выше «только чтение» (аналогично задачам).</p>
|
||||
<p><strong>Особенности:</strong></p>
|
||||
<ul>
|
||||
<li>Вех может быть несколько в проекте</li>
|
||||
<li>Несколько вех могут иметь одну и ту же дату</li>
|
||||
<li>Вехи сортируются строго по дате (по возрастанию)</li>
|
||||
<li>Вехи не привязаны к секциям — они отображаются <strong>сквозь все секции</strong></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3 id="br-2-управление-вехами-модальное-окно">BR-2: Управление вехами (модальное окно)</h3>
|
||||
<p><strong>Вызов:</strong> Кнопка «Вехи» рядом с кнопкой «Создать секцию» на странице диаграммы.</p>
|
||||
<p><strong>Модальное окно содержит:</strong></p>
|
||||
<ul>
|
||||
<li>Заголовок: «Вехи проекта»</li>
|
||||
<li>Список всех вех текущей диаграммы (сортировка по дате по возрастанию)</li>
|
||||
<li>Каждая веха в списке показывает: цвет (индикатор), название, дату, кнопки «Редактировать» и «Удалить»</li>
|
||||
<li>Кнопка «Добавить веху»</li>
|
||||
</ul>
|
||||
<p><strong>Форма добавления/редактирования вехи:</strong></p>
|
||||
<ul>
|
||||
<li>Название — текстовый ввод (обязательное, 1–255 символов)</li>
|
||||
<li>Дата — datepicker (обязательное)</li>
|
||||
<li>Цвет — выбор цвета (по аналогии с палитрой цветов в задачах)</li>
|
||||
<li>По умолчанию цвет = красный (<code>#dc3545</code>)</li>
|
||||
</ul>
|
||||
<p><strong>Удаление:</strong> с подтверждением (confirm dialog).</p>
|
||||
<hr>
|
||||
<h3 id="br-3-отображение-вех-на-таймлайне">BR-3: Отображение вех на таймлайне</h3>
|
||||
<p><strong>Визуальное представление:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Маркер-флажок</strong> (flag marker) на шкале таймлайна</li>
|
||||
<li>От маркера вниз идёт <strong>пунктирная вертикальная линия</strong> через все секции (на всю высоту диаграммы)</li>
|
||||
<li>Цвет линии и маркера = цвет вехи</li>
|
||||
</ul>
|
||||
<p><strong>Позиционирование:</strong></p>
|
||||
<ul>
|
||||
<li>Веха ставится ровно на свою дату на шкале времени</li>
|
||||
<li>Если дата вехи выходит за рамки текущего диапазона задач — таймлайн <strong>расширяется</strong> (аналогично задачам)</li>
|
||||
<li>При изменении масштаба (days/weeks/months/quarters) позиция вехи пересчитывается</li>
|
||||
</ul>
|
||||
<p><strong>Tooltip при наведении:</strong></p>
|
||||
<ul>
|
||||
<li>При наведении на маркер/линию — показывается tooltip с названием вехи</li>
|
||||
<li>Если на одну дату приходится несколько вех — в tooltip показываются <strong>все</strong> вехи этой даты с их названиями (каждая с индикацией своего цвета)</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="технические-требования">Технические требования</h2>
|
||||
<h3 id="база-данных">База данных</h3>
|
||||
<h4 id="новая-таблица-gantt_milestones">Новая таблица: gantt_milestones</h4>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> gantt_milestones (
|
||||
</span></span><span style="display:flex;"><span> id UUID <span style="color:#66d9ef">PRIMARY</span> <span style="color:#66d9ef">KEY</span> <span style="color:#66d9ef">DEFAULT</span> gen_random_uuid(),
|
||||
</span></span><span style="display:flex;"><span> gantt_chart_id UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> gantt_charts(id) <span style="color:#66d9ef">ON</span> <span style="color:#66d9ef">DELETE</span> <span style="color:#66d9ef">CASCADE</span>,
|
||||
</span></span><span style="display:flex;"><span> title VARCHAR(<span style="color:#ae81ff">255</span>) <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> date DATE <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> color VARCHAR(<span style="color:#ae81ff">20</span>) <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">DEFAULT</span> <span style="color:#e6db74">'#dc3545'</span>,
|
||||
</span></span><span style="display:flex;"><span> created_by UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> users(id),
|
||||
</span></span><span style="display:flex;"><span> updated_by UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> users(id),
|
||||
</span></span><span style="display:flex;"><span> created_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span> <span style="color:#66d9ef">DEFAULT</span> NOW(),
|
||||
</span></span><span style="display:flex;"><span> updated_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span> <span style="color:#66d9ef">DEFAULT</span> NOW(),
|
||||
</span></span><span style="display:flex;"><span> deleted_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span>
|
||||
</span></span><span style="display:flex;"><span>);
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_milestones_chart <span style="color:#66d9ef">ON</span> gantt_milestones(gantt_chart_id);
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_milestones_date <span style="color:#66d9ef">ON</span> gantt_milestones(date);
|
||||
</span></span></code></pre></div><hr>
|
||||
<h3 id="backend-go-api">Backend (Go API)</h3>
|
||||
<h4 id="этап-1-миграция--домен">Этап 1: Миграция + Домен</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Добавь в проект новую таблицу <code>gantt_milestones</code>.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Создай миграцию в <code>cmd/migrate/</code>:</p>
|
||||
<ul>
|
||||
<li>Новая таблица <code>gantt_milestones</code> с полями: id (UUID PK), gantt_chart_id (UUID FK → gantt_charts, ON DELETE CASCADE), title (VARCHAR 255 NOT NULL), date (DATE NOT NULL), color (VARCHAR 20 NOT NULL DEFAULT ‘#dc3545’), created_by (UUID FK → users), updated_by (UUID FK → users), created_at, updated_at, deleted_at. Индексы по gantt_chart_id и date.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Создай доменную модель <code>GanttMilestone</code> в <code>internal/domain/gantt.go</code>:</p>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GanttMilestone</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ID</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"id"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">GanttChartID</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"gantt_chart_id"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"title"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"date"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"color"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedBy</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"created_by"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedBy</span> <span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"updated_by"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"created_at"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"updated_at"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">DeletedAt</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"deleted_at,omitempty"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div></blockquote>
|
||||
<h4 id="этап-2-crud-для-ganttmilestones">Этап 2: CRUD для GanttMilestones</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Реализуй полный CRUD для сущности <code>GanttMilestone</code> по аналогии с существующими handlers/services/repositories в проекте.</p>
|
||||
<p><strong>Роуты:</strong></p>
|
||||
<ul>
|
||||
<li><code>GET /api/gantt-charts/:chartId/milestones</code> — список вех диаграммы</li>
|
||||
<li><code>POST /api/gantt-charts/:chartId/milestones</code> — создать веху</li>
|
||||
<li><code>PUT /api/gantt-charts/:chartId/milestones/:milestoneId</code> — обновить веху</li>
|
||||
<li><code>DELETE /api/gantt-charts/:chartId/milestones/:milestoneId</code> — удалить веху (soft delete)</li>
|
||||
</ul>
|
||||
<p><strong>Repository:</strong> <code>internal/repository/gantt_milestone_repository.go</code></p>
|
||||
<ul>
|
||||
<li><code>GetByChartID(ctx, chartID) ([]domain.GanttMilestone, error)</code> — все неудалённые вехи диаграммы, отсортированные по date ASC</li>
|
||||
<li><code>GetByID(ctx, id) (*domain.GanttMilestone, error)</code></li>
|
||||
<li><code>Create(ctx, milestone) error</code></li>
|
||||
<li><code>Update(ctx, milestone) error</code></li>
|
||||
<li><code>Delete(ctx, id) error</code> — soft delete (set deleted_at)</li>
|
||||
<li>Все запросы — raw SQL через pgx, как в остальных репозиториях проекта</li>
|
||||
</ul>
|
||||
<p><strong>Service:</strong> <code>internal/service/gantt_milestone_service.go</code></p>
|
||||
<ul>
|
||||
<li>Валидация: title (1–255 символов), date (обязательное), color (валидный HEX, default ‘#dc3545’)</li>
|
||||
<li>Проверка прав доступа: пользователь должен иметь права выше «только чтение» в пространстве (space) диаграммы</li>
|
||||
<li>Проверка что chartId существует и не архивирован</li>
|
||||
</ul>
|
||||
<p><strong>Handler:</strong> <code>internal/handler/gantt_milestone_handler.go</code></p>
|
||||
<ul>
|
||||
<li>Swagger-аннотации для всех эндпоинтов</li>
|
||||
<li>Валидация входных данных</li>
|
||||
<li>Ошибки в формате проекта</li>
|
||||
</ul>
|
||||
<p><strong>DTO:</strong> <code>internal/dto/gantt/milestones_dto.go</code></p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">CreateGanttMilestoneRequest</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"title" binding:"required,min=1,max=255"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"date" binding:"required"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"color" binding:"omitempty,max=20"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">UpdateGanttMilestoneRequest</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"title" binding:"omitempty,min=1,max=255"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"date" binding:"omitempty"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#f92672">*</span><span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"color" binding:"omitempty,max=20"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GanttMilestoneResponse</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ID</span> <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"id"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">GanttChartID</span> <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">UUID</span> <span style="color:#e6db74">`json:"gantt_chart_id"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Title</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"title"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Date</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"date"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Color</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"color"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedByUser</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CreatedByUpdatedUser</span> <span style="color:#e6db74">`json:"created_by_user"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedByUser</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CreatedByUpdatedUser</span> <span style="color:#e6db74">`json:"updated_by_user"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"created_at"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedAt</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"updated_at"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">GanttMilestonesListResponse</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Milestones</span> []<span style="color:#a6e22e">GanttMilestoneResponse</span> <span style="color:#e6db74">`json:"milestones"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><p><strong>Регистрация роутов:</strong> Добавь группу milestone-роутов в роутер рядом с существующими роутами секций/задач. Проверка прав через существующий middleware.</p>
|
||||
</blockquote>
|
||||
<h4 id="этап-3-включение-вех-в-ответ-диаграммы">Этап 3: Включение вех в ответ диаграммы</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>При запросе данных диаграммы Ганта (полная загрузка) включай связанные вехи в ответ.</p>
|
||||
<ol>
|
||||
<li>Добавь эндпоинт или расширь существующий <code>GET /api/gantt-charts/:chartId</code> — включить поле <code>milestones</code> с массивом <code>GanttMilestoneResponse</code></li>
|
||||
<li>Вехи с <code>deleted_at IS NOT NULL</code> не возвращаются</li>
|
||||
<li>Вехи сортируются по <code>date</code> ASC</li>
|
||||
<li>Вехи загружаются вместе с данными диаграммы (tasks, sections) — один запрос при открытии Ганта</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<h3 id="frontend-angular">Frontend (Angular)</h3>
|
||||
<h4 id="этап-fe-1-модели--сервис">Этап FE-1: Модели + Сервис</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Добавь в проект поддержку вех (milestones) на диаграмме Ганта.</p>
|
||||
<ol>
|
||||
<li>В <code>pages/gantt/models/</code> (или соответствующем файле моделей) добавь:</li>
|
||||
</ol>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttMilestone</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">gantt_chart_id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">color</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_by_user</span><span style="color:#f92672">?:</span> { <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>; <span style="color:#a6e22e">full_name</span>: <span style="color:#66d9ef">string</span> };
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_by_user</span><span style="color:#f92672">?:</span> { <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>; <span style="color:#a6e22e">full_name</span>: <span style="color:#66d9ef">string</span> };
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_at</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_at</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttMilestoneCreateRequest</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">color?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttMilestoneUpdateRequest</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">color?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><ol start="2">
|
||||
<li>
|
||||
<p>Добавь <code>milestones: GanttMilestone[]</code> в интерфейс/модель полной загрузки данных диаграммы</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Создай сервис <code>pages/gantt/services/gantt-milestone.service.ts</code>:</p>
|
||||
</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>Inject HttpClient</li>
|
||||
<li>Методы:
|
||||
<ul>
|
||||
<li><code>getMilestones(chartId: string): Observable<GanttMilestone[]></code></li>
|
||||
<li><code>createMilestone(chartId: string, data: GanttMilestoneCreateRequest): Observable<GanttMilestone></code></li>
|
||||
<li><code>updateMilestone(chartId: string, milestoneId: string, data: GanttMilestoneUpdateRequest): Observable<GanttMilestone></code></li>
|
||||
<li><code>deleteMilestone(chartId: string, milestoneId: string): Observable<void></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Все методы возвращают Observable с типизированными ответами (по аналогии с GanttChartService)</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-2-модальное-окно-управления-вехами">Этап FE-2: Модальное окно управления вехами</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Создай компонент модального окна для управления вехами диаграммы Ганта.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Создай компонент <code>pages/gantt/components/milestones-modal/</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Кнопка вызова:</strong> Добавь кнопку «Вехи» рядом с кнопкой «Создать секцию» в toolbar диаграммы. По клику — открытие модального окна через ng-bootstrap NgbModal.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Модальное окно</strong> (ng-bootstrap modal):</p>
|
||||
<ul>
|
||||
<li>Заголовок: «Вехи проекта»</li>
|
||||
<li>Список вех (сортировка по date ASC):
|
||||
<ul>
|
||||
<li>Каждый элемент: цветной кружок (color), название, дата (форматированная), кнопки ✏️ (редактировать) и 🗑️ (удалить с confirm)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Кнопка «Добавить веху» внизу списка</li>
|
||||
<li>Если вех нет — сообщение «Вех пока нет. Добавьте первую!»</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Форма добавления/редактирования</strong> (inline в модальном окне):</p>
|
||||
<ul>
|
||||
<li>Название — текстовый ввод (обязательное)</li>
|
||||
<li>Дата — ng-bootstrap datepicker (обязательное)</li>
|
||||
<li>Цвет — палитра цветов (использовать тот же компонент/подход, что и для задач)</li>
|
||||
<li>По умолчанию цвет = <code>#dc3545</code> (красный)</li>
|
||||
<li>Кнопки «Сохранить» и «Отмена»</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>При сохранении — вызов соответствующего метода сервиса (create/update)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>При удалении — confirm dialog → вызов deleteMilestone</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>После любого изменения — обновить список вех и обновить данные на канвасе (через сервис/GanttStore)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Инжектировать GanttMilestoneService и NgbModal</p>
|
||||
</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-3-отображение-вех-на-таймлайне-konva-canvas">Этап FE-3: Отображение вех на таймлайне (Konva canvas)</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Отобрази вехи (milestones) на canvas-диаграмме Ганта.</p>
|
||||
<p>Проект использует Konva.js для рендеринга.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>При загрузке данных диаграммы — извлечь массив <code>milestones</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Для каждой вехи нарисовать на canvas:</p>
|
||||
<ul>
|
||||
<li><strong>Маркер-флажок</strong> (flag marker) на позиции даты вехи на шкале времени
|
||||
<ul>
|
||||
<li>Стиль: маленький треугольник или флаг на верхней границе таймлайна</li>
|
||||
<li>Цвет = milestone.color</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Пунктирная вертикальная линия</strong> от маркера вниз через все секции (на всю высоту области секций)
|
||||
<ul>
|
||||
<li>Стиль: Konva.Line, dash: [5, 5], strokeWidth: 1.5, color = milestone.color, opacity: 0.7</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Позиционирование:</strong></p>
|
||||
<ul>
|
||||
<li>Рассчитать X-позицию по дате вехи с использованием текущей шкалы времени (как для задач)</li>
|
||||
<li>Если дата вехи выходит за текущий диапазон — расширить диапазон (влияет на общую ширину таймлайна)</li>
|
||||
<li>При изменении масштаба (days/weeks/months/quarters) — пересчитать позиции всех вех</li>
|
||||
<li>При скролле — вехи скроллятся вместе с задачами</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Tooltip при наведении:</strong></p>
|
||||
<ul>
|
||||
<li>При наведении на маркер или пунктирную линию — показать Konva.Tooltip или HTML tooltip:
|
||||
<ul>
|
||||
<li>Если веха одна на эту дату — показать название</li>
|
||||
<li>Если несколько вех на одну дату — показать список: цветной кружок + название для каждой</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Z-index:</strong></p>
|
||||
<ul>
|
||||
<li>Линии вех рисуются <strong>поверх</strong> фона секций, но <strong>под</strong> задачами</li>
|
||||
<li>Маркеры-флажки — поверх всего (самый высокий z-index)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Реактивность:</strong></p>
|
||||
<ul>
|
||||
<li>При добавлении/удалении/редактировании вехи — перерисовать только вехи (без полного ре рендера задач)</li>
|
||||
<li>Подписаться на изменения массива milestones через сервис/store</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<h2 id="порядок-реализации">Порядок реализации</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Этап</th>
|
||||
<th>Сторона</th>
|
||||
<th>Зависимости</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>Миграция + Домен</td>
|
||||
<td>Backend</td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>CRUD вех</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>Включение вех в ответ диаграммы</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>Модели + Сервис</td>
|
||||
<td>Frontend</td>
|
||||
<td>— (параллельно с backend)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>Модальное окно управления вехами</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 4 + backend 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td>Отображение вех на таймлайне</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 4 + backend 3</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,681 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera: Ограничения секций в диаграмме Ганта — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera: Ограничения секций в диаграмме Ганта</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/leadera/leadera-section-constraints.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/leadera\/leadera-section-constraints.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../../tags/angular/" class="tag">angular</a>
|
||||
|
||||
<a href="../../tags/go/" class="tag">go</a>
|
||||
|
||||
<a href="../../tags/api/" class="tag">api</a>
|
||||
|
||||
<a href="../../tags/ux/" class="tag">ux</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h2 id="бизнес-требования">Бизнес-требования</h2>
|
||||
<h3 id="br-1-лимит-одновременных-задач-concurrency-limit">BR-1: Лимит одновременных задач (Concurrency Limit)</h3>
|
||||
<p>У каждой секции появляется параметр <strong>max_concurrent_tasks</strong> (целое число ≥ 0, по умолчанию 0).</p>
|
||||
<p><strong>Поведение:</strong></p>
|
||||
<ul>
|
||||
<li>Если <code>max_concurrent_tasks = 3</code> — в любой данный день в этой секции не может быть больше 3 активных задач одновременно</li>
|
||||
<li>Если <code>max_concurrent_tasks = 0</code> — ограничение отключено, лимита нет</li>
|
||||
<li>Пересечение считается по дате: задача занимает дни с <code>start_date</code> по <code>end_date</code> включительно. Две задачи «пересекаются», если у них есть хотя бы один общий день</li>
|
||||
<li>Ограничение работает <strong>только в рамках одной секции</strong>. Задачи из разных секций не влияют друг на друга</li>
|
||||
</ul>
|
||||
<p><strong>Срабатывание при:</strong></p>
|
||||
<ol>
|
||||
<li>Создании задачи — если добавление нарушает лимит → Alert</li>
|
||||
<li>Перетаскивании задачи (drag) — если новая позиция нарушает лимит → Alert</li>
|
||||
<li>Растягивании задачи (resize) — если новый размер нарушает лимит → Alert</li>
|
||||
<li>Редактировании задачи (форма) — если изменённые даты нарушают лимит → Alert</li>
|
||||
</ol>
|
||||
<p><strong>Важно:</strong> Действие <strong>НЕ отменяется</strong>. Задача создаётся/перемещается в любом случае, но пользователь видит предупреждение.</p>
|
||||
<p><strong>Текст Alert:</strong> «В секции “[название секции]” на [дата/диапазон] превышен лимит одновременных задач (максимум: N)»</p>
|
||||
<hr>
|
||||
<h3 id="br-2-блоки-секций-section-blocks">BR-2: Блоки секций (Section Blocks)</h3>
|
||||
<p>У каждой секции появляется набор <strong>блоков</strong> — особый вид записей, отображаемых на таймлайне как заштрихованные области.</p>
|
||||
<p><strong>Сущность GanttSectionBlock:</strong></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Поле</th>
|
||||
<th>Тип</th>
|
||||
<th>Описание</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>id</td>
|
||||
<td>UUID</td>
|
||||
<td>Идентификатор</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>section_id</td>
|
||||
<td>UUID</td>
|
||||
<td>Принадлежность к секции</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>title</td>
|
||||
<td>string</td>
|
||||
<td>Название (например: «Отпуск», «Командировка»)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>start_date</td>
|
||||
<td>date</td>
|
||||
<td>Начало интервала (минимум 1 день)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>end_date</td>
|
||||
<td>date</td>
|
||||
<td>Конец интервала</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>block_concurrency</td>
|
||||
<td>boolean</td>
|
||||
<td>Запрещать ли пересечение обычных задач с данным блоком</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Валидация:</strong></p>
|
||||
<ul>
|
||||
<li><code>end_date</code> не может быть раньше <code>start_date</code></li>
|
||||
<li>Все поля обязательны</li>
|
||||
<li>Блок может быть в прошлом</li>
|
||||
</ul>
|
||||
<p><strong>Визуальное отображение на Ганте:</strong></p>
|
||||
<ul>
|
||||
<li>Заштрихованная область на <strong>всю высоту секции</strong> (включая все строки задач в секции)</li>
|
||||
<li>Название блока отображается <strong>поверх штриховки с подложкой</strong> (для читаемости)</li>
|
||||
</ul>
|
||||
<p><strong>Поведение block_concurrency:</strong></p>
|
||||
<ul>
|
||||
<li>Если <code>block_concurrency = true</code> — при пересечении задачи с данным блоком показывается Alert (аналогично BR-1), но задача <strong>всё равно создаётся/перемещается</strong></li>
|
||||
<li>Если <code>block_concurrency = false</code> — блок отображается визуально, но не влияет на задачи</li>
|
||||
</ul>
|
||||
<p><strong>Срабатывание при:</strong></p>
|
||||
<ol>
|
||||
<li>Создании задачи — пересекает блок с <code>block_concurrency = true</code> → Alert</li>
|
||||
<li>Перетаскивании задачи (drag) → Alert</li>
|
||||
<li>Растягивании задачи (resize) → Alert</li>
|
||||
<li>Редактировании задачи (форма) → Alert</li>
|
||||
</ol>
|
||||
<p><strong>Текст Alert:</strong> «В секции “[название секции]” задача пересекает блок “[название блока]” ([дата] — [дата])»</p>
|
||||
<hr>
|
||||
<h3 id="br-3-система-alert-ов">BR-3: Система Alert-ов</h3>
|
||||
<p>Все предупреждения (от BR-1 и BR-2) выводятся в <strong>единую область</strong> над диаграммой Ганта.</p>
|
||||
<p><strong>Поведение:</strong></p>
|
||||
<ul>
|
||||
<li>Alert-ы — Bootstrap Alert (warning), <strong>постоянные</strong></li>
|
||||
<li>Пользователь <strong>НЕ может</strong> закрыть Alert вручную</li>
|
||||
<li>Alert исчезает <strong>автоматически</strong>, когда текущая дата прошла дату конфликта (конфликт больше не актуален)</li>
|
||||
<li>Проверка конфликтов — <strong>только на фронтенде</strong> (на основе загруженных данных: задачи секции, блоки, max_concurrent_tasks)</li>
|
||||
<li>Если Alert-ов много — область <strong>сворачиваемая</strong> с счётчиком в бейдже (например: «⚠️ 3 предупреждения»)</li>
|
||||
<li>По умолчанию — развёрнуто</li>
|
||||
<li>Сворачивание/разворачивание — клик по заголовку области</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3 id="br-4-настройки-секции">BR-4: Настройки секции</h3>
|
||||
<ul>
|
||||
<li>Настройки совмещены с <strong>существующим модальным окном редактирования секции</strong> (не отдельное окно)</li>
|
||||
</ul>
|
||||
<p><strong>Окно создания секции</strong> — добавляется:</p>
|
||||
<ul>
|
||||
<li>Поле «Макс. одновременных задач» (числовой ввод, 0 = без лимита, по умолчанию 0)</li>
|
||||
</ul>
|
||||
<p><strong>Окно редактирования секции</strong> — добавляется:</p>
|
||||
<ul>
|
||||
<li>Поле «Макс. одновременных задач» (числовой ввод, 0 = без лимита)</li>
|
||||
<li>Список блоков (таблица/список с CRUD: добавить, редактировать, удалить)
|
||||
<ul>
|
||||
<li><strong>Сортировка:</strong> по <code>start_date</code> по убыванию (будущее сверху)</li>
|
||||
<li>Каждый блок — форма: title, start_date, end_date, block_concurrency (чекбокс: «Запретить пересечение задач»)</li>
|
||||
<li>Валидация: end_date >= start_date, все поля заполнены</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3 id="br-5-наследование">BR-5: Наследование</h3>
|
||||
<ul>
|
||||
<li>При появлении функционала копирования/дублирования диаграммы — настройки секций (лимит + блоки) наследуются</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="технические-требования">Технические требования</h2>
|
||||
<h3 id="база-данных">База данных</h3>
|
||||
<h4 id="новая-таблица-gantt_section_blocks">Новая таблица: gantt_section_blocks</h4>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> gantt_section_blocks (
|
||||
</span></span><span style="display:flex;"><span> id UUID <span style="color:#66d9ef">PRIMARY</span> <span style="color:#66d9ef">KEY</span> <span style="color:#66d9ef">DEFAULT</span> gen_random_uuid(),
|
||||
</span></span><span style="display:flex;"><span> section_id UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> gantt_sections(id) <span style="color:#66d9ef">ON</span> <span style="color:#66d9ef">DELETE</span> <span style="color:#66d9ef">CASCADE</span>,
|
||||
</span></span><span style="display:flex;"><span> title VARCHAR(<span style="color:#ae81ff">255</span>) <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> start_date DATE <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> end_date DATE <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
|
||||
</span></span><span style="display:flex;"><span> block_concurrency BOOLEAN <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">DEFAULT</span> <span style="color:#66d9ef">false</span>,
|
||||
</span></span><span style="display:flex;"><span> created_by UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> users(id),
|
||||
</span></span><span style="display:flex;"><span> updated_by UUID <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">REFERENCES</span> users(id),
|
||||
</span></span><span style="display:flex;"><span> created_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span> <span style="color:#66d9ef">DEFAULT</span> NOW(),
|
||||
</span></span><span style="display:flex;"><span> updated_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span> <span style="color:#66d9ef">DEFAULT</span> NOW(),
|
||||
</span></span><span style="display:flex;"><span> deleted_at <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">WITH</span> TIME <span style="color:#66d9ef">ZONE</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">CONSTRAINT</span> chk_block_dates <span style="color:#66d9ef">CHECK</span> (end_date <span style="color:#f92672">>=</span> start_date)
|
||||
</span></span><span style="display:flex;"><span>);
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_section_blocks_section <span style="color:#66d9ef">ON</span> gantt_section_blocks(section_id);
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_gantt_section_blocks_dates <span style="color:#66d9ef">ON</span> gantt_section_blocks(start_date, end_date);
|
||||
</span></span></code></pre></div><h4 id="изменение-таблицы-gantt_sections">Изменение таблицы: gantt_sections</h4>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> gantt_sections <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> max_concurrent_tasks INTEGER <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span> <span style="color:#66d9ef">DEFAULT</span> <span style="color:#ae81ff">0</span>;
|
||||
</span></span></code></pre></div><hr>
|
||||
<h3 id="backend-go-api">Backend (Go API)</h3>
|
||||
<h4 id="этап-1-миграция--домен">Этап 1: Миграция + Домен</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Добавь в проект новую таблицу <code>gantt_section_blocks</code> и колонку <code>max_concurrent_tasks</code> в <code>gantt_sections</code>.</p>
|
||||
<ol>
|
||||
<li>Создай миграцию в <code>cmd/migrate/</code>:
|
||||
<ul>
|
||||
<li>Новая таблица <code>gantt_section_blocks</code> с полями: id (UUID PK), section_id (UUID FK → gantt_sections, ON DELETE CASCADE), title (VARCHAR 255 NOT NULL), start_date (DATE NOT NULL), end_date (DATE NOT NULL), block_concurrency (BOOLEAN DEFAULT false), created_by (UUID FK → users), updated_by (UUID FK → users), created_at, updated_at, deleted_at. CHECK constraint: end_date >= start_date. Индексы по section_id и (start_date, end_date).</li>
|
||||
<li>ALTER TABLE gantt_sections ADD COLUMN max_concurrent_tasks INTEGER NOT NULL DEFAULT 0.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Создай доменную модель <code>GanttSectionBlock</code> в <code>internal/domain/gantt.go</code> со всеми полями таблицы.</li>
|
||||
<li>Добавь поле <code>MaxConcurrentTasks int</code> в структуру <code>GanttSection</code>.</li>
|
||||
<li>Обнови маппинг в relevant repository файлах, чтобы новое поле <code>max_concurrent_tasks</code> читалось/писалось.</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-2-crud-для-ganttsectionblocks">Этап 2: CRUD для GanttSectionBlocks</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Реализуй полный CRUD для сущности <code>GanttSectionBlock</code> по аналогии с существующими handlers/services/repositories в проекте.</p>
|
||||
<p><strong>Роуты (внутри группы секции):</strong></p>
|
||||
<ul>
|
||||
<li><code>GET /api/gantt-charts/:chartId/sections/:sectionId/blocks</code> — список блоков секции</li>
|
||||
<li><code>POST /api/gantt-charts/:chartId/sections/:sectionId/blocks</code> — создать блок</li>
|
||||
<li><code>PUT /api/gantt-charts/:chartId/sections/:sectionId/blocks/:blockId</code> — обновить блок</li>
|
||||
<li><code>DELETE /api/gantt-charts/:chartId/sections/:sectionId/blocks/:blockId</code> — удалить блок (soft delete)</li>
|
||||
</ul>
|
||||
<p><strong>Repository:</strong> <code>internal/repository/gantt_section_block_repository.go</code></p>
|
||||
<ul>
|
||||
<li><code>GetBySectionID(ctx, sectionID) ([]domain.GanttSectionBlock, error)</code></li>
|
||||
<li><code>GetByID(ctx, id) (*domain.GanttSectionBlock, error)</code></li>
|
||||
<li><code>Create(ctx, block) error</code></li>
|
||||
<li><code>Update(ctx, block) error</code></li>
|
||||
<li><code>Delete(ctx, id) error</code> — soft delete (set deleted_at)</li>
|
||||
<li>Все запросы — raw SQL через pgx, как в остальных репозиториях проекта</li>
|
||||
</ul>
|
||||
<p><strong>Service:</strong> <code>internal/service/gantt_section_block_service.go</code></p>
|
||||
<ul>
|
||||
<li>Бизнес-логика: валидация dates (end >= start), проверка прав доступа к секции</li>
|
||||
<li>Inject repository</li>
|
||||
</ul>
|
||||
<p><strong>Handler:</strong> <code>internal/handler/gantt_section_block_handler.go</code></p>
|
||||
<ul>
|
||||
<li>Swagger-аннотации для всех эндпоинтов</li>
|
||||
<li>Валидация входных данных</li>
|
||||
<li>Ошибки в формате проекта (GanttErrorResponse)</li>
|
||||
</ul>
|
||||
<p><strong>Модели запросов:</strong></p>
|
||||
<ul>
|
||||
<li>Create: title (required), start_date (required), end_date (required), block_concurrency (bool, default false)</li>
|
||||
<li>Update: title, start_date, end_date, block_concurrency — все опциональны</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
<h4 id="этап-3-обновление-настроек-секции">Этап 3: Обновление настроек секции</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Обнови CRUD секций (gantt_sections) для поддержки нового поля <code>max_concurrent_tasks</code>.</p>
|
||||
<ol>
|
||||
<li>Добавь <code>max_concurrent_tasks</code> в <code>GanttSectionCreateRequest</code> (опциональное, int ≥ 0, default 0)</li>
|
||||
<li>Добавь <code>max_concurrent_tasks</code> в <code>GanttSectionUpdateRequest</code> (опциональное, int ≥ 0)</li>
|
||||
<li>Обнови SQL-запросы в repository: INSERT и UPDATE должны включать <code>max_concurrent_tasks</code></li>
|
||||
<li>При GET секции (или списке секций) — возвращать <code>max_concurrent_tasks</code> в ответе</li>
|
||||
<li>Валидация: значение должно быть ≥ 0</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-4-включение-блоков-в-ответ-секций">Этап 4: Включение блоков в ответ секций</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>При запросе списка секций или одной секции включай связанные блоки в ответ.</p>
|
||||
<ol>
|
||||
<li>Обнови <code>GET /api/gantt-charts/:chartId/sections/:sectionId</code> — добавить поле <code>blocks</code> с массивом GanttSectionBlock</li>
|
||||
<li>Обнови <code>GET /api/gantt-charts/:chartId/sections</code> — добавить поле <code>blocks</code> для каждой секции</li>
|
||||
<li>Блоки с <code>deleted_at IS NOT NULL</code> не возвращаются</li>
|
||||
<li>Блоки сортируются по start_date по убыванию (будущее сверху)</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<h3 id="frontend-angular">Frontend (Angular)</h3>
|
||||
<h4 id="этап-fe-1-модели--сервис">Этап FE-1: Модели + Сервис</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Добавь в проект поддержку ограничений секций Ганта.</p>
|
||||
<ol>
|
||||
<li>В <code>pages/gantt/models/gantt-chart.model.ts</code> добавь:</li>
|
||||
</ol>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttSectionBlock</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">section_id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">start_date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_concurrency</span>: <span style="color:#66d9ef">boolean</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_by_user</span>: <span style="color:#66d9ef">UserInfo</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_by_user</span>: <span style="color:#66d9ef">UserInfo</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">created_at</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">updated_at</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttSectionBlockCreateRequest</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">start_date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_concurrency?</span>: <span style="color:#66d9ef">boolean</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">GanttSectionBlockUpdateRequest</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">title?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">start_date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_concurrency?</span>: <span style="color:#66d9ef">boolean</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ConflictCheck</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">has_conflicts</span>: <span style="color:#66d9ef">boolean</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">conflicts</span>: <span style="color:#66d9ef">ConflictItem</span>[];
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ConflictItem</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'concurrency'</span> <span style="color:#f92672">|</span> <span style="color:#e6db74">'block'</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">section_id</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">section_title</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">block_title?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">start_date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">end_date?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">message</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">dates?</span>: <span style="color:#66d9ef">string</span>[];
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><ol start="2">
|
||||
<li>
|
||||
<p>Добавь <code>max_concurrent_tasks: number</code> в интерфейс <code>GanttSection</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Добавь <code>blocks: GanttSectionBlock[]</code> в интерфейс <code>GanttSection</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Добавь <code>max_concurrent_tasks?: number</code> в <code>GanttSectionCreateRequest</code> и <code>GanttSectionUpdateRequest</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Создай сервис <code>pages/gantt/services/gantt-section-block.service.ts</code>:</p>
|
||||
</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>Inject HttpClient</li>
|
||||
<li>Методы: getBlocks(chartId, sectionId), createBlock(chartId, sectionId, data), updateBlock(chartId, sectionId, blockId, data), deleteBlock(chartId, sectionId, blockId)</li>
|
||||
<li>Все методы возвращают Observable с типизированными ответами (по аналогии с GanttChartService)</li>
|
||||
</ul>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-2-интеграция-в-модальные-окна-секции">Этап FE-2: Интеграция в модальные окна секции</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Интегрируй настройки ограничений в существующие модальные окна создания и редактирования секции Ганта.</p>
|
||||
<ol>
|
||||
<li>Найди существующие компоненты модальных окон секции (создание/редактирование).</li>
|
||||
</ol>
|
||||
<p><strong>Окно создания секции</strong> — добавить:</p>
|
||||
<ul>
|
||||
<li>Числовой ввод: «Максимум одновременных задач»</li>
|
||||
<li>Подсказка: «0 = без ограничений»</li>
|
||||
<li>Валидация: число ≥ 0</li>
|
||||
<li>Значение по умолчанию: 0</li>
|
||||
<li>Отправляется в <code>GanttSectionCreateRequest</code> как <code>max_concurrent_tasks</code></li>
|
||||
</ul>
|
||||
<p><strong>Окно редактирования секции</strong> — добавить:</p>
|
||||
<ul>
|
||||
<li>Числовой ввод: «Максимум одновременных задач» (аналогично созданию)</li>
|
||||
<li>Секция «Блоки (запретные интервалы)»:
|
||||
<ul>
|
||||
<li>Таблица/список существующих блоков с колонками: Название, Начало, Конец, Блокировка (чекбокс/иконка), Действия (редактировать/удалить)</li>
|
||||
<li><strong>Сортировка:</strong> по <code>start_date</code> по убыванию (будущее сверху)</li>
|
||||
<li>Кнопка «Добавить блок» — inline-форма:
|
||||
<ul>
|
||||
<li>title (текстовый ввод, обязательный)</li>
|
||||
<li>start_date (datepicker, обязательный)</li>
|
||||
<li>end_date (datepicker, обязательный)</li>
|
||||
<li>block_concurrency (чекбокс: «Запретить пересечение задач»)</li>
|
||||
<li>Валидация: end_date >= start_date, все поля заполнены</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Редактирование — inline или через ту же форму</li>
|
||||
<li>Удаление — с подтверждением</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<ol start="2">
|
||||
<li>При открытии окна редактирования — загрузить текущие настройки секции + блоки</li>
|
||||
<li>При сохранении — обновить max_concurrent_tasks и сохранить изменения блоков</li>
|
||||
<li>Инжектировать GanttChartService и GanttSectionBlockService</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-3-визуализация-блоков-на-ганте">Этап FE-3: Визуализация блоков на Ганте</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Отобрази блоки секций (GanttSectionBlock) на canvas-диаграмме Ганта.</p>
|
||||
<p>Проект использует Konva.js для рендеринга. Основной компонент: <code>gantt-chart-canvas</code>.</p>
|
||||
<ol>
|
||||
<li>При загрузке данных диаграммы — извлечь блоки из каждой секции</li>
|
||||
<li>В рендерере секций (canvas/timeline-renderer.ts или аналогичный) — для каждого блока:
|
||||
<ul>
|
||||
<li>Нарисовать заштрихованную область (diagonal hatch pattern) на <strong>всю высоту секции</strong> (все строки задач в этой секции)</li>
|
||||
<li>Область ограничена по горизонтали: от start_date до end_date (в пикселях по шкале времени)</li>
|
||||
<li>Цвет штриховки: полупрозрачный серый или configurable</li>
|
||||
<li>Поверх штриховки — текст с названием блока (block.title) на подложке (прямоугольник с фоном, чтобы текст читался)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Блоки рендерятся <strong>до</strong> задач (z-index ниже), чтобы задачи были поверх</li>
|
||||
<li>Блоки с <code>block_concurrency = true</code> и <code>false</code> визуально не отличаются (штриховка одинаковая)</li>
|
||||
<li>При скролле/зуме — блоки пересчитываются как обычные элементы таймлайна</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-4-система-alert-ов">Этап FE-4: Система Alert-ов</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Создай компонент для отображения Alert-ов о конфликтах на диаграмме Ганта.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Создай компонент <code>pages/gantt/components/gantt-conflict-alerts/</code></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Компонент размещается <strong>над</strong> canvas-диаграммой Ганта</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Входные данные: массив <code>ConflictItem[]</code> (из модели)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Визуал:</p>
|
||||
<ul>
|
||||
<li>Bootstrap Alert (warning) для каждого конфликта</li>
|
||||
<li>Каждый Alert показывает message из ConflictItem</li>
|
||||
<li><strong>Без кнопки закрытия</strong> — Alert нельзя закрыть вручную</li>
|
||||
<li>Alert исчезает автоматически, когда текущая дата прошла дату конфликта</li>
|
||||
<li>Область <strong>сворачиваемая</strong>: заголовок «⚠️ Предупреждения (N)» — клик сворачивает/разворачивает список</li>
|
||||
<li>По умолчанию — развёрнуто</li>
|
||||
<li>При 0 конфликтов — область скрыта</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Состояние хранится в сервисе или в компоненте диаграммы:</p>
|
||||
<ul>
|
||||
<li>Добавление нового конфликта → area auto-expands</li>
|
||||
<li>Alert удаляется из массива <strong>автоматически</strong>, когда текущая дата прошла дату конфликта</li>
|
||||
<li>Проверка актуальности конфликтов — при каждой отрисовке / изменении данных</li>
|
||||
<li>Не использовать toast — Alert-ы постоянные</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<h4 id="этап-fe-5-проверка-конфликтов-при-операциях-с-задачами">Этап FE-5: Проверка конфликтов при операциях с задачами</h4>
|
||||
<p><strong>Промт для агента:</strong></p>
|
||||
<blockquote>
|
||||
<p>Интегрируй проверку конфликтов во все операции с задачами на диаграмме Ганта.</p>
|
||||
<p>Проект использует interaction-handler.ts для drag/resize и формы для создания/редактирования.</p>
|
||||
<p><strong>Важно:</strong> Проверка конфликтов — <strong>только на фронтенде</strong>, без вызова API. Используются загруженные данные секции (задачи, блоки, max_concurrent_tasks).</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Вынеси логику проверки конфликтов <code>checkConflicts()</code> в отдельный метод в gantt-canvas.service.ts:</p>
|
||||
<ul>
|
||||
<li>Принимает: section (GanttSection — содержит max_concurrent_tasks и blocks), tasks (GanttTask[] — задачи секции), task_id (nullable — исключить текущую задачу), start_date, end_date</li>
|
||||
<li>Логика:
|
||||
a. Если section.max_concurrent_tasks > 0: посчитать кол-во задач (исключая task_id), пересекающихся с [start_date, end_date] на каждый день. Если в любой день count > max_concurrent_tasks — добавить конфликт concurrency
|
||||
b. Для каждого блока секции с block_concurrency = true, пересекающегося с [start_date, end_date] — добавить конфликт block</li>
|
||||
<li>Возвращает ConflictCheck</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>После <strong>каждой</strong> успешной операции с задачей (создание, обновление, drag, resize):</p>
|
||||
<ul>
|
||||
<li>Вызвать checkConflicts с параметрами задачи</li>
|
||||
<li>Если has_conflicts = true — передать конфликты в GanttConflictAlertsComponent</li>
|
||||
<li>Alert-ы показываются, действие <strong>НЕ отменяется</strong></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>Точки интеграции:</p>
|
||||
<ul>
|
||||
<li><strong>Создание задачи</strong> (форма): после успешного POST → checkConflicts → show alerts</li>
|
||||
<li><strong>Редактирование задачи</strong> (форма): после успешного PUT → checkConflicts → show alerts</li>
|
||||
<li><strong>Drag задачи</strong> (interaction-handler.ts): после успешного обновления позиции → checkConflicts → show alerts</li>
|
||||
<li><strong>Resize задачи</strong> (interaction-handler.ts): после успешного обновления размера → checkConflicts → show alerts</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>При переключении диаграммы или секции — очищать Alert-ы</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Alert-ы автоматически удаляются, когда текущая дата прошла дату конфликта — область скрывается, когда конфликтов не осталось</p>
|
||||
</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<h2 id="порядок-реализации">Порядок реализации</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Этап</th>
|
||||
<th>Сторона</th>
|
||||
<th>Зависимости</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>Миграция + Домен</td>
|
||||
<td>Backend</td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>CRUD блоков</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>Обновление настроек секции</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>Включение блоков в ответ секций</td>
|
||||
<td>Backend</td>
|
||||
<td>Этап 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>Модели + Сервис</td>
|
||||
<td>Frontend</td>
|
||||
<td>— (можно параллельно с backend)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td>Интеграция в модальные окна</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 5 + backend 2,3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>7</td>
|
||||
<td>Визуализация блоков на Ганте</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 5 + backend 4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>8</td>
|
||||
<td>Система Alert-ов</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этап 5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>9</td>
|
||||
<td>Проверка конфликтов при операциях</td>
|
||||
<td>Frontend</td>
|
||||
<td>Этапы 5,7,8</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,942 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Leadera — WebSocket для диаграмм Ганта — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Leadera — WebSocket для диаграмм Ганта</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/leadera/leadera-websocket-gantt.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/leadera\/leadera-websocket-gantt.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tags mb-3">
|
||||
|
||||
<a href="../../tags/leadera/" class="tag">leadera</a>
|
||||
|
||||
<a href="../../tags/websocket/" class="tag">websocket</a>
|
||||
|
||||
<a href="../../tags/realtime/" class="tag">realtime</a>
|
||||
|
||||
<a href="../../tags/gantt/" class="tag">gantt</a>
|
||||
|
||||
<a href="../../tags/architecture/" class="tag">architecture</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h1 id="websocket-для-диаграмм-ганта--realtime-совместное-редактирование">WebSocket для диаграмм Ганта — realtime-совместное редактирование</h1>
|
||||
<p><strong>Дата:</strong> 07.04.2026
|
||||
<strong>Статус:</strong> Планирование
|
||||
<strong>Проект:</strong> Leadera (dev — sand.a2v.space)</p>
|
||||
<hr>
|
||||
<h2 id="1-постановка-задачи">1. Постановка задачи</h2>
|
||||
<p>Диаграммы Ганта должны обновляться в реальном времени при совместной работе: если два или более пользователей редактируют один Гант, изменения каждого мгновенно отображаются у остальных — без перезагрузки страницы и ручного обновления.</p>
|
||||
<hr>
|
||||
<h2 id="2-архитектура">2. Архитектура</h2>
|
||||
<pre tabindex="0"><code>Angular frontend ←── WebSocket ──→ Go backend (Hub) ──→ PostgreSQL
|
||||
│ │
|
||||
│ HTTP (мутации) │ Broadcast
|
||||
└──→ REST API ──→ Service ──→ DB ──→ Hub ──→ все клиенты комнаты
|
||||
</code></pre><p><strong>Ключевой принцип:</strong> мутации данных идут через HTTP (REST), WebSocket используется <strong>только для доставки уведомлений</strong>. Сервер — источник истины, клиент не может обойти проверки через WS.</p>
|
||||
<h3 id="комнаты">Комнаты</h3>
|
||||
<p>Каждый открытый Гант — это «комната» с идентификатором <code>chartId</code>. При подключении клиент подписывается на комнату. Hub рассылает события всем подписчикам комнаты.</p>
|
||||
<hr>
|
||||
<h2 id="3-backend-go">3. Backend (Go)</h2>
|
||||
<h3 id="31-зависимости">3.1. Зависимости</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>go get github.com/gorilla/websocket
|
||||
</span></span></code></pre></div><h3 id="32-новые-файлы--internalws">3.2. Новые файлы — <code>internal/ws/</code></h3>
|
||||
<h4 id="messagego--типы-сообщений"><code>message.go</code> — типы сообщений</h4>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">ws</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> <span style="color:#e6db74">"time"</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// EventType — тип события</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">EventType</span> <span style="color:#66d9ef">string</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> (
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Данные</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventSectionCreated</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"section_created"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventSectionUpdated</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"section_updated"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventSectionDeleted</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"section_deleted"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventSectionsReordered</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"sections_reordered"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventTaskCreated</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"task_created"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventTaskUpdated</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"task_updated"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventTaskDeleted</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"task_deleted"</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Присутствие</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventUserJoined</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"user_joined"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventUserLeft</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"user_left"</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Безопасность / доступ</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventAccessRevoked</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"access_revoked"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventChartArchived</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"chart_archived"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventRoleChanged</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"role_changed"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">EventTokenExpired</span> <span style="color:#a6e22e">EventType</span> = <span style="color:#e6db74">"token_expired"</span>
|
||||
</span></span><span style="display:flex;"><span>)
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Event — WS-событие, отправляемое клиентам</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Event</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span> <span style="color:#a6e22e">EventType</span> <span style="color:#e6db74">`json:"type"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"chartId"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"userId,omitempty"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Payload</span> <span style="color:#66d9ef">interface</span>{} <span style="color:#e6db74">`json:"payload,omitempty"`</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Timestamp</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:"timestamp"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// IncomingMessage — сообщение от клиента (для ping/pong)</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">IncomingMessage</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:"type"`</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h4 id="hubgo--центральный-брокер"><code>hub.go</code> — центральный брокер</h4>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">ws</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"sync"</span>
|
||||
</span></span><span style="display:flex;"><span>)
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Hub управляет комнатами и рассылкой событий</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Hub</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">mu</span> <span style="color:#a6e22e">sync</span>.<span style="color:#a6e22e">RWMutex</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">rooms</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">map</span>[<span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>]<span style="color:#66d9ef">bool</span> <span style="color:#75715e">// chartId → клиенты</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewHub</span>() <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#f92672">&</span><span style="color:#a6e22e">Hub</span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">rooms</span>: make(<span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">map</span>[<span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>]<span style="color:#66d9ef">bool</span>),
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Subscribe добавляет клиента в комнату</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>) <span style="color:#a6e22e">Subscribe</span>(<span style="color:#a6e22e">chartID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">client</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Lock</span>()
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Unlock</span>()
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>[<span style="color:#a6e22e">chartID</span>] <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>[<span style="color:#a6e22e">chartID</span>] = make(<span style="color:#66d9ef">map</span>[<span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>]<span style="color:#66d9ef">bool</span>)
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>[<span style="color:#a6e22e">chartID</span>][<span style="color:#a6e22e">client</span>] = <span style="color:#66d9ef">true</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Уведомить остальных: user_joined</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">broadcastExcept</span>(<span style="color:#a6e22e">chartID</span>, <span style="color:#a6e22e">client</span>, <span style="color:#a6e22e">Event</span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span>: <span style="color:#a6e22e">EventUserJoined</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span>: <span style="color:#a6e22e">chartID</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span>: <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">UserID</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Payload</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">"username"</span>: <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Username</span>},
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Timestamp</span>: <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>(),
|
||||
</span></span><span style="display:flex;"><span> })
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Unsubscribe удаляет клиента из комнаты</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>) <span style="color:#a6e22e">Unsubscribe</span>(<span style="color:#a6e22e">chartID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">client</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Lock</span>()
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Unlock</span>()
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">clients</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>[<span style="color:#a6e22e">chartID</span>]; <span style="color:#a6e22e">ok</span> {
|
||||
</span></span><span style="display:flex;"><span> delete(<span style="color:#a6e22e">clients</span>, <span style="color:#a6e22e">client</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">clients</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
|
||||
</span></span><span style="display:flex;"><span> delete(<span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>, <span style="color:#a6e22e">chartID</span>)
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Уведомить остальных: user_left</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">broadcastExcept</span>(<span style="color:#a6e22e">chartID</span>, <span style="color:#a6e22e">client</span>, <span style="color:#a6e22e">Event</span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span>: <span style="color:#a6e22e">EventUserLeft</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span>: <span style="color:#a6e22e">chartID</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span>: <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">UserID</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Timestamp</span>: <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>(),
|
||||
</span></span><span style="display:flex;"><span> })
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Broadcast рассылает событие всем клиентам в комнате</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>) <span style="color:#a6e22e">Broadcast</span>(<span style="color:#a6e22e">chartID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">event</span> <span style="color:#a6e22e">Event</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">RLock</span>()
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">RUnlock</span>()
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">clients</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>[<span style="color:#a6e22e">chartID</span>]; <span style="color:#a6e22e">ok</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">clients</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Send</span>(<span style="color:#a6e22e">event</span>)
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// SendToUser отправляет событие конкретному пользователю во всех его комнатах</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>) <span style="color:#a6e22e">SendToUser</span>(<span style="color:#a6e22e">userID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">event</span> <span style="color:#a6e22e">Event</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">RLock</span>()
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">RUnlock</span>()
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">clients</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">clients</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">UserID</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">userID</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Send</span>(<span style="color:#a6e22e">event</span>)
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// broadcastExcept рассылает всем, кроме отправителя</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>) <span style="color:#a6e22e">broadcastExcept</span>(<span style="color:#a6e22e">chartID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">sender</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>, <span style="color:#a6e22e">event</span> <span style="color:#a6e22e">Event</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">clients</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">rooms</span>[<span style="color:#a6e22e">chartID</span>]; <span style="color:#a6e22e">ok</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">clients</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">!=</span> <span style="color:#a6e22e">sender</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Send</span>(<span style="color:#a6e22e">event</span>)
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h4 id="clientgo--обёртка-ws-соединения"><code>client.go</code> — обёртка WS-соединения</h4>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">ws</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"encoding/json"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"sync"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"time"</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/gorilla/websocket"</span>
|
||||
</span></span><span style="display:flex;"><span>)
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> (
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">writeWait</span> = <span style="color:#ae81ff">10</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">pongWait</span> = <span style="color:#ae81ff">60</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">pingPeriod</span> = (<span style="color:#a6e22e">pongWait</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">9</span>) <span style="color:#f92672">/</span> <span style="color:#ae81ff">10</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">maxMessageSize</span> = <span style="color:#ae81ff">1024</span>
|
||||
</span></span><span style="display:flex;"><span>)
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Client</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Hub</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Conn</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">Conn</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span> <span style="color:#66d9ef">string</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Username</span> <span style="color:#66d9ef">string</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span> <span style="color:#66d9ef">string</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">SendCh</span> <span style="color:#66d9ef">chan</span> <span style="color:#a6e22e">Event</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">mu</span> <span style="color:#a6e22e">sync</span>.<span style="color:#a6e22e">Mutex</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewClient</span>(<span style="color:#a6e22e">hub</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Hub</span>, <span style="color:#a6e22e">conn</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">Conn</span>, <span style="color:#a6e22e">userID</span>, <span style="color:#a6e22e">username</span>, <span style="color:#a6e22e">chartID</span> <span style="color:#66d9ef">string</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#f92672">&</span><span style="color:#a6e22e">Client</span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Hub</span>: <span style="color:#a6e22e">hub</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Conn</span>: <span style="color:#a6e22e">conn</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span>: <span style="color:#a6e22e">userID</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Username</span>: <span style="color:#a6e22e">username</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span>: <span style="color:#a6e22e">chartID</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">SendCh</span>: make(<span style="color:#66d9ef">chan</span> <span style="color:#a6e22e">Event</span>, <span style="color:#ae81ff">64</span>),
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// ReadPump читает входящие сообщения (ping/pong)</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) <span style="color:#a6e22e">ReadPump</span>() {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Hub</span>.<span style="color:#a6e22e">Unsubscribe</span>(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">ChartID</span>, <span style="color:#a6e22e">c</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">Close</span>()
|
||||
</span></span><span style="display:flex;"><span> }()
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">SetReadLimit</span>(<span style="color:#a6e22e">maxMessageSize</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">SetReadDeadline</span>(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>().<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">pongWait</span>))
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">SetPongHandler</span>(<span style="color:#66d9ef">func</span>(<span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">error</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">SetReadDeadline</span>(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>().<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">pongWait</span>))
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
|
||||
</span></span><span style="display:flex;"><span> })
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">message</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">ReadMessage</span>()
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Клиент может слать только ping, данные игнорируем</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">_</span> = <span style="color:#a6e22e">message</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// WritePump отправляет события клиенту</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) <span style="color:#a6e22e">WritePump</span>() {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ticker</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">NewTicker</span>(<span style="color:#a6e22e">pingPeriod</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ticker</span>.<span style="color:#a6e22e">Stop</span>()
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">Close</span>()
|
||||
</span></span><span style="display:flex;"><span> }()
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">select</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">event</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#f92672"><-</span><span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">SendCh</span>:
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">SetWriteDeadline</span>(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>().<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">writeWait</span>))
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">WriteMessage</span>(<span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">CloseMessage</span>, []<span style="color:#66d9ef">byte</span>{})
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">data</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">Marshal</span>(<span style="color:#a6e22e">event</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Lock</span>()
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">WriteMessage</span>(<span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">TextMessage</span>, <span style="color:#a6e22e">data</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">mu</span>.<span style="color:#a6e22e">Unlock</span>()
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#f92672"><-</span><span style="color:#a6e22e">ticker</span>.<span style="color:#a6e22e">C</span>:
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">SetWriteDeadline</span>(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>().<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">writeWait</span>))
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Conn</span>.<span style="color:#a6e22e">WriteMessage</span>(<span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">PingMessage</span>, <span style="color:#66d9ef">nil</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Send ставит событие в очередь отправки</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) <span style="color:#a6e22e">Send</span>(<span style="color:#a6e22e">event</span> <span style="color:#a6e22e">Event</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">select</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">SendCh</span> <span style="color:#f92672"><-</span> <span style="color:#a6e22e">event</span>:
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">default</span>:
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Очередь полна — отключаем медленного клиента</span>
|
||||
</span></span><span style="display:flex;"><span> close(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">SendCh</span>)
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h3 id="33-ws-endpoint--internalhandlersws_handlergo">3.3. WS endpoint — <code>internal/handlers/ws_handler.go</code></h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">handlers</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"net/http"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"time"</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"app-leadera-api/internal/middleware"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"app-leadera-api/internal/ws"</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/gin-gonic/gin"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/google/uuid"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/gorilla/websocket"</span>
|
||||
</span></span><span style="display:flex;"><span>)
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">upgrader</span> = <span style="color:#a6e22e">websocket</span>.<span style="color:#a6e22e">Upgrader</span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ReadBufferSize</span>: <span style="color:#ae81ff">1024</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">WriteBufferSize</span>: <span style="color:#ae81ff">1024</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CheckOrigin</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) <span style="color:#66d9ef">bool</span> { <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span> }, <span style="color:#75715e">// nginx проксирует</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">WSHandler</span> <span style="color:#66d9ef">struct</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">hub</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">Hub</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ganttMW</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">middleware</span>.<span style="color:#a6e22e">GanttMiddleware</span>
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewWSHandler</span>(<span style="color:#a6e22e">hub</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">Hub</span>, <span style="color:#a6e22e">ganttMW</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">middleware</span>.<span style="color:#a6e22e">GanttMiddleware</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">WSHandler</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#f92672">&</span><span style="color:#a6e22e">WSHandler</span>{<span style="color:#a6e22e">hub</span>: <span style="color:#a6e22e">hub</span>, <span style="color:#a6e22e">ganttMW</span>: <span style="color:#a6e22e">ganttMW</span>}
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">WSHandler</span>) <span style="color:#a6e22e">HandleGanttWS</span>(<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">gin</span>.<span style="color:#a6e22e">Context</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// 1. Получить пользователя из контекста (auth middleware уже отработал)</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">userID</span>, <span style="color:#a6e22e">exists</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">"userID"</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">exists</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AbortWithStatusJSON</span>(<span style="color:#ae81ff">401</span>, <span style="color:#a6e22e">gin</span>.<span style="color:#a6e22e">H</span>{<span style="color:#e6db74">"error"</span>: <span style="color:#e6db74">"unauthorized"</span>})
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// 2. Проверить доступ к chartId (gantt middleware)</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">chartID</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Param</span>(<span style="color:#e6db74">"chartId"</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">uuid</span>.<span style="color:#a6e22e">Parse</span>(<span style="color:#a6e22e">chartID</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">AbortWithStatusJSON</span>(<span style="color:#ae81ff">400</span>, <span style="color:#a6e22e">gin</span>.<span style="color:#a6e22e">H</span>{<span style="color:#e6db74">"error"</span>: <span style="color:#e6db74">"invalid chart id"</span>})
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// TODO: проверить через ganttMW что userID имеет доступ к этому chart</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// 3. Upgrade до WebSocket</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">conn</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">upgrader</span>.<span style="color:#a6e22e">Upgrade</span>(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Writer</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Request</span>, <span style="color:#66d9ef">nil</span>)
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span>
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// 4. Создать клиент и подписать на комнату</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">NewClient</span>(
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">hub</span>, <span style="color:#a6e22e">conn</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">userID</span>.(<span style="color:#66d9ef">string</span>),
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">GetString</span>(<span style="color:#e6db74">"username"</span>),
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">chartID</span>,
|
||||
</span></span><span style="display:flex;"><span> )
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">hub</span>.<span style="color:#a6e22e">Subscribe</span>(<span style="color:#a6e22e">chartID</span>, <span style="color:#a6e22e">client</span>)
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// 5. Запустить pump'ы</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">go</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">WritePump</span>()
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">go</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">ReadPump</span>()
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h3 id="34-регистрация-маршрута">3.4. Регистрация маршрута</h3>
|
||||
<p>В <code>cmd/server/main.go</code> или где регистрируются routes:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// После создания hub</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wsHub</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">NewHub</span>()
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// WS endpoint</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wsHandler</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">handlers</span>.<span style="color:#a6e22e">NewWSHandler</span>(<span style="color:#a6e22e">wsHub</span>, <span style="color:#a6e22e">ganttMW</span>)
|
||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">api</span>.<span style="color:#a6e22e">GET</span>(<span style="color:#e6db74">"/ws/gantt/:chartId"</span>, <span style="color:#a6e22e">authMW</span>.<span style="color:#a6e22e">RequireAuth</span>(), <span style="color:#a6e22e">wsHandler</span>.<span style="color:#a6e22e">HandleGanttWS</span>)
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Передать hub в ganttHandler для broadcast'ов</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">ganttHandler</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">handlers</span>.<span style="color:#a6e22e">NewGanttHandler</span>(<span style="color:#a6e22e">ganttService</span>, <span style="color:#a6e22e">sectionService</span>, <span style="color:#a6e22e">taskService</span>, <span style="color:#a6e22e">blockService</span>, <span style="color:#a6e22e">wsHub</span>)
|
||||
</span></span></code></pre></div><h3 id="35-интеграция-broadcast-в-handlers">3.5. Интеграция broadcast в handlers</h3>
|
||||
<p>В каждый write-handler — одна строка после успешной операции:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Пример: после успешного UpdateTask</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">hub</span>.<span style="color:#a6e22e">Broadcast</span>(<span style="color:#a6e22e">chartID</span>, <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">Event</span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span>: <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">EventTaskUpdated</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span>: <span style="color:#a6e22e">chartID</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span>: <span style="color:#a6e22e">userID</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Payload</span>: <span style="color:#a6e22e">updatedTask</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Timestamp</span>: <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>(),
|
||||
</span></span><span style="display:flex;"><span>})
|
||||
</span></span></code></pre></div><p>Точки интеграции в <code>gantt_handler.go</code>:</p>
|
||||
<ul>
|
||||
<li><code>CreateGanttChart</code> → не нужен (пользователь только что создал, он один)</li>
|
||||
<li><code>UpdateGanttChart</code> / <code>ArchiveGanttChart</code> → <code>chart_archived</code></li>
|
||||
<li><code>CreateSection</code> → <code>section_created</code></li>
|
||||
<li><code>UpdateSection</code> → <code>section_updated</code></li>
|
||||
<li><code>DeleteSection</code> → <code>section_deleted</code></li>
|
||||
<li><code>ReorderSections</code> → <code>sections_reordered</code></li>
|
||||
<li><code>CreateTask</code> → <code>task_created</code></li>
|
||||
<li><code>UpdateTask</code> → <code>task_updated</code></li>
|
||||
<li><code>DeleteTask</code> → <code>task_deleted</code></li>
|
||||
</ul>
|
||||
<h3 id="36-интеграция-событий-безопасности">3.6. Интеграция событий безопасности</h3>
|
||||
<p>В соответствующих service-методах:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// При исключении из space (SpaceService.RemoveMember)</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wsHub</span>.<span style="color:#a6e22e">SendToUser</span>(<span style="color:#a6e22e">removedUserID</span>, <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">Event</span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span>: <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">EventAccessRevoked</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Payload</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">"spaceId"</span>: <span style="color:#a6e22e">spaceID</span>},
|
||||
</span></span><span style="display:flex;"><span>})
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// При архивации ганта</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wsHub</span>.<span style="color:#a6e22e">Broadcast</span>(<span style="color:#a6e22e">chartID</span>, <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">Event</span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span>: <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">EventChartArchived</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ChartID</span>: <span style="color:#a6e22e">chartID</span>,
|
||||
</span></span><span style="display:flex;"><span>})
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#75715e">// При изменении роли</span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wsHub</span>.<span style="color:#a6e22e">SendToUser</span>(<span style="color:#a6e22e">userID</span>, <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">Event</span>{
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Type</span>: <span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">EventRoleChanged</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Payload</span>: <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">"role"</span>: <span style="color:#a6e22e">newRole</span>},
|
||||
</span></span><span style="display:flex;"><span>})
|
||||
</span></span></code></pre></div><hr>
|
||||
<h2 id="4-frontend-angular">4. Frontend (Angular)</h2>
|
||||
<h3 id="41-websocket-сервис--gantt-wsservicets">4.1. WebSocket сервис — <code>gantt-ws.service.ts</code></h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// src/app/pages/gantt/services/gantt-ws.service.ts
|
||||
</span></span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Injectable</span>, <span style="color:#a6e22e">inject</span>, <span style="color:#a6e22e">OnDestroy</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">'@angular/core'</span>;
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Subject</span>, <span style="color:#a6e22e">Observable</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">'rxjs'</span>;
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">environment</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">'../../../environments/environment'</span>;
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">SpaceStorageService</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">'../../../core/services/space-storage.service'</span>;
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">AuthService</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">'../../../core/auth/services/auth.service'</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">WSEvent</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">type</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">chartId?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">userId?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">payload?</span>: <span style="color:#66d9ef">any</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">timestamp?</span>: <span style="color:#66d9ef">string</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">@Injectable</span>({ <span style="color:#a6e22e">providedIn</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'root'</span> })
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">GanttWSService</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">OnDestroy</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">spaceStorage</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">inject</span>(<span style="color:#a6e22e">SpaceStorageService</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">auth</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">inject</span>(<span style="color:#a6e22e">AuthService</span>);
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">socket</span>: <span style="color:#66d9ef">WebSocket</span> <span style="color:#f92672">|</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">eventSubject</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Subject</span><<span style="color:#f92672">WSEvent</span>>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">reconnectTimer</span>: <span style="color:#66d9ef">any</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">chartId</span>: <span style="color:#66d9ef">string</span> <span style="color:#f92672">|</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">/** Observable для подписки на события */</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">get</span> <span style="color:#a6e22e">events$</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">Observable</span><<span style="color:#f92672">WSEvent</span>> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">eventSubject</span>.<span style="color:#a6e22e">asObservable</span>();
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">/** Подключиться к комнате ганта */</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">connect</span>(<span style="color:#a6e22e">chartId</span>: <span style="color:#66d9ef">string</span>)<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">disconnect</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">chartId</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">chartId</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">token</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">auth</span>.<span style="color:#a6e22e">getAccessToken</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">wsUrl</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">buildWSUrl</span>(<span style="color:#a6e22e">chartId</span>, <span style="color:#a6e22e">token</span>);
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">WebSocket</span>(<span style="color:#a6e22e">wsUrl</span>);
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span>.<span style="color:#a6e22e">onopen</span> <span style="color:#f92672">=</span> () <span style="color:#f92672">=></span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">`[WS] Connected to gantt </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">chartId</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>);
|
||||
</span></span><span style="display:flex;"><span> };
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span>.<span style="color:#a6e22e">onmessage</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">msg</span>) <span style="color:#f92672">=></span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">try</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">event</span>: <span style="color:#66d9ef">WSEvent</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">parse</span>(<span style="color:#a6e22e">msg</span>.<span style="color:#a6e22e">data</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">eventSubject</span>.<span style="color:#a6e22e">next</span>(<span style="color:#a6e22e">event</span>);
|
||||
</span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">e</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">error</span>(<span style="color:#e6db74">'[WS] Parse error'</span>, <span style="color:#a6e22e">e</span>);
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> };
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span>.<span style="color:#a6e22e">onclose</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">event</span>) <span style="color:#f92672">=></span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">`[WS] Closed: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">code</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">code</span> <span style="color:#f92672">!==</span> <span style="color:#ae81ff">1000</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">scheduleReconnect</span>();
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> };
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span>.<span style="color:#a6e22e">onerror</span> <span style="color:#f92672">=</span> () <span style="color:#f92672">=></span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">close</span>();
|
||||
</span></span><span style="display:flex;"><span> };
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">/** Отключиться */</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">disconnect</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">clearTimeout</span>(<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">reconnectTimer</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span>.<span style="color:#a6e22e">close</span>(<span style="color:#ae81ff">1000</span>, <span style="color:#e6db74">'manual'</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">socket</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">chartId</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ngOnDestroy</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">disconnect</span>();
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">buildWSUrl</span>(<span style="color:#a6e22e">chartId</span>: <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">token</span>: <span style="color:#66d9ef">string</span>)<span style="color:#f92672">:</span> <span style="color:#66d9ef">string</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">base</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">environment</span>.<span style="color:#a6e22e">wsUrl</span> <span style="color:#f92672">||</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">location</span>.<span style="color:#a6e22e">protocol</span>.<span style="color:#a6e22e">replace</span>(<span style="color:#e6db74">'http'</span>, <span style="color:#e6db74">'ws'</span>) <span style="color:#f92672">+</span> <span style="color:#e6db74">'//'</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">location</span>.<span style="color:#a6e22e">host</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#e6db74">`</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">base</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/api/v1/ws/gantt/</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">chartId</span><span style="color:#e6db74">}</span><span style="color:#e6db74">?token=</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">token</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">scheduleReconnect</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">chartId</span>) <span style="color:#66d9ef">return</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">reconnectTimer</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">setTimeout</span>(() <span style="color:#f92672">=></span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">'[WS] Reconnecting...'</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">chartId</span>) <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">connect</span>(<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">chartId</span>);
|
||||
</span></span><span style="display:flex;"><span> }, <span style="color:#ae81ff">3000</span>);
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h3 id="42-интеграция-в-gantt-chart-canvas-компонент">4.2. Интеграция в gantt-chart-canvas компонент</h3>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#75715e">// В gantt-chart-canvas.component.ts
|
||||
</span></span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">GanttChartCanvasComponent</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">OnInit</span>, <span style="color:#a6e22e">OnDestroy</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">ws</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">inject</span>(<span style="color:#a6e22e">GanttWSService</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">onlineUsers</span>: <span style="color:#66d9ef">Set</span><<span style="color:#f92672">string</span>> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Set</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">wsSubscription</span>: <span style="color:#66d9ef">Subscription</span> <span style="color:#f92672">|</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">null</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ngOnInit</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Подключиться к WS при загрузке ганта
|
||||
</span></span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">connect</span>(<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">chartId</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">wsSubscription</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">events$</span>.<span style="color:#a6e22e">subscribe</span>(<span style="color:#a6e22e">event</span> <span style="color:#f92672">=></span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">handleWSEvent</span>(<span style="color:#a6e22e">event</span>);
|
||||
</span></span><span style="display:flex;"><span> });
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ngOnDestroy</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">wsSubscription</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">unsubscribe</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">ws</span>.<span style="color:#a6e22e">disconnect</span>();
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">handleWSEvent</span>(<span style="color:#a6e22e">event</span>: <span style="color:#66d9ef">WSEvent</span>)<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Если это событие от текущего пользователя — пропускаем
|
||||
</span></span></span><span style="display:flex;"><span> <span style="color:#75715e">// (мы уже обновили UI оптимистично или после HTTP-ответа)
|
||||
</span></span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">userId</span> <span style="color:#f92672">===</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">currentUserId</span>) <span style="color:#66d9ef">return</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">switch</span> (<span style="color:#a6e22e">event</span>.<span style="color:#66d9ef">type</span>) {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'section_created'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">addSectionLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'section_updated'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">updateSectionLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'section_deleted'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">removeSectionLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>.<span style="color:#a6e22e">id</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'sections_reordered'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">reorderSectionsLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>.<span style="color:#a6e22e">ordered_ids</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'task_created'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">addTaskLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'task_updated'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">updateTaskLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'task_deleted'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">removeTaskLocal</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>.<span style="color:#a6e22e">id</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">redrawCanvas</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'user_joined'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">onlineUsers</span>.<span style="color:#a6e22e">add</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">userId</span><span style="color:#f92672">!</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">showOnlineIndicator</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'user_left'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">onlineUsers</span>.<span style="color:#66d9ef">delete</span>(<span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">userId</span><span style="color:#f92672">!</span>);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">showOnlineIndicator</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// === Безопасность ===
|
||||
</span></span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'access_revoked'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">router</span>.<span style="color:#a6e22e">navigate</span>([<span style="color:#e6db74">'/gantt'</span>], {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">state</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">alert</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'Доступ к пространству был отозван'</span> }
|
||||
</span></span><span style="display:flex;"><span> });
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'chart_archived'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">router</span>.<span style="color:#a6e22e">navigate</span>([<span style="color:#e6db74">'/gantt'</span>], {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">state</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">alert</span><span style="color:#f92672">:</span> <span style="color:#e6db74">'Диаграмма была заархивирована'</span> }
|
||||
</span></span><span style="display:flex;"><span> });
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'role_changed'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">currentRole</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">payload</span>.<span style="color:#a6e22e">role</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">updateEditableState</span>();
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#e6db74">'token_expired'</span><span style="color:#f92672">:</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">router</span>.<span style="color:#a6e22e">navigate</span>([<span style="color:#e6db74">'/auth/login'</span>]);
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">redrawCanvas</span>()<span style="color:#f92672">:</span> <span style="color:#66d9ef">void</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Перерисовать Konva canvas с обновлёнными данными
|
||||
</span></span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><h3 id="43-индикатор-онлайн-пользователей">4.3. Индикатор онлайн-пользователей</h3>
|
||||
<p>Добавить в шаблон canvas-компонента:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-html" data-lang="html"><span style="display:flex;"><span><span style="color:#75715e"><!-- Онлайн-индикатор --></span>
|
||||
</span></span><span style="display:flex;"><span><<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">"online-indicator"</span> <span style="color:#960050;background-color:#1e0010">*</span><span style="color:#a6e22e">ngIf</span><span style="color:#f92672">=</span><span style="color:#e6db74">"onlineUsers.size > 0"</span>>
|
||||
</span></span><span style="display:flex;"><span> <<span style="color:#f92672">span</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">"pulse-dot"</span>></<span style="color:#f92672">span</span>>
|
||||
</span></span><span style="display:flex;"><span> {{ onlineUsers.size }} онлайн
|
||||
</span></span><span style="display:flex;"><span></<span style="color:#f92672">div</span>>
|
||||
</span></span></code></pre></div><hr>
|
||||
<h2 id="5-безопасность">5. Безопасность</h2>
|
||||
<h3 id="51-авторизация-при-подключении">5.1. Авторизация при подключении</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Этап</th>
|
||||
<th>Проверка</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>WS Handshake</td>
|
||||
<td>JWT в query-параметре <code>?token=...</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Подписка на комнату</td>
|
||||
<td>Проверка membership в space</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Каждое broadcast-событие</td>
|
||||
<td>Hub знает userId каждого клиента</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="52-сценарии-отзыва-доступа">5.2. Сценарии отзыва доступа</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Событие</th>
|
||||
<th>Триггер</th>
|
||||
<th>Действие на клиенте</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>access_revoked</code></td>
|
||||
<td>Исключение из space</td>
|
||||
<td>Редирект на список гантов</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>chart_archived</code></td>
|
||||
<td>Архивация ганта</td>
|
||||
<td>Редирект на список гантов</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>role_changed</code></td>
|
||||
<td>Смена роли (editor→viewer)</td>
|
||||
<td>Отключить редактирование</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>token_expired</code></td>
|
||||
<td>Токен протух</td>
|
||||
<td>Редирект на логин</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="53-защита-от-злоупотреблений">5.3. Защита от злоупотреблений</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Угроза</th>
|
||||
<th>Мера</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Спам WS-сообщений</td>
|
||||
<td>Rate limit: 30 msg/min на клиента, <code>ReadLimit</code> в gorilla/websocket</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Подключение без прав</td>
|
||||
<td>Проверка space membership при handshake</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Истёкший токен</td>
|
||||
<td>Ping/pong + периодическая проверка JWT (раз в 5 мин)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>XSS через WS</td>
|
||||
<td>Payload — JSON, sanitize имён на frontend</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Мутации через WS</td>
|
||||
<td>WS <strong>только для чтения</strong> (уведомления). Мутации — через HTTP API с полной валидацией</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="54-nginx-конфигурация">5.4. Nginx конфигурация</h3>
|
||||
<p>В конфиг <code>sand.a2v.space</code> добавить:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#75715e"># WebSocket проксирование
|
||||
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">location</span> <span style="color:#e6db74">/api/v1/ws/</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8080</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_http_version</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">.1</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Upgrade</span> $http_upgrade;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Connection</span> <span style="color:#e6db74">"upgrade"</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span> $remote_addr;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $proxy_add_x_forwarded_for;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_read_timeout</span> <span style="color:#e6db74">86400s</span>; <span style="color:#75715e"># 24h для long-lived WS
|
||||
</span></span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_send_timeout</span> <span style="color:#e6db74">86400s</span>;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><hr>
|
||||
<h2 id="6-оценка-трудозатрат">6. Оценка трудозатрат</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Компонент</th>
|
||||
<th>Строки кода</th>
|
||||
<th>Время</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>internal/ws/</code> (hub, client, message)</td>
|
||||
<td>~200</td>
|
||||
<td>2-3 часа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ws_handler.go</code> (endpoint)</td>
|
||||
<td>~60</td>
|
||||
<td>30 мин</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Интеграция broadcast в handlers</td>
|
||||
<td>~20 (по строке на endpoint)</td>
|
||||
<td>30 мин</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>События безопасности в services</td>
|
||||
<td>~30</td>
|
||||
<td>30 мин</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>gantt-ws.service.ts</code> (Angular)</td>
|
||||
<td>~80</td>
|
||||
<td>1 час</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Интеграция в canvas-компонент</td>
|
||||
<td>~80</td>
|
||||
<td>1-2 часа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Индикатор онлайн</td>
|
||||
<td>~30</td>
|
||||
<td>30 мин</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Nginx конфигурация</td>
|
||||
<td>~10</td>
|
||||
<td>10 мин</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Тестирование</td>
|
||||
<td>—</td>
|
||||
<td>2-3 часа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Итого</strong></td>
|
||||
<td><strong>~510 строк</strong></td>
|
||||
<td><strong>~1-2 дня</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="7-порядок-реализации">7. Порядок реализации</h2>
|
||||
<ol>
|
||||
<li><strong>Backend: <code>internal/ws/</code></strong> — hub, client, message types</li>
|
||||
<li><strong>Backend: WS endpoint</strong> — handler + route</li>
|
||||
<li><strong>Backend: Broadcast в handlers</strong> — интеграция в существующие CRUD-эндпоинты</li>
|
||||
<li><strong>Backend: События безопасности</strong> — access_revoked, chart_archived, role_changed</li>
|
||||
<li><strong>Nginx: WS location</strong> — проксирование WebSocket</li>
|
||||
<li><strong>Frontend: WS service</strong> — подключение, реконнект, парсинг событий</li>
|
||||
<li><strong>Frontend: Интеграция в canvas</strong> — обработка событий, обновление Konva</li>
|
||||
<li><strong>Frontend: Индикатор онлайн</strong> — визуальная обратная связь</li>
|
||||
<li><strong>Тестирование</strong> — многопользовательский сценарий, отзыв прав, обрывы связи</li>
|
||||
</ol>
|
||||
<hr>
|
||||
<h2 id="8-дальнейшие-улучшения-после-mvp">8. Дальнейшие улучшения (после MVP)</h2>
|
||||
<ul>
|
||||
<li><strong>Cursor других пользователей</strong> — показывать позицию курсора на таймлайне</li>
|
||||
<li><strong>Блокировка элемента</strong> — «кто-то редактирует эту задачу» (optimistic locking через WS)</li>
|
||||
<li><strong>Чат в ганте</strong> — комментарии к задачам прямо на канвасе</li>
|
||||
<li><strong>History/undo</strong> — откат изменений с визуальной историей</li>
|
||||
<li><strong>Уведомления</strong> — push-уведомления при @mention в комментариях к задачам</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -111,6 +117,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Бинарный файл в репозитории</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/personal-site/binary-files-in-repo.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/personal-site\/binary-files-in-repo.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h2 id="проблема">Проблема</h2>
|
||||
<p>В корне репозитория лежат два файла, которые не должны быть в git:</p>
|
||||
<ul>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -111,6 +117,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Copyright без 2026 года</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/personal-site/copyright-outdated.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/personal-site\/copyright-outdated.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h2 id="проблема">Проблема</h2>
|
||||
<p>В <code>params.toml</code>:</p>
|
||||
<pre tabindex="0"><code>copyRight = "Copyright (c) 2020-2025 Антон Агальцов"
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -111,6 +117,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Digest не обновлялся с февраля 2025</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/personal-site/digest-stale.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/personal-site\/digest-stale.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h2 id="проблема">Проблема</h2>
|
||||
<p>Последний дайджест — <strong>февраль 2025</strong>. С тех пор 12+ месяцев без новых выпусков.</p>
|
||||
<h3 id="что-есть">Что есть</h3>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../creds/">Креды</a></li>
|
||||
@@ -115,21 +121,21 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../personal-site/copyright-outdated/">Copyright без 2026 года</a></li>
|
||||
|
||||
<li><a href="../personal-site/digest-stale/">Digest не обновлялся с февраля 2025</a></li>
|
||||
|
||||
<li><a href="../personal-site/outdated-doks-theme/">Hugo Doks 0.5 — сильно устарел</a></li>
|
||||
|
||||
<li><a href="../personal-site/twitter-handle-mismatch/">Twitter handle не соответствует имени</a></li>
|
||||
|
||||
<li><a href="../personal-site/no-analytics/">Yandex Metrika отключена</a></li>
|
||||
|
||||
<li><a href="../personal-site/binary-files-in-repo/">Бинарный файл в репозитории</a></li>
|
||||
<li><a href="../personal-site/netlify-node-version/">Устаревший Node.js 16 в Netlify</a></li>
|
||||
|
||||
<li><a href="../personal-site/no-node-modules/">Отсутствуют node_modules — нужна установка</a></li>
|
||||
|
||||
<li><a href="../personal-site/netlify-node-version/">Устаревший Node.js 16 в Netlify</a></li>
|
||||
<li><a href="../personal-site/binary-files-in-repo/">Бинарный файл в репозитории</a></li>
|
||||
|
||||
<li><a href="../personal-site/no-analytics/">Yandex Metrika отключена</a></li>
|
||||
|
||||
<li><a href="../personal-site/twitter-handle-mismatch/">Twitter handle не соответствует имени</a></li>
|
||||
|
||||
<li><a href="../personal-site/outdated-doks-theme/">Hugo Doks 0.5 — сильно устарел</a></li>
|
||||
|
||||
<li><a href="../personal-site/digest-stale/">Digest не обновлялся с февраля 2025</a></li>
|
||||
|
||||
<li><a href="../personal-site/copyright-outdated/">Copyright без 2026 года</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -111,6 +117,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Устаревший Node.js 16 в Netlify</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/personal-site/netlify-node-version.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/personal-site\/netlify-node-version.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h1 id="устаревший-nodejs-16-в-netlify">Устаревший Node.js 16 в Netlify</h1>
|
||||
<p><strong>Файл:</strong> <code>netlify.toml:5</code></p>
|
||||
<pre tabindex="0"><code>NODE_VERSION = "16.16.0"
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -111,6 +117,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Yandex Metrika отключена</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/personal-site/no-analytics.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/personal-site\/no-analytics.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h2 id="проблема">Проблема</h2>
|
||||
<p>В <code>params.toml</code>: <code>yandexMetrika = false</code></p>
|
||||
<p>Аналитика с сайта не собирается. Непонятно, есть ли вообще посетители и какие страницы популярны.</p>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -111,6 +117,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Отсутствуют node_modules — нужна установка</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/personal-site/no-node-modules.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/personal-site\/no-node-modules.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h2 id="проблема">Проблема</h2>
|
||||
<p><code>node_modules/</code> отсутствует — проект нельзя собрать без <code>npm install</code>.</p>
|
||||
<h3 id="риск">Риск</h3>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -111,6 +117,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Hugo Doks 0.5 — сильно устарел</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/personal-site/outdated-doks-theme.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/personal-site\/outdated-doks-theme.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h2 id="проблема">Проблема</h2>
|
||||
<p>Проект использует тему <strong>Hyas/Doks v0.5.0</strong> (package.json). Актуальная версия Doks — <strong>1.x</strong> с полной переработкой архитектуры.</p>
|
||||
<h3 id="что-не-так">Что не так</h3>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -111,6 +117,15 @@
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Twitter handle не соответствует имени</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/personal-site/twitter-handle-mismatch.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/personal-site\/twitter-handle-mismatch.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h2 id="проблема">Проблема</h2>
|
||||
<p>В <code>params.toml</code> указан Twitter: <code>@AntonTotoshkin</code></p>
|
||||
<p>Но владелец сайта — <strong>Антон Агальцов</strong>. Ники не совпадают.</p>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -103,6 +107,17 @@
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Завершить Eversolo Companion - этапы 3-9</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/plans/eversolo-companion-phases-3-9.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/plans\/eversolo-companion-phases-3-9.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<h1>Завершить Eversolo Companion - этапы 3-9</h1>
|
||||
<span class="status-badge h5">
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -103,6 +107,17 @@
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Реализовать управление Eversolo из OpenClaw</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/plans/eversolo-control.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/plans\/eversolo-control.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<h1>Реализовать управление Eversolo из OpenClaw</h1>
|
||||
<span class="status-badge h5">
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -103,6 +107,17 @@
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Настроить интеграцию Ollama с OpenClaw</h1>
|
||||
<div class="source-path mb-3">
|
||||
<small class="text-muted">
|
||||
📁 <code id="source-path" class="user-select-all">~/Projects/docs-a2v/content/plans/ollama-openclaw.md</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1 py-0 px-1" onclick="navigator.clipboard.writeText('~/Projects/docs-a2v/content/plans\/ollama-openclaw.md')" title="Копировать путь">
|
||||
📋
|
||||
</button>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<h1>Настроить интеграцию Ollama с OpenClaw</h1>
|
||||
<span class="status-badge h5">
|
||||
|
||||
+74
-26
@@ -2,11 +2,83 @@
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
||||
<url>
|
||||
<loc>/leadera/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/personal-site/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/guides/leadera-audit/</loc>
|
||||
<loc>/leadera/leadera-audit/</loc>
|
||||
<lastmod>2026-03-30T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/leadera/leadera-websocket-gantt/</loc>
|
||||
<lastmod>2026-04-07T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/leadera/leadera-gantt-excel-export/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/leadera/leadera-gantt-filters/</loc>
|
||||
<lastmod>2026-04-07T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/leadera/leadera-gantt-public-access/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/excel/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/export/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/gantt/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/leadera/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/public-access/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/sharing/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/</loc>
|
||||
<lastmod>2026-04-09T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/angular/</loc>
|
||||
<lastmod>2026-04-08T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/api/</loc>
|
||||
<lastmod>2026-04-08T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/go/</loc>
|
||||
<lastmod>2026-04-08T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/leadera/leadera-milestones/</loc>
|
||||
<lastmod>2026-04-08T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/ux/</loc>
|
||||
<lastmod>2026-04-08T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/architecture/</loc>
|
||||
<lastmod>2026-04-07T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/frontend/</loc>
|
||||
<lastmod>2026-04-07T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/realtime/</loc>
|
||||
<lastmod>2026-04-07T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/requirements/</loc>
|
||||
<lastmod>2026-04-07T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/websocket/</loc>
|
||||
<lastmod>2026-04-07T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/leadera/leadera-section-constraints/</loc>
|
||||
<lastmod>2026-04-06T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/diary/2026-03-31/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
@@ -25,12 +97,6 @@
|
||||
</url><url>
|
||||
<loc>/personal-site/digest-stale/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/gantt/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/github/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
@@ -43,9 +109,6 @@
|
||||
</url><url>
|
||||
<loc>/tags/infrastructure/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/leadera/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/opencode/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
@@ -58,9 +121,6 @@
|
||||
</url><url>
|
||||
<loc>/tags/system-utils/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/personal-site/twitter-handle-mismatch/</loc>
|
||||
<lastmod>2026-03-31T00:00:00+00:00</lastmod>
|
||||
@@ -79,12 +139,6 @@
|
||||
</url><url>
|
||||
<loc>/diary/2026-03-30/</loc>
|
||||
<lastmod>2026-03-30T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/architecture/</loc>
|
||||
<lastmod>2026-03-30T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/guides/</loc>
|
||||
<lastmod>2026-03-30T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/mtproxy/</loc>
|
||||
<lastmod>2026-03-30T00:00:00+00:00</lastmod>
|
||||
@@ -109,12 +163,6 @@
|
||||
</url><url>
|
||||
<loc>/tags/amnezia/</loc>
|
||||
<lastmod>2026-03-29T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/angular/</loc>
|
||||
<lastmod>2026-03-29T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/api/</loc>
|
||||
<lastmod>2026-03-29T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/authentication/</loc>
|
||||
<lastmod>2026-03-29T00:00:00+00:00</lastmod>
|
||||
@@ -152,7 +200,7 @@
|
||||
<loc>/kb/eversolo-api/</loc>
|
||||
<lastmod>2026-03-29T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/go/</loc>
|
||||
<loc>/guides/</loc>
|
||||
<lastmod>2026-03-29T00:00:00+00:00</lastmod>
|
||||
</url><url>
|
||||
<loc>/tags/identity/</loc>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,11 +121,15 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
<li><a href="../../leadera/leadera-milestones/">Leadera: Вехи (Milestones) на диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-section-constraints/">Leadera: Ограничения секций в диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../guides/sand-a2v-space/">Подключение sand.a2v.space (Leadera Project)</a></li>
|
||||
|
||||
<li><a href="../../plans/eversolo-companion-phases-3-9/">Завершить Eversolo Companion - этапы 3-9</a></li>
|
||||
|
||||
<li><a href="../../guides/sand-a2v-space/">Подключение sand.a2v.space (Leadera Project)</a></li>
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,22 @@
|
||||
<description>Recent content in Angular on docs.a2v.space</description>
|
||||
<generator>Hugo</generator>
|
||||
<language>ru-ru</language>
|
||||
<lastBuildDate>Sun, 29 Mar 2026 00:00:00 +0000</lastBuildDate>
|
||||
<lastBuildDate>Wed, 08 Apr 2026 00:00:00 +0000</lastBuildDate>
|
||||
<atom:link href="/tags/angular/index.xml" rel="self" type="application/rss+xml" />
|
||||
<item>
|
||||
<title>Leadera: Вехи (Milestones) на диаграмме Ганта</title>
|
||||
<link>/leadera/leadera-milestones/</link>
|
||||
<pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-milestones/</guid>
|
||||
<description><h2 id="бизнес-требования">Бизнес-требования</h2>
<h3 id="br-1-сущность-веха-milestone">BR-1: Сущность «Веха» (Milestone)</h3>
<p>Веха — это маркер на таймлайне диаграммы Ганта, обозначающий важную дату в проекте. В отличие от задач, веха не имеет длительности и привязана к конкретной дате.</p>
<p><strong>Поля вехи:</strong></p>
<table>
 <thead>
 <tr>
 <th>Поле</th>
 <th>Тип</th>
 <th>Описание</th>
 </tr>
 </thead>
 <tbody>
 <tr>
 <td>id</td>
 <td>UUID</td>
 <td>Идентификатор</td>
 </tr>
 <tr>
 <td>gantt_chart_id</td>
 <td>UUID</td>
 <td>Принадлежность к диаграмме (не к секции!)</td>
 </tr>
 <tr>
 <td>title</td>
 <td>string</td>
 <td>Название вехи (обязательное, 1–255 символов)</td>
 </tr>
 <tr>
 <td>date</td>
 <td>date</td>
 <td>Дата вехи (без времени, обязательная)</td>
 </tr>
 <tr>
 <td>color</td>
 <td>string</td>
 <td>Цвет маркера (HEX, по умолчанию <code>#dc3545</code> — красный)</td>
 </tr>
 <tr>
 <td>created_by</td>
 <td>UUID</td>
 <td>Кто создал (не отображается в UI, хранится для аудита)</td>
 </tr>
 <tr>
 <td>updated_by</td>
 <td>UUID</td>
 <td>Кто обновил (не отображается в UI)</td>
 </tr>
 <tr>
 <td>created_at</td>
 <td>timestamp</td>
 <td>Дата создания</td>
 </tr>
 <tr>
 <td>updated_at</td>
 <td>timestamp</td>
 <td>Дата обновления</td>
 </tr>
 <tr>
 <td>deleted_at</td>
 <td>timestamp</td>
 <td>Soft delete</td>
 </tr>
 </tbody>
</table>
<p><strong>Права доступа:</strong> Создание, редактирование и удаление вех доступно участникам с правами выше «только чтение» (аналогично задачам).</p></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Leadera: Ограничения секций в диаграмме Ганта</title>
|
||||
<link>/leadera/leadera-section-constraints/</link>
|
||||
<pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-section-constraints/</guid>
|
||||
<description><h2 id="бизнес-требования">Бизнес-требования</h2>
<h3 id="br-1-лимит-одновременных-задач-concurrency-limit">BR-1: Лимит одновременных задач (Concurrency Limit)</h3>
<p>У каждой секции появляется параметр <strong>max_concurrent_tasks</strong> (целое число ≥ 0, по умолчанию 0).</p>
<p><strong>Поведение:</strong></p>
<ul>
<li>Если <code>max_concurrent_tasks = 3</code> — в любой данный день в этой секции не может быть больше 3 активных задач одновременно</li>
<li>Если <code>max_concurrent_tasks = 0</code> — ограничение отключено, лимита нет</li>
<li>Пересечение считается по дате: задача занимает дни с <code>start_date</code> по <code>end_date</code> включительно. Две задачи «пересекаются», если у них есть хотя бы один общий день</li>
<li>Ограничение работает <strong>только в рамках одной секции</strong>. Задачи из разных секций не влияют друг на друга</li>
</ul>
<p><strong>Срабатывание при:</strong></p></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>29 марта 2026</title>
|
||||
<link>/diary/2026-03-29/</link>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,10 +121,16 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../kb/eversolo-api/">Eversolo T8 API</a></li>
|
||||
<li><a href="../../leadera/leadera-milestones/">Leadera: Вехи (Milestones) на диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-filters/">Leadera — Фильтры и реорганизация списка диаграмм Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-section-constraints/">Leadera: Ограничения секций в диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../plans/eversolo-control/">Реализовать управление Eversolo из OpenClaw</a></li>
|
||||
|
||||
<li><a href="../../kb/eversolo-api/">Eversolo T8 API</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,8 +6,29 @@
|
||||
<description>Recent content in Api on docs.a2v.space</description>
|
||||
<generator>Hugo</generator>
|
||||
<language>ru-ru</language>
|
||||
<lastBuildDate>Sun, 29 Mar 2026 00:00:00 +0000</lastBuildDate>
|
||||
<lastBuildDate>Wed, 08 Apr 2026 00:00:00 +0000</lastBuildDate>
|
||||
<atom:link href="/tags/api/index.xml" rel="self" type="application/rss+xml" />
|
||||
<item>
|
||||
<title>Leadera — Фильтры и реорганизация списка диаграмм Ганта</title>
|
||||
<link>/leadera/leadera-gantt-filters/</link>
|
||||
<pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-gantt-filters/</guid>
|
||||
<description><h1 id="фильтры-и-реорганизация-списка-диаграмм-ганта">Фильтры и реорганизация списка диаграмм Ганта</h1>
<p><strong>Дата:</strong> 07.04.2026
<strong>Статус:</strong> Требования
<strong>Проект:</strong> Leadera (dev — sand.a2v.space)</p>
<hr>
<h2 id="1-постановка-задачи">1. Постановка задачи</h2>
<p>Список диаграмм Ганта должен показывать <strong>все доступные пользователю диаграммы</strong> из всех пространств (spaces), с возможностью фильтрации и поиска. Сейчас список привязан к одному пространству — нужно убрать эту зависимость и добавить удобный toolbar.</p>
<hr>
<h2 id="2-изменения-api">2. Изменения API</h2>
<h3 id="21-новый-endpoint-списка-диаграмм">2.1. Новый endpoint списка диаграмм</h3>
<p><strong>Было:</strong></p>
<pre tabindex="0"><code>GET /api/v1/spaces/{spaceId}/gantt-charts?page=1&amp;per_page=10
</code></pre><p><strong>Стало:</strong></p>
<pre tabindex="0"><code>GET /api/v1/gantt-charts?page=1&amp;per_page=10
</code></pre><p>Пользователь видит все диаграммы из <strong>всех пространств</strong>, где он является подтверждённым участником (статус <code>accepted</code>).</p></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Leadera: Вехи (Milestones) на диаграмме Ганта</title>
|
||||
<link>/leadera/leadera-milestones/</link>
|
||||
<pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-milestones/</guid>
|
||||
<description><h2 id="бизнес-требования">Бизнес-требования</h2>
<h3 id="br-1-сущность-веха-milestone">BR-1: Сущность «Веха» (Milestone)</h3>
<p>Веха — это маркер на таймлайне диаграммы Ганта, обозначающий важную дату в проекте. В отличие от задач, веха не имеет длительности и привязана к конкретной дате.</p>
<p><strong>Поля вехи:</strong></p>
<table>
 <thead>
 <tr>
 <th>Поле</th>
 <th>Тип</th>
 <th>Описание</th>
 </tr>
 </thead>
 <tbody>
 <tr>
 <td>id</td>
 <td>UUID</td>
 <td>Идентификатор</td>
 </tr>
 <tr>
 <td>gantt_chart_id</td>
 <td>UUID</td>
 <td>Принадлежность к диаграмме (не к секции!)</td>
 </tr>
 <tr>
 <td>title</td>
 <td>string</td>
 <td>Название вехи (обязательное, 1–255 символов)</td>
 </tr>
 <tr>
 <td>date</td>
 <td>date</td>
 <td>Дата вехи (без времени, обязательная)</td>
 </tr>
 <tr>
 <td>color</td>
 <td>string</td>
 <td>Цвет маркера (HEX, по умолчанию <code>#dc3545</code> — красный)</td>
 </tr>
 <tr>
 <td>created_by</td>
 <td>UUID</td>
 <td>Кто создал (не отображается в UI, хранится для аудита)</td>
 </tr>
 <tr>
 <td>updated_by</td>
 <td>UUID</td>
 <td>Кто обновил (не отображается в UI)</td>
 </tr>
 <tr>
 <td>created_at</td>
 <td>timestamp</td>
 <td>Дата создания</td>
 </tr>
 <tr>
 <td>updated_at</td>
 <td>timestamp</td>
 <td>Дата обновления</td>
 </tr>
 <tr>
 <td>deleted_at</td>
 <td>timestamp</td>
 <td>Soft delete</td>
 </tr>
 </tbody>
</table>
<p><strong>Права доступа:</strong> Создание, редактирование и удаление вех доступно участникам с правами выше «только чтение» (аналогично задачам).</p></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Leadera: Ограничения секций в диаграмме Ганта</title>
|
||||
<link>/leadera/leadera-section-constraints/</link>
|
||||
<pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-section-constraints/</guid>
|
||||
<description><h2 id="бизнес-требования">Бизнес-требования</h2>
<h3 id="br-1-лимит-одновременных-задач-concurrency-limit">BR-1: Лимит одновременных задач (Concurrency Limit)</h3>
<p>У каждой секции появляется параметр <strong>max_concurrent_tasks</strong> (целое число ≥ 0, по умолчанию 0).</p>
<p><strong>Поведение:</strong></p>
<ul>
<li>Если <code>max_concurrent_tasks = 3</code> — в любой данный день в этой секции не может быть больше 3 активных задач одновременно</li>
<li>Если <code>max_concurrent_tasks = 0</code> — ограничение отключено, лимита нет</li>
<li>Пересечение считается по дате: задача занимает дни с <code>start_date</code> по <code>end_date</code> включительно. Две задачи «пересекаются», если у них есть хотя бы один общий день</li>
<li>Ограничение работает <strong>только в рамках одной секции</strong>. Задачи из разных секций не влияют друг на друга</li>
</ul>
<p><strong>Срабатывание при:</strong></p></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Eversolo T8 API</title>
|
||||
<link>/kb/eversolo-api/</link>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,7 +121,9 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../guides/leadera-audit/">Аудит Leadera — полный отчёт</a></li>
|
||||
<li><a href="../../leadera/leadera-websocket-gantt/">Leadera — WebSocket для диаграмм Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-audit/">Аудит Leadera — полный отчёт</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -6,14 +6,21 @@
|
||||
<description>Recent content in Architecture on docs.a2v.space</description>
|
||||
<generator>Hugo</generator>
|
||||
<language>ru-ru</language>
|
||||
<lastBuildDate>Mon, 30 Mar 2026 00:00:00 +0000</lastBuildDate>
|
||||
<lastBuildDate>Tue, 07 Apr 2026 00:00:00 +0000</lastBuildDate>
|
||||
<atom:link href="/tags/architecture/index.xml" rel="self" type="application/rss+xml" />
|
||||
<item>
|
||||
<title>Аудит Leadera — полный отчёт</title>
|
||||
<link>/guides/leadera-audit/</link>
|
||||
<link>/leadera/leadera-audit/</link>
|
||||
<pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/guides/leadera-audit/</guid>
|
||||
<guid>/leadera/leadera-audit/</guid>
|
||||
<description><h1 id="аудит-проекта-leadera">Аудит проекта Leadera</h1>
<p><strong>Дата:</strong> 30.03.2026<br>
<strong>Версия:</strong> pre-production, миграции 1–5</p>
<hr>
<h2 id="1-обзор-проекта">1. Обзор проекта</h2>
<p>Leadera — SaaS-система управления IT-ландшафтом с диаграммами Ганта.</p>
<h3 id="стек">Стек</h3>
<table>
 <thead>
 <tr>
 <th>Компонент</th>
 <th>Технологии</th>
 </tr>
 </thead>
 <tbody>
 <tr>
 <td>Backend</td>
 <td>Go 1.21+, Gin, PostgreSQL 16, pgx/v5 + sqlx, JWT (HMAC-SHA256)</td>
 </tr>
 <tr>
 <td>Frontend</td>
 <td>Angular 21.1, Bootstrap 5, NgBootstrap, RxJS, Signals</td>
 </tr>
 <tr>
 <td>Инфраструктура</td>
 <td>Docker (Colima), pm2, nginx reverse proxy, VPS reg.ru</td>
 </tr>
 </tbody>
</table>
<h3 id="доменная-модель">Доменная модель</h3>
<pre tabindex="0"><code>User ──┬── Space (через space_members с role)
 │
 └── GanttChart (принадлежит Space)
 ├── GanttSection
 └── GanttTask
</code></pre><p><strong>Роли:</strong> admin, editor, viewer — назначаются <strong>на уровне Space</strong> через таблицу <code>space_members</code>.</p></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Leadera — WebSocket для диаграмм Ганта</title>
|
||||
<link>/leadera/leadera-websocket-gantt/</link>
|
||||
<pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-websocket-gantt/</guid>
|
||||
<description><h1 id="websocket-для-диаграмм-ганта--realtime-совместное-редактирование">WebSocket для диаграмм Ганта — realtime-совместное редактирование</h1>
<p><strong>Дата:</strong> 07.04.2026
<strong>Статус:</strong> Планирование
<strong>Проект:</strong> Leadera (dev — sand.a2v.space)</p>
<hr>
<h2 id="1-постановка-задачи">1. Постановка задачи</h2>
<p>Диаграммы Ганта должны обновляться в реальном времени при совместной работе: если два или более пользователей редактируют один Гант, изменения каждого мгновенно отображаются у остальных — без перезагрузки страницы и ручного обновления.</p>
<hr>
<h2 id="2-архитектура">2. Архитектура</h2>
<pre tabindex="0"><code>Angular frontend ←── WebSocket ──→ Go backend (Hub) ──→ PostgreSQL
 │ │
 │ HTTP (мутации) │ Broadcast
 └──→ REST API ──→ Service ──→ DB ──→ Hub ──→ все клиенты комнаты
</code></pre><p><strong>Ключевой принцип:</strong> мутации данных идут через HTTP (REST), WebSocket используется <strong>только для доставки уведомлений</strong>. Сервер — источник истины, клиент не может обойти проверки через WS.</p></description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,25 +121,25 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../personal-site/">Личный сайт (agaltsovav.ru)</a></li>
|
||||
|
||||
<li><a href="../../guides/leadera-audit/">Аудит Leadera — полный отчёт</a></li>
|
||||
|
||||
<li><a href="../../personal-site/copyright-outdated/">Copyright без 2026 года</a></li>
|
||||
|
||||
<li><a href="../../personal-site/digest-stale/">Digest не обновлялся с февраля 2025</a></li>
|
||||
|
||||
<li><a href="../../personal-site/outdated-doks-theme/">Hugo Doks 0.5 — сильно устарел</a></li>
|
||||
|
||||
<li><a href="../../personal-site/twitter-handle-mismatch/">Twitter handle не соответствует имени</a></li>
|
||||
|
||||
<li><a href="../../personal-site/no-analytics/">Yandex Metrika отключена</a></li>
|
||||
|
||||
<li><a href="../../personal-site/binary-files-in-repo/">Бинарный файл в репозитории</a></li>
|
||||
<li><a href="../../personal-site/netlify-node-version/">Устаревший Node.js 16 в Netlify</a></li>
|
||||
|
||||
<li><a href="../../personal-site/no-node-modules/">Отсутствуют node_modules — нужна установка</a></li>
|
||||
|
||||
<li><a href="../../personal-site/netlify-node-version/">Устаревший Node.js 16 в Netlify</a></li>
|
||||
<li><a href="../../personal-site/binary-files-in-repo/">Бинарный файл в репозитории</a></li>
|
||||
|
||||
<li><a href="../../personal-site/no-analytics/">Yandex Metrika отключена</a></li>
|
||||
|
||||
<li><a href="../../personal-site/twitter-handle-mismatch/">Twitter handle не соответствует имени</a></li>
|
||||
|
||||
<li><a href="../../personal-site/outdated-doks-theme/">Hugo Doks 0.5 — сильно устарел</a></li>
|
||||
|
||||
<li><a href="../../personal-site/digest-stale/">Digest не обновлялся с февраля 2025</a></li>
|
||||
|
||||
<li><a href="../../personal-site/copyright-outdated/">Copyright без 2026 года</a></li>
|
||||
|
||||
<li><a href="../../personal-site/">Личный сайт (agaltsovav.ru)</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-audit/">Аудит Leadera — полный отчёт</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
</item>
|
||||
<item>
|
||||
<title>Аудит Leadera — полный отчёт</title>
|
||||
<link>/guides/leadera-audit/</link>
|
||||
<link>/leadera/leadera-audit/</link>
|
||||
<pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/guides/leadera-audit/</guid>
|
||||
<guid>/leadera/leadera-audit/</guid>
|
||||
<description><h1 id="аудит-проекта-leadera">Аудит проекта Leadera</h1>
<p><strong>Дата:</strong> 30.03.2026<br>
<strong>Версия:</strong> pre-production, миграции 1–5</p>
<hr>
<h2 id="1-обзор-проекта">1. Обзор проекта</h2>
<p>Leadera — SaaS-система управления IT-ландшафтом с диаграммами Ганта.</p>
<h3 id="стек">Стек</h3>
<table>
 <thead>
 <tr>
 <th>Компонент</th>
 <th>Технологии</th>
 </tr>
 </thead>
 <tbody>
 <tr>
 <td>Backend</td>
 <td>Go 1.21+, Gin, PostgreSQL 16, pgx/v5 + sqlx, JWT (HMAC-SHA256)</td>
 </tr>
 <tr>
 <td>Frontend</td>
 <td>Angular 21.1, Bootstrap 5, NgBootstrap, RxJS, Signals</td>
 </tr>
 <tr>
 <td>Инфраструктура</td>
 <td>Docker (Colima), pm2, nginx reverse proxy, VPS reg.ru</td>
 </tr>
 </tbody>
</table>
<h3 id="доменная-модель">Доменная модель</h3>
<pre tabindex="0"><code>User ──┬── Space (через space_members с role)
 │
 └── GanttChart (принадлежит Space)
 ├── GanttSection
 └── GanttTask
</code></pre><p><strong>Роли:</strong> admin, editor, viewer — назначаются <strong>на уровне Space</strong> через таблицу <code>space_members</code>.</p></description>
|
||||
</item>
|
||||
<item>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,11 +121,11 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
<li><a href="../../plans/eversolo-companion-phases-3-9/">Завершить Eversolo Companion - этапы 3-9</a></li>
|
||||
|
||||
<li><a href="../../creds/eversolo-companion-postgres/">Eversolo Companion PostgreSQL</a></li>
|
||||
|
||||
<li><a href="../../plans/eversolo-companion-phases-3-9/">Завершить Eversolo Companion - этапы 3-9</a></li>
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,10 +121,10 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../creds/leadera-db-docker/">Mac Studio — leadera-api БД (docker-compose)</a></li>
|
||||
|
||||
<li><a href="../../creds/leadera-db-local/">Mac Studio — leadera-api БД (локальная)</a></li>
|
||||
|
||||
<li><a href="../../creds/leadera-db-docker/">Mac Studio — leadera-api БД (docker-compose)</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,15 +121,15 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
<li><a href="../../creds/eversolo-companion-postgres/">Eversolo Companion PostgreSQL</a></li>
|
||||
|
||||
<li><a href="../../creds/leadera-db-local/">Mac Studio — leadera-api БД (локальная)</a></li>
|
||||
<li><a href="../../guides/sand-a2v-space/">Подключение sand.a2v.space (Leadera Project)</a></li>
|
||||
|
||||
<li><a href="../../plans/eversolo-companion-phases-3-9/">Завершить Eversolo Companion - этапы 3-9</a></li>
|
||||
|
||||
<li><a href="../../guides/sand-a2v-space/">Подключение sand.a2v.space (Leadera Project)</a></li>
|
||||
<li><a href="../../creds/leadera-db-local/">Mac Studio — leadera-api БД (локальная)</a></li>
|
||||
|
||||
<li><a href="../../creds/eversolo-companion-postgres/">Eversolo Companion PostgreSQL</a></li>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,10 +121,10 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
<li><a href="../../creds/docs-a2v-space-auth/">docs.a2v.space Basic Auth</a></li>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,15 +121,15 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
<li><a href="../../creds/eversolo-companion-postgres/">Eversolo Companion PostgreSQL</a></li>
|
||||
|
||||
<li><a href="../../kb/eversolo-api/">Eversolo T8 API</a></li>
|
||||
<li><a href="../../plans/eversolo-control/">Реализовать управление Eversolo из OpenClaw</a></li>
|
||||
|
||||
<li><a href="../../plans/eversolo-companion-phases-3-9/">Завершить Eversolo Companion - этапы 3-9</a></li>
|
||||
|
||||
<li><a href="../../plans/eversolo-control/">Реализовать управление Eversolo из OpenClaw</a></li>
|
||||
<li><a href="../../kb/eversolo-api/">Eversolo T8 API</a></li>
|
||||
|
||||
<li><a href="../../creds/eversolo-companion-postgres/">Eversolo Companion PostgreSQL</a></li>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Excel — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Excel</h1>
|
||||
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-excel-export/">Leadera — Экспорт диаграммы Ганта в Excel</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>Excel on docs.a2v.space</title>
|
||||
<link>/tags/excel/</link>
|
||||
<description>Recent content in Excel on docs.a2v.space</description>
|
||||
<generator>Hugo</generator>
|
||||
<language>ru-ru</language>
|
||||
<lastBuildDate>Thu, 09 Apr 2026 00:00:00 +0000</lastBuildDate>
|
||||
<atom:link href="/tags/excel/index.xml" rel="self" type="application/rss+xml" />
|
||||
<item>
|
||||
<title>Leadera — Экспорт диаграммы Ганта в Excel</title>
|
||||
<link>/leadera/leadera-gantt-excel-export/</link>
|
||||
<pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-gantt-excel-export/</guid>
|
||||
<description><h1 id="экспорт-диаграммы-ганта-в-excel">Экспорт диаграммы Ганта в Excel</h1>
<p><strong>Дата:</strong> 09.04.2026
<strong>Статус:</strong> Требования
<strong>Проект:</strong> Leadera</p>
<hr>
<h2 id="общее-описание">Общее описание</h2>
<p>Пользователь нажимает кнопку экспорта в интерфейсе диаграммы Ганта. Система запрашивает диапазон дат выгрузки (по умолчанию — текущий видимый диапазон из интерфейса). После указания диапазона скачивается Excel-файл с визуальным представлением диаграммы Ганта.</p>
<hr>
<h2 id="вводные-данные">Вводные данные</h2>
<ul>
<li><strong>Источник:</strong> текущая диаграмма Ганта проекта (один проект = одна диаграмма)</li>
<li><strong>Диапазон дат:</strong> пользователь указывает начальную и конечную дату. По умолчанию подставляется видимый в момент запроса диапазон из интерфейса</li>
<li><strong>Масштаб таймлайна:</strong> берётся тот, который выбран пользователем в интерфейсе в момент запроса (не сохранённый в системе, а текущий). Варианты: по ячейкам или по дням</li>
</ul>
<hr>
<h2 id="структура-excel-файла">Структура Excel-файла</h2>
<h3 id="шапка-первые-строки">Шапка (первые строки)</h3>
<ul>
<li>Название диаграммы (название проекта)</li>
<li>Диапазон дат, в рамках которого сделана выгрузка</li>
</ul>
<h3 id="лист--один">Лист — один</h3>
<p>Вся диаграмма размещается на одном листе.</p></description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Export — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Export</h1>
|
||||
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-excel-export/">Leadera — Экспорт диаграммы Ганта в Excel</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>Export on docs.a2v.space</title>
|
||||
<link>/tags/export/</link>
|
||||
<description>Recent content in Export on docs.a2v.space</description>
|
||||
<generator>Hugo</generator>
|
||||
<language>ru-ru</language>
|
||||
<lastBuildDate>Thu, 09 Apr 2026 00:00:00 +0000</lastBuildDate>
|
||||
<atom:link href="/tags/export/index.xml" rel="self" type="application/rss+xml" />
|
||||
<item>
|
||||
<title>Leadera — Экспорт диаграммы Ганта в Excel</title>
|
||||
<link>/leadera/leadera-gantt-excel-export/</link>
|
||||
<pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-gantt-excel-export/</guid>
|
||||
<description><h1 id="экспорт-диаграммы-ганта-в-excel">Экспорт диаграммы Ганта в Excel</h1>
<p><strong>Дата:</strong> 09.04.2026
<strong>Статус:</strong> Требования
<strong>Проект:</strong> Leadera</p>
<hr>
<h2 id="общее-описание">Общее описание</h2>
<p>Пользователь нажимает кнопку экспорта в интерфейсе диаграммы Ганта. Система запрашивает диапазон дат выгрузки (по умолчанию — текущий видимый диапазон из интерфейса). После указания диапазона скачивается Excel-файл с визуальным представлением диаграммы Ганта.</p>
<hr>
<h2 id="вводные-данные">Вводные данные</h2>
<ul>
<li><strong>Источник:</strong> текущая диаграмма Ганта проекта (один проект = одна диаграмма)</li>
<li><strong>Диапазон дат:</strong> пользователь указывает начальную и конечную дату. По умолчанию подставляется видимый в момент запроса диапазон из интерфейса</li>
<li><strong>Масштаб таймлайна:</strong> берётся тот, который выбран пользователем в интерфейсе в момент запроса (не сохранённый в системе, а текущий). Варианты: по ячейкам или по дням</li>
</ul>
<hr>
<h2 id="структура-excel-файла">Структура Excel-файла</h2>
<h3 id="шапка-первые-строки">Шапка (первые строки)</h3>
<ul>
<li>Название диаграммы (название проекта)</li>
<li>Диапазон дат, в рамках которого сделана выгрузка</li>
</ul>
<h3 id="лист--один">Лист — один</h3>
<p>Вся диаграмма размещается на одном листе.</p></description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Frontend — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Frontend</h1>
|
||||
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-filters/">Leadera — Фильтры и реорганизация списка диаграмм Ганта</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>Frontend on docs.a2v.space</title>
|
||||
<link>/tags/frontend/</link>
|
||||
<description>Recent content in Frontend on docs.a2v.space</description>
|
||||
<generator>Hugo</generator>
|
||||
<language>ru-ru</language>
|
||||
<lastBuildDate>Tue, 07 Apr 2026 00:00:00 +0000</lastBuildDate>
|
||||
<atom:link href="/tags/frontend/index.xml" rel="self" type="application/rss+xml" />
|
||||
<item>
|
||||
<title>Leadera — Фильтры и реорганизация списка диаграмм Ганта</title>
|
||||
<link>/leadera/leadera-gantt-filters/</link>
|
||||
<pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-gantt-filters/</guid>
|
||||
<description><h1 id="фильтры-и-реорганизация-списка-диаграмм-ганта">Фильтры и реорганизация списка диаграмм Ганта</h1>
<p><strong>Дата:</strong> 07.04.2026
<strong>Статус:</strong> Требования
<strong>Проект:</strong> Leadera (dev — sand.a2v.space)</p>
<hr>
<h2 id="1-постановка-задачи">1. Постановка задачи</h2>
<p>Список диаграмм Ганта должен показывать <strong>все доступные пользователю диаграммы</strong> из всех пространств (spaces), с возможностью фильтрации и поиска. Сейчас список привязан к одному пространству — нужно убрать эту зависимость и добавить удобный toolbar.</p>
<hr>
<h2 id="2-изменения-api">2. Изменения API</h2>
<h3 id="21-новый-endpoint-списка-диаграмм">2.1. Новый endpoint списка диаграмм</h3>
<p><strong>Было:</strong></p>
<pre tabindex="0"><code>GET /api/v1/spaces/{spaceId}/gantt-charts?page=1&amp;per_page=10
</code></pre><p><strong>Стало:</strong></p>
<pre tabindex="0"><code>GET /api/v1/gantt-charts?page=1&amp;per_page=10
</code></pre><p>Пользователь видит все диаграммы из <strong>всех пространств</strong>, где он является подтверждённым участником (статус <code>accepted</code>).</p></description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,6 +121,18 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-public-access/">Leadera — Публичный доступ к диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-excel-export/">Leadera — Экспорт диаграммы Ганта в Excel</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-milestones/">Leadera: Вехи (Milestones) на диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-filters/">Leadera — Фильтры и реорганизация списка диаграмм Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-websocket-gantt/">Leadera — WebSocket для диаграмм Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-section-constraints/">Leadera: Ограничения секций в диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../diary/2026-03-31/">31 марта 2026</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,11 +121,15 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
<li><a href="../../leadera/leadera-milestones/">Leadera: Вехи (Milestones) на диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-section-constraints/">Leadera: Ограничения секций в диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../guides/sand-a2v-space/">Подключение sand.a2v.space (Leadera Project)</a></li>
|
||||
|
||||
<li><a href="../../plans/eversolo-companion-phases-3-9/">Завершить Eversolo Companion - этапы 3-9</a></li>
|
||||
|
||||
<li><a href="../../guides/sand-a2v-space/">Подключение sand.a2v.space (Leadera Project)</a></li>
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,22 @@
|
||||
<description>Recent content in Go on docs.a2v.space</description>
|
||||
<generator>Hugo</generator>
|
||||
<language>ru-ru</language>
|
||||
<lastBuildDate>Sun, 29 Mar 2026 00:00:00 +0000</lastBuildDate>
|
||||
<lastBuildDate>Wed, 08 Apr 2026 00:00:00 +0000</lastBuildDate>
|
||||
<atom:link href="/tags/go/index.xml" rel="self" type="application/rss+xml" />
|
||||
<item>
|
||||
<title>Leadera: Вехи (Milestones) на диаграмме Ганта</title>
|
||||
<link>/leadera/leadera-milestones/</link>
|
||||
<pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-milestones/</guid>
|
||||
<description><h2 id="бизнес-требования">Бизнес-требования</h2>
<h3 id="br-1-сущность-веха-milestone">BR-1: Сущность «Веха» (Milestone)</h3>
<p>Веха — это маркер на таймлайне диаграммы Ганта, обозначающий важную дату в проекте. В отличие от задач, веха не имеет длительности и привязана к конкретной дате.</p>
<p><strong>Поля вехи:</strong></p>
<table>
 <thead>
 <tr>
 <th>Поле</th>
 <th>Тип</th>
 <th>Описание</th>
 </tr>
 </thead>
 <tbody>
 <tr>
 <td>id</td>
 <td>UUID</td>
 <td>Идентификатор</td>
 </tr>
 <tr>
 <td>gantt_chart_id</td>
 <td>UUID</td>
 <td>Принадлежность к диаграмме (не к секции!)</td>
 </tr>
 <tr>
 <td>title</td>
 <td>string</td>
 <td>Название вехи (обязательное, 1–255 символов)</td>
 </tr>
 <tr>
 <td>date</td>
 <td>date</td>
 <td>Дата вехи (без времени, обязательная)</td>
 </tr>
 <tr>
 <td>color</td>
 <td>string</td>
 <td>Цвет маркера (HEX, по умолчанию <code>#dc3545</code> — красный)</td>
 </tr>
 <tr>
 <td>created_by</td>
 <td>UUID</td>
 <td>Кто создал (не отображается в UI, хранится для аудита)</td>
 </tr>
 <tr>
 <td>updated_by</td>
 <td>UUID</td>
 <td>Кто обновил (не отображается в UI)</td>
 </tr>
 <tr>
 <td>created_at</td>
 <td>timestamp</td>
 <td>Дата создания</td>
 </tr>
 <tr>
 <td>updated_at</td>
 <td>timestamp</td>
 <td>Дата обновления</td>
 </tr>
 <tr>
 <td>deleted_at</td>
 <td>timestamp</td>
 <td>Soft delete</td>
 </tr>
 </tbody>
</table>
<p><strong>Права доступа:</strong> Создание, редактирование и удаление вех доступно участникам с правами выше «только чтение» (аналогично задачам).</p></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Leadera: Ограничения секций в диаграмме Ганта</title>
|
||||
<link>/leadera/leadera-section-constraints/</link>
|
||||
<pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/leadera/leadera-section-constraints/</guid>
|
||||
<description><h2 id="бизнес-требования">Бизнес-требования</h2>
<h3 id="br-1-лимит-одновременных-задач-concurrency-limit">BR-1: Лимит одновременных задач (Concurrency Limit)</h3>
<p>У каждой секции появляется параметр <strong>max_concurrent_tasks</strong> (целое число ≥ 0, по умолчанию 0).</p>
<p><strong>Поведение:</strong></p>
<ul>
<li>Если <code>max_concurrent_tasks = 3</code> — в любой данный день в этой секции не может быть больше 3 активных задач одновременно</li>
<li>Если <code>max_concurrent_tasks = 0</code> — ограничение отключено, лимита нет</li>
<li>Пересечение считается по дате: задача занимает дни с <code>start_date</code> по <code>end_date</code> включительно. Две задачи «пересекаются», если у них есть хотя бы один общий день</li>
<li>Ограничение работает <strong>только в рамках одной секции</strong>. Задачи из разных секций не влияют друг на друга</li>
</ul>
<p><strong>Срабатывание при:</strong></p></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>29 марта 2026</title>
|
||||
<link>/diary/2026-03-29/</link>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
+86
-62
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../creds/">Креды</a></li>
|
||||
@@ -115,91 +121,109 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../tags/audit/">Audit</a></li>
|
||||
<li><a href="../tags/sharing/">Sharing</a></li>
|
||||
|
||||
<li><a href="../tags/canvas/">Canvas</a></li>
|
||||
|
||||
<li><a href="../tags/gantt/">Gantt</a></li>
|
||||
|
||||
<li><a href="../tags/github/">Github</a></li>
|
||||
|
||||
<li><a href="../tags/hugo/">Hugo</a></li>
|
||||
|
||||
<li><a href="../tags/infrastructure/">Infrastructure</a></li>
|
||||
<li><a href="../tags/public-access/">Public-Access</a></li>
|
||||
|
||||
<li><a href="../tags/leadera/">Leadera</a></li>
|
||||
|
||||
<li><a href="../tags/opencode/">Opencode</a></li>
|
||||
<li><a href="../tags/gantt/">Gantt</a></li>
|
||||
|
||||
<li><a href="../tags/personal-site/">Personal-Site</a></li>
|
||||
<li><a href="../tags/export/">Export</a></li>
|
||||
|
||||
<li><a href="../tags/roadmap/">Roadmap</a></li>
|
||||
<li><a href="../tags/excel/">Excel</a></li>
|
||||
|
||||
<li><a href="../tags/system-utils/">System-Utils</a></li>
|
||||
|
||||
<li><a href="../tags/architecture/">Architecture</a></li>
|
||||
|
||||
<li><a href="../tags/mtproxy/">Mtproxy</a></li>
|
||||
|
||||
<li><a href="../tags/ollama/">Ollama</a></li>
|
||||
|
||||
<li><a href="../tags/qwen/">Qwen</a></li>
|
||||
|
||||
<li><a href="../tags/security/">Security</a></li>
|
||||
|
||||
<li><a href="../tags/tasktracker/">Tasktracker</a></li>
|
||||
|
||||
<li><a href="../tags/vikunja/">Vikunja</a></li>
|
||||
|
||||
<li><a href="../tags/amnezia/">Amnezia</a></li>
|
||||
|
||||
<li><a href="../tags/angular/">Angular</a></li>
|
||||
|
||||
<li><a href="../tags/api/">Api</a></li>
|
||||
|
||||
<li><a href="../tags/authentication/">Authentication</a></li>
|
||||
|
||||
<li><a href="../tags/companion/">Companion</a></li>
|
||||
|
||||
<li><a href="../tags/database/">Database</a></li>
|
||||
|
||||
<li><a href="../tags/devops/">Devops</a></li>
|
||||
|
||||
<li><a href="../tags/docker/">Docker</a></li>
|
||||
|
||||
<li><a href="../tags/docker-compose/">Docker-Compose</a></li>
|
||||
|
||||
<li><a href="../tags/docs/">Docs</a></li>
|
||||
|
||||
<li><a href="../tags/eversolo/">Eversolo</a></li>
|
||||
<li><a href="../tags/ux/">Ux</a></li>
|
||||
|
||||
<li><a href="../tags/go/">Go</a></li>
|
||||
|
||||
<li><a href="../tags/identity/">Identity</a></li>
|
||||
<li><a href="../tags/api/">Api</a></li>
|
||||
|
||||
<li><a href="../tags/llm/">Llm</a></li>
|
||||
<li><a href="../tags/angular/">Angular</a></li>
|
||||
|
||||
<li><a href="../tags/music/">Music</a></li>
|
||||
<li><a href="../tags/websocket/">Websocket</a></li>
|
||||
|
||||
<li><a href="../tags/nginx/">Nginx</a></li>
|
||||
<li><a href="../tags/requirements/">Requirements</a></li>
|
||||
|
||||
<li><a href="../tags/openclaw/">Openclaw</a></li>
|
||||
<li><a href="../tags/realtime/">Realtime</a></li>
|
||||
|
||||
<li><a href="../tags/postgresql/">Postgresql</a></li>
|
||||
<li><a href="../tags/frontend/">Frontend</a></li>
|
||||
|
||||
<li><a href="../tags/reg.ru/">Reg.ru</a></li>
|
||||
<li><a href="../tags/architecture/">Architecture</a></li>
|
||||
|
||||
<li><a href="../tags/reverse-proxy/">Reverse-Proxy</a></li>
|
||||
<li><a href="../tags/system-utils/">System-Utils</a></li>
|
||||
|
||||
<li><a href="../tags/ssh/">Ssh</a></li>
|
||||
<li><a href="../tags/roadmap/">Roadmap</a></li>
|
||||
|
||||
<li><a href="../tags/streamer/">Streamer</a></li>
|
||||
<li><a href="../tags/personal-site/">Personal-Site</a></li>
|
||||
|
||||
<li><a href="../tags/tunnel/">Tunnel</a></li>
|
||||
<li><a href="../tags/opencode/">Opencode</a></li>
|
||||
|
||||
<li><a href="../tags/infrastructure/">Infrastructure</a></li>
|
||||
|
||||
<li><a href="../tags/hugo/">Hugo</a></li>
|
||||
|
||||
<li><a href="../tags/github/">Github</a></li>
|
||||
|
||||
<li><a href="../tags/canvas/">Canvas</a></li>
|
||||
|
||||
<li><a href="../tags/audit/">Audit</a></li>
|
||||
|
||||
<li><a href="../tags/vikunja/">Vikunja</a></li>
|
||||
|
||||
<li><a href="../tags/tasktracker/">Tasktracker</a></li>
|
||||
|
||||
<li><a href="../tags/security/">Security</a></li>
|
||||
|
||||
<li><a href="../tags/qwen/">Qwen</a></li>
|
||||
|
||||
<li><a href="../tags/ollama/">Ollama</a></li>
|
||||
|
||||
<li><a href="../tags/mtproxy/">Mtproxy</a></li>
|
||||
|
||||
<li><a href="../tags/vps/">Vps</a></li>
|
||||
|
||||
<li><a href="../tags/vpn/">Vpn</a></li>
|
||||
|
||||
<li><a href="../tags/vps/">Vps</a></li>
|
||||
<li><a href="../tags/tunnel/">Tunnel</a></li>
|
||||
|
||||
<li><a href="../tags/streamer/">Streamer</a></li>
|
||||
|
||||
<li><a href="../tags/ssh/">Ssh</a></li>
|
||||
|
||||
<li><a href="../tags/reverse-proxy/">Reverse-Proxy</a></li>
|
||||
|
||||
<li><a href="../tags/reg.ru/">Reg.ru</a></li>
|
||||
|
||||
<li><a href="../tags/postgresql/">Postgresql</a></li>
|
||||
|
||||
<li><a href="../tags/openclaw/">Openclaw</a></li>
|
||||
|
||||
<li><a href="../tags/nginx/">Nginx</a></li>
|
||||
|
||||
<li><a href="../tags/music/">Music</a></li>
|
||||
|
||||
<li><a href="../tags/llm/">Llm</a></li>
|
||||
|
||||
<li><a href="../tags/identity/">Identity</a></li>
|
||||
|
||||
<li><a href="../tags/eversolo/">Eversolo</a></li>
|
||||
|
||||
<li><a href="../tags/docs/">Docs</a></li>
|
||||
|
||||
<li><a href="../tags/docker-compose/">Docker-Compose</a></li>
|
||||
|
||||
<li><a href="../tags/docker/">Docker</a></li>
|
||||
|
||||
<li><a href="../tags/devops/">Devops</a></li>
|
||||
|
||||
<li><a href="../tags/database/">Database</a></li>
|
||||
|
||||
<li><a href="../tags/companion/">Companion</a></li>
|
||||
|
||||
<li><a href="../tags/authentication/">Authentication</a></li>
|
||||
|
||||
<li><a href="../tags/amnezia/">Amnezia</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
+106
-43
@@ -6,8 +6,113 @@
|
||||
<description>Recent content in Tags on docs.a2v.space</description>
|
||||
<generator>Hugo</generator>
|
||||
<language>ru-ru</language>
|
||||
<lastBuildDate>Tue, 31 Mar 2026 00:00:00 +0000</lastBuildDate>
|
||||
<lastBuildDate>Thu, 09 Apr 2026 00:00:00 +0000</lastBuildDate>
|
||||
<atom:link href="/tags/index.xml" rel="self" type="application/rss+xml" />
|
||||
<item>
|
||||
<title>Excel</title>
|
||||
<link>/tags/excel/</link>
|
||||
<pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/excel/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Export</title>
|
||||
<link>/tags/export/</link>
|
||||
<pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/export/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Gantt</title>
|
||||
<link>/tags/gantt/</link>
|
||||
<pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/gantt/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Leadera</title>
|
||||
<link>/tags/leadera/</link>
|
||||
<pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/leadera/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Public-Access</title>
|
||||
<link>/tags/public-access/</link>
|
||||
<pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/public-access/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Sharing</title>
|
||||
<link>/tags/sharing/</link>
|
||||
<pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/sharing/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Angular</title>
|
||||
<link>/tags/angular/</link>
|
||||
<pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/angular/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Api</title>
|
||||
<link>/tags/api/</link>
|
||||
<pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/api/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Go</title>
|
||||
<link>/tags/go/</link>
|
||||
<pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/go/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Ux</title>
|
||||
<link>/tags/ux/</link>
|
||||
<pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/ux/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Architecture</title>
|
||||
<link>/tags/architecture/</link>
|
||||
<pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/architecture/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Frontend</title>
|
||||
<link>/tags/frontend/</link>
|
||||
<pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/frontend/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Realtime</title>
|
||||
<link>/tags/realtime/</link>
|
||||
<pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/realtime/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Requirements</title>
|
||||
<link>/tags/requirements/</link>
|
||||
<pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/requirements/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Websocket</title>
|
||||
<link>/tags/websocket/</link>
|
||||
<pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/websocket/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Audit</title>
|
||||
<link>/tags/audit/</link>
|
||||
@@ -22,13 +127,6 @@
|
||||
<guid>/tags/canvas/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Gantt</title>
|
||||
<link>/tags/gantt/</link>
|
||||
<pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/gantt/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Github</title>
|
||||
<link>/tags/github/</link>
|
||||
@@ -50,13 +148,6 @@
|
||||
<guid>/tags/infrastructure/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Leadera</title>
|
||||
<link>/tags/leadera/</link>
|
||||
<pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/leadera/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Opencode</title>
|
||||
<link>/tags/opencode/</link>
|
||||
@@ -85,13 +176,6 @@
|
||||
<guid>/tags/system-utils/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Architecture</title>
|
||||
<link>/tags/architecture/</link>
|
||||
<pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/architecture/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Mtproxy</title>
|
||||
<link>/tags/mtproxy/</link>
|
||||
@@ -141,20 +225,6 @@
|
||||
<guid>/tags/amnezia/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Angular</title>
|
||||
<link>/tags/angular/</link>
|
||||
<pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/angular/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Api</title>
|
||||
<link>/tags/api/</link>
|
||||
<pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/api/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Authentication</title>
|
||||
<link>/tags/authentication/</link>
|
||||
@@ -211,13 +281,6 @@
|
||||
<guid>/tags/eversolo/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Go</title>
|
||||
<link>/tags/go/</link>
|
||||
<pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate>
|
||||
<guid>/tags/go/</guid>
|
||||
<description></description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Identity</title>
|
||||
<link>/tags/identity/</link>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -119,11 +125,11 @@
|
||||
|
||||
<li><a href="../../diary/2026-03-30/">30 марта 2026</a></li>
|
||||
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
<li><a href="../../creds/vps-reg-ru/">VPS reg.ru</a></li>
|
||||
|
||||
<li><a href="../../creds/vpn-vps/">VPN VPS</a></li>
|
||||
|
||||
<li><a href="../../creds/vps-reg-ru/">VPS reg.ru</a></li>
|
||||
<li><a href="../../diary/2026-03-29/">29 марта 2026</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,16 +121,28 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../guides/leadera-audit/">Аудит Leadera — полный отчёт</a></li>
|
||||
<li><a href="../../leadera/leadera-gantt-public-access/">Leadera — Публичный доступ к диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-excel-export/">Leadera — Экспорт диаграммы Ганта в Excel</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-milestones/">Leadera: Вехи (Milestones) на диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-filters/">Leadera — Фильтры и реорганизация списка диаграмм Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-websocket-gantt/">Leadera — WebSocket для диаграмм Ганта</a></li>
|
||||
|
||||
<li><a href="../../leadera/leadera-section-constraints/">Leadera: Ограничения секций в диаграмме Ганта</a></li>
|
||||
|
||||
<li><a href="../../diary/2026-03-31/">31 марта 2026</a></li>
|
||||
|
||||
<li><a href="../../diary/2026-03-30/">30 марта 2026</a></li>
|
||||
|
||||
<li><a href="../../creds/leadera-db-docker/">Mac Studio — leadera-api БД (docker-compose)</a></li>
|
||||
<li><a href="../../leadera/leadera-audit/">Аудит Leadera — полный отчёт</a></li>
|
||||
|
||||
<li><a href="../../creds/leadera-db-local/">Mac Studio — leadera-api БД (локальная)</a></li>
|
||||
|
||||
<li><a href="../../creds/leadera-db-docker/">Mac Studio — leadera-api БД (docker-compose)</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,15 +121,15 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../creds/docs-a2v-space-auth/">docs.a2v.space Basic Auth</a></li>
|
||||
|
||||
<li><a href="../../guides/vps-reverse-proxy/">Настройка VPS как реверс-прокси</a></li>
|
||||
|
||||
<li><a href="../../guides/claw-a2v-space/">Подключение claw.a2v.space (OpenClaw)</a></li>
|
||||
<li><a href="../../guides/sand-a2v-space/">Подключение sand.a2v.space (Leadera Project)</a></li>
|
||||
|
||||
<li><a href="../../guides/code-a2v-space/">Подключение code.a2v.space (OpenCode)</a></li>
|
||||
|
||||
<li><a href="../../guides/sand-a2v-space/">Подключение sand.a2v.space (Leadera Project)</a></li>
|
||||
<li><a href="../../guides/claw-a2v-space/">Подключение claw.a2v.space (OpenClaw)</a></li>
|
||||
|
||||
<li><a href="../../guides/vps-reverse-proxy/">Настройка VPS как реверс-прокси</a></li>
|
||||
|
||||
<li><a href="../../creds/docs-a2v-space-auth/">docs.a2v.space Basic Auth</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,11 +121,11 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../plans/ollama-openclaw/">Настроить интеграцию Ollama с OpenClaw</a></li>
|
||||
<li><a href="../../plans/eversolo-control/">Реализовать управление Eversolo из OpenClaw</a></li>
|
||||
|
||||
<li><a href="../../guides/claw-a2v-space/">Подключение claw.a2v.space (OpenClaw)</a></li>
|
||||
|
||||
<li><a href="../../plans/eversolo-control/">Реализовать управление Eversolo из OpenClaw</a></li>
|
||||
<li><a href="../../plans/ollama-openclaw/">Настроить интеграцию Ollama с OpenClaw</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,23 +121,23 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../personal-site/">Личный сайт (agaltsovav.ru)</a></li>
|
||||
|
||||
<li><a href="../../personal-site/copyright-outdated/">Copyright без 2026 года</a></li>
|
||||
|
||||
<li><a href="../../personal-site/digest-stale/">Digest не обновлялся с февраля 2025</a></li>
|
||||
|
||||
<li><a href="../../personal-site/outdated-doks-theme/">Hugo Doks 0.5 — сильно устарел</a></li>
|
||||
|
||||
<li><a href="../../personal-site/twitter-handle-mismatch/">Twitter handle не соответствует имени</a></li>
|
||||
|
||||
<li><a href="../../personal-site/no-analytics/">Yandex Metrika отключена</a></li>
|
||||
|
||||
<li><a href="../../personal-site/binary-files-in-repo/">Бинарный файл в репозитории</a></li>
|
||||
<li><a href="../../personal-site/netlify-node-version/">Устаревший Node.js 16 в Netlify</a></li>
|
||||
|
||||
<li><a href="../../personal-site/no-node-modules/">Отсутствуют node_modules — нужна установка</a></li>
|
||||
|
||||
<li><a href="../../personal-site/netlify-node-version/">Устаревший Node.js 16 в Netlify</a></li>
|
||||
<li><a href="../../personal-site/binary-files-in-repo/">Бинарный файл в репозитории</a></li>
|
||||
|
||||
<li><a href="../../personal-site/no-analytics/">Yandex Metrika отключена</a></li>
|
||||
|
||||
<li><a href="../../personal-site/twitter-handle-mismatch/">Twitter handle не соответствует имени</a></li>
|
||||
|
||||
<li><a href="../../personal-site/outdated-doks-theme/">Hugo Doks 0.5 — сильно устарел</a></li>
|
||||
|
||||
<li><a href="../../personal-site/digest-stale/">Digest не обновлялся с февраля 2025</a></li>
|
||||
|
||||
<li><a href="../../personal-site/copyright-outdated/">Copyright без 2026 года</a></li>
|
||||
|
||||
<li><a href="../../personal-site/">Личный сайт (agaltsovav.ru)</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
@@ -94,6 +98,8 @@
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
@@ -115,11 +121,11 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../creds/eversolo-companion-postgres/">Eversolo Companion PostgreSQL</a></li>
|
||||
<li><a href="../../creds/leadera-db-local/">Mac Studio — leadera-api БД (локальная)</a></li>
|
||||
|
||||
<li><a href="../../creds/leadera-db-docker/">Mac Studio — leadera-api БД (docker-compose)</a></li>
|
||||
|
||||
<li><a href="../../creds/leadera-db-local/">Mac Studio — leadera-api БД (локальная)</a></li>
|
||||
<li><a href="../../creds/eversolo-companion-postgres/">Eversolo Companion PostgreSQL</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Public-Access — docs.a2v.space</title>
|
||||
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../css/custom.css">
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="../../">docs.a2v.space</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../diary/">Дневник</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../kb/">База знаний</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../plans/">Планы</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../leadera/">Leadera</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../guides/">Инструкции</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../creds/">Креды</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="../../personal-site/">Личный сайт</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<form class="d-flex search-form" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Поиск..." aria-label="Search" id="search-input">
|
||||
<button class="btn btn-outline-light" type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline-light d-md-none ms-2" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar" aria-expanded="false" aria-controls="sidebar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="search-results" class="search-results" style="display: none;">
|
||||
<div class="search-results-content">
|
||||
<button type="button" class="btn-close float-end" id="close-search"></button>
|
||||
<h5>Результаты поиска</h5>
|
||||
<ul id="results-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
||||
<aside class="col-md-3 col-lg-2 d-md-block sidebar collapse" id="sidebar">
|
||||
<div class="sidebar-content">
|
||||
|
||||
|
||||
|
||||
<h6>Разделы</h6>
|
||||
<ul class="nav flex-column">
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../diary/">Дневник</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../kb/">База знаний</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../plans/">Планы</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../leadera/">Leadera</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../guides/">Инструкции</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../creds/">Креды</a></li>
|
||||
|
||||
<li class="nav-item"><a class="nav-link" href="../../personal-site/">Личный сайт</a></li>
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
|
||||
<main class="col-md-9 col-lg-10 main-content">
|
||||
|
||||
<div class="article-content">
|
||||
<h1>Public-Access</h1>
|
||||
|
||||
|
||||
<ul>
|
||||
|
||||
<li><a href="../../leadera/leadera-gantt-public-access/">Leadera — Публичный доступ к диаграмме Ганта</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p class="text-center mb-0">© 2026 docs.a2v.space</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0/dist/fuse.min.js"></script>
|
||||
|
||||
|
||||
<script src="../../js/search.js"></script>
|
||||
<script src="../../js/creds.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user