diff --git a/app/api/admin_routes.py b/app/api/admin_routes.py
index 9f32125..afbe127 100644
--- a/app/api/admin_routes.py
+++ b/app/api/admin_routes.py
@@ -41,6 +41,7 @@
SourceOut,
TargetIn,
TargetOut,
+ TargetPatch,
TypeIn,
TypeOut,
)
@@ -276,6 +277,35 @@
return TargetOut(id=target.id, enabled=target.enabled, created_at=target.created_at, **payload.model_dump())
+@router.patch("/targets/{target_id}", response_model=TargetOut)
+def patch_target(
+ target_id: int,
+ payload: TargetPatch,
+ user: AuthenticatedUser = Depends(require_admin),
+ db: Session = Depends(get_db),
+) -> TargetOut:
+ target = db.get(ChannelTarget, target_id)
+ if target is None:
+ raise HTTPException(status_code=404, detail="Цель не найдена")
+ data = payload.model_dump(exclude_unset=True)
+ if "name" in data and (data["name"] != target.name):
+ dup = db.execute(
+ select(ChannelTarget.id).where(
+ ChannelTarget.channel == target.channel, ChannelTarget.name == data["name"]
+ )
+ ).scalar_one_or_none()
+ if dup:
+ raise _conflict(f"Цель {target.channel}/{data['name']} уже есть")
+ for field, value in data.items():
+ setattr(target, field, value)
+ db.commit()
+ db.refresh(target)
+ return TargetOut(
+ id=target.id, channel=target.channel, name=target.name, description=target.description,
+ config=target.config, enabled=target.enabled, created_at=target.created_at,
+ )
+
+
@router.delete("/targets/{target_id}", status_code=204)
def delete_target(
target_id: int,
diff --git a/app/api/admin_schemas.py b/app/api/admin_schemas.py
index 76e16f2..cb01c4c 100644
--- a/app/api/admin_schemas.py
+++ b/app/api/admin_schemas.py
@@ -76,6 +76,15 @@
created_at: datetime
+class TargetPatch(BaseModel):
+ """Частичное обновление цели; канал не меняется (ссылки из правил)."""
+
+ name: str | None = Field(default=None, min_length=1, max_length=120)
+ description: str | None = None
+ config: dict | None = None
+ enabled: bool | None = None
+
+
# --- rules ---
class RuleActionIn(BaseModel):
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 4daef8d..1b74ab5 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -175,4 +175,14 @@
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/frontend/src/api.js b/frontend/src/api.js
index f9b67d7..4dddea0 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -130,6 +130,7 @@
// Цели каналов
listTargets: () => apiFetch("/api/v1/admin/targets"),
createTarget: (data) => apiFetch("/api/v1/admin/targets", { method: "POST", body: JSON.stringify(data) }),
+ patchTarget: (id, data) => apiFetch(`/api/v1/admin/targets/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
deleteTarget: (id) => apiFetch(`/api/v1/admin/targets/${id}`, { method: "DELETE" }),
// Правила
diff --git a/frontend/src/ui.js b/frontend/src/ui.js
index 4657335..b3c6dde 100644
--- a/frontend/src/ui.js
+++ b/frontend/src/ui.js
@@ -70,10 +70,12 @@
/**
* Заголовок колонки GnTable с тултипом кита: GnTable рендерит column.label
- * как есть, поэтому передаём VNode с GnTooltip вокруг подписи.
+ * как есть, поэтому передаём VNode с GnTooltip. Класс th-tip переключает
+ * панель тултипа вниз — иначе её обрезает overflow у .table-wrapper
+ * (см. глобальный стиль в App.vue).
*/
export function th(label, text) {
- return h(GnTooltip, { text }, () => label);
+ return h(GnTooltip, { text, class: "th-tip" }, () => label);
}
export function truncate(text, n = 80) {
diff --git a/frontend/src/views/DeliveriesView.vue b/frontend/src/views/DeliveriesView.vue
index c13059e..09cee46 100644
--- a/frontend/src/views/DeliveriesView.vue
+++ b/frontend/src/views/DeliveriesView.vue
@@ -69,7 +69,9 @@
@@ -120,8 +122,10 @@
align-items: flex-end;
gap: var(--gn-space-4);
}
-.filters > :nth-child(3) {
- margin-bottom: var(--gn-space-3);
+/* вход селекта сидит на 5px выше низа form-group (margin-bottom подписи
+ в ките) — тем же отступом поднимаем кнопку, центры совпадают */
+.refresh-slot {
+ margin-bottom: var(--gn-space-5);
}
.event-id {
font-size: 0.78em;
diff --git a/frontend/src/views/RuleDetailView.vue b/frontend/src/views/RuleDetailView.vue
index 9279659..bc4eecd 100644
--- a/frontend/src/views/RuleDetailView.vue
+++ b/frontend/src/views/RuleDetailView.vue
@@ -296,10 +296,20 @@
border-radius: 6px;
background: var(--gn-surface-panel-muted);
display: flex;
- align-items: baseline;
+ flex-direction: column;
+ gap: var(--gn-space-2);
+}
+.action-head {
+ display: flex;
+ align-items: center;
gap: var(--gn-space-3);
flex-wrap: wrap;
}
+.action-body {
+ display: flex;
+ align-items: baseline;
+ gap: var(--gn-space-3);
+}
.action-num {
opacity: 0.6;
font-size: 0.85em;
diff --git a/frontend/src/views/RulesView.vue b/frontend/src/views/RulesView.vue
index d04075e..09ffbad 100644
--- a/frontend/src/views/RulesView.vue
+++ b/frontend/src/views/RulesView.vue
@@ -21,7 +21,7 @@
import { useToast } from "gnexus-ui-kit/vue";
import { useRouter } from "vue-router";
import { api } from "../api.js";
-import { parseList, shortJson, th, truncate } from "../ui.js";
+import { parseList, th, truncate } from "../ui.js";
const toast = useToast();
const router = useRouter();
@@ -187,17 +187,14 @@
:columns="[
{ key: 'name', label: 'Правило' },
{ key: 'weight', label: th('Вес', WEIGHT_TIP) },
- { key: 'conditions', label: th('Условия', 'Source, subjects, actions, priority_min и требования к payload — правило применяется, только если всё совпало') },
{ key: 'throttle_seconds', label: th('Шумодав', THROTTLE_TIP) },
- { key: 'actionsList', label: th('Действия', 'Куда доставить: канал и цель. internal_log без цели пишет журнал Synapse') },
+ { key: 'actionsList', label: th('Действия', 'Куда доставить: канал и цель. internal_log без цели пишет журнал Synapse. Условия — на странице правила') },
{ key: 'enabled', label: 'Вкл./выкл.' },
{ key: 'actions', label: '' },
]"
>
-
-
- {{ shortJson(row.conditions, 100) }}
-
+
+ {{ row.name }}
{{ row.throttle_seconds }} с
@@ -213,9 +210,6 @@
toggleRule(row, v)" />
-
- {{ row.name }}
-
@@ -333,17 +327,11 @@
.action-row > :nth-child(2) {
flex: 1;
}
-.cond {
- font-size: 0.78em;
- word-break: break-all;
-}
-.rule-link,
-.cond-link {
+.rule-link {
color: inherit;
text-decoration: none;
}
-.rule-link:hover,
-.cond-link:hover .cond {
+.rule-link:hover {
text-decoration: underline;
}
.chips {
diff --git a/frontend/src/views/SourcesView.vue b/frontend/src/views/SourcesView.vue
index 17886dc..218a623 100644
--- a/frontend/src/views/SourcesView.vue
+++ b/frontend/src/views/SourcesView.vue
@@ -14,13 +14,10 @@
GnTextarea,
} from "gnexus-ui-kit/vue";
import { useToast } from "gnexus-ui-kit/vue";
-import { useRouter } from "vue-router";
import { api } from "../api.js";
import { fmtTime, th } from "../ui.js";
const toast = useToast();
-const router = useRouter();
-
const sources = ref([]);
const loading = ref(true);
const error = ref("");
@@ -95,18 +92,21 @@
:rows="sources"
empty-text="Источников ещё нет — создайте первый"
:columns="[
- { key: 'name', label: th('Slug', 'Имя источника в событии (поле source конверта)') },
+ { key: 'name', label: th('Slug', 'Имя источника в событии (поле source конверта). Клик — страница источника') },
{ key: 'label', label: 'Название' },
{ key: 'created_at', label: 'Создан' },
{ key: 'actions', label: '' },
]"
>
+
+ {{ row.name }}
+
+
+ {{ row.label || "—" }}
+
{{ fmtTime(value) }}
-
-
-
-
+
@@ -153,9 +153,12 @@
flex-direction: column;
gap: var(--gn-space-4);
}
-.row-actions {
- display: flex;
- gap: var(--gn-space-2);
+.row-link {
+ color: inherit;
+ text-decoration: none;
+}
+.row-link:hover {
+ text-decoration: underline;
}
.form {
display: flex;
diff --git a/frontend/src/views/TargetDetailView.vue b/frontend/src/views/TargetDetailView.vue
index 0e4a47e..8e2ee62 100644
--- a/frontend/src/views/TargetDetailView.vue
+++ b/frontend/src/views/TargetDetailView.vue
@@ -1,4 +1,5 @@
-
+