hlassiege/darylpublic Fork
⑂ 2739efc
Commits
⬇ Clone ▾
git clone https://git.rickub.com/hlassiege/daryl.git
git clone ssh://git@rickub.com/hlassiege/daryl.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat(publish): add a default description shared by every video

A new Publishing tab in settings holds the block of links that every
video carries. It fills the description of long videos and shorts until
it is edited, and stays out of the channel profile given to Claude.
hlassiege committed 2026-10-13T16:24:55+02:00 Browse files
2739efc parent: 783b8eb
modified backend/studio/modules/projects/publish.py +10 -4
@@ -13,7 +13,8 @@ project.json, "publish" (modifié par l'interface) :
1313 }
1414
1515 Un champ absent prend sa valeur par défaut (titres et vignettes retenus, titre et accroche du
16-short…). L'envoi de chaque vidéo est un job, dans project["jobs"]["upload:<vidéo>"].
16+short, description par défaut des réglages…). L'envoi de chaque vidéo est un job, dans
17+project["jobs"]["upload:<vidéo>"].
1718
1819 L'API ne sait pas créer le test A/B de YouTube : la variante A est envoyée, B et C se posent
1920 dans YouTube Studio. Les shorts n'ont pas de vignette personnalisée par l'API.
@@ -26,7 +27,7 @@ from pathlib import Path
2627 from ...core import jobs, youtube_api
2728 from ...core.system import tool
2829 from ...core.tasks import TaskContext
29-from ..settings.service import language
30+from ..settings.service import default_description, language
3031 from . import montage, render, service
3132
3233 KIND = "project_upload"
@@ -72,12 +73,17 @@ def _long_defaults(project: dict) -> dict:
7273 titles = [t["t"] for t in project["titles"] if t.get("pick")]
7374 others = [t for t in picked if first is None or t["id"] != first["id"]][:MAX_VARIANTS - 1]
7475 return {"title": (first or {}).get("title") or (titles[0] if titles else project["title"]),
75- "thumb": first["id"] if first else None, "desc": "", "tags": [],
76+ "thumb": first["id"] if first else None, "desc": _with_footer(""), "tags": [],
7677 "ab_on": bool(others), "ab": [{"thumb": t["id"], "title": t.get("title") or ""} for t in others]}
7778
7879
80+def _with_footer(text: str) -> str:
81+ """Le texte, suivi de la description par défaut des réglages (le bloc commun à toutes les vidéos)."""
82+ return "\n\n".join(x for x in (text.strip(), default_description()) if x)
83+
84+
7985 def _short_defaults(short: dict) -> dict:
80- return {"title": short["title"], "desc": f"{short['hook']}\n\n#shorts".strip(), "tags": ["shorts"]}
86+ return {"title": short["title"], "desc": _with_footer(f"{short['hook']}\n\n#shorts"), "tags": ["shorts"]}
8187
8288
8389 def _check_title(title: str) -> bool:
@@ -13,7 +13,8 @@ project.json, "publish" (modifié par l'interface) :
13 }13 }
14 14
15 Un champ absent prend sa valeur par défaut (titres et vignettes retenus, titre et accroche du15 Un champ absent prend sa valeur par défaut (titres et vignettes retenus, titre et accroche du
16-short…). L'envoi de chaque vidéo est un job, dans project["jobs"]["upload:<vidéo>"].16+short, description par défaut des réglages…). L'envoi de chaque vidéo est un job, dans
17+project["jobs"]["upload:<vidéo>"].
17 18
18 L'API ne sait pas créer le test A/B de YouTube : la variante A est envoyée, B et C se posent19 L'API ne sait pas créer le test A/B de YouTube : la variante A est envoyée, B et C se posent
19 dans YouTube Studio. Les shorts n'ont pas de vignette personnalisée par l'API.20 dans YouTube Studio. Les shorts n'ont pas de vignette personnalisée par l'API.
@@ -26,7 +27,7 @@ from pathlib import Path
26 from ...core import jobs, youtube_api27 from ...core import jobs, youtube_api
27 from ...core.system import tool28 from ...core.system import tool
28 from ...core.tasks import TaskContext29 from ...core.tasks import TaskContext
29-from ..settings.service import language30+from ..settings.service import default_description, language
30 from . import montage, render, service31 from . import montage, render, service
31 32
32 KIND = "project_upload"33 KIND = "project_upload"
@@ -72,12 +73,17 @@ def _long_defaults(project: dict) -> dict:
72 titles = [t["t"] for t in project["titles"] if t.get("pick")]73 titles = [t["t"] for t in project["titles"] if t.get("pick")]
73 others = [t for t in picked if first is None or t["id"] != first["id"]][:MAX_VARIANTS - 1]74 others = [t for t in picked if first is None or t["id"] != first["id"]][:MAX_VARIANTS - 1]
74 return {"title": (first or {}).get("title") or (titles[0] if titles else project["title"]),75 return {"title": (first or {}).get("title") or (titles[0] if titles else project["title"]),
75- "thumb": first["id"] if first else None, "desc": "", "tags": [],76+ "thumb": first["id"] if first else None, "desc": _with_footer(""), "tags": [],
76 "ab_on": bool(others), "ab": [{"thumb": t["id"], "title": t.get("title") or ""} for t in others]}77 "ab_on": bool(others), "ab": [{"thumb": t["id"], "title": t.get("title") or ""} for t in others]}
77 78
78 79
80+def _with_footer(text: str) -> str:
81+ """Le texte, suivi de la description par défaut des réglages (le bloc commun à toutes les vidéos)."""
82+ return "\n\n".join(x for x in (text.strip(), default_description()) if x)
83+
84+
79 def _short_defaults(short: dict) -> dict:85 def _short_defaults(short: dict) -> dict:
80- return {"title": short["title"], "desc": f"{short['hook']}\n\n#shorts".strip(), "tags": ["shorts"]}86+ return {"title": short["title"], "desc": _with_footer(f"{short['hook']}\n\n#shorts"), "tags": ["shorts"]}
81 87
82 88
83 def _check_title(title: str) -> bool:89 def _check_title(title: str) -> bool:
modified backend/studio/modules/settings/api.py +12 -1
@@ -1,6 +1,7 @@
11 """
22 API des reglages : le profil de la chaine, la recherche de la chaine sur YouTube, la
3-connexion a ytwatch, l'analyse de la chaine par Claude et le moteur de transcription.
3+connexion a ytwatch, l'analyse de la chaine par Claude, la description par defaut des
4+videos et le moteur de transcription.
45 """
56 from fastapi import APIRouter, File, HTTPException, UploadFile
67 from pydantic import BaseModel
@@ -31,6 +32,16 @@ def search_channels(q: str):
3132 raise HTTPException(502, f"recherche YouTube impossible : {e}") from e
3233
3334
35+@router.get("/publish")
36+def get_publish():
37+ return service.publish_settings()
38+
39+
40+@router.put("/publish")
41+def put_publish(values: dict):
42+ return service.save_publish_settings(values)
43+
44+
3445 class Ytwatch(BaseModel):
3546 url: str = ""
3647 token: str = ""
@@ -1,6 +1,7 @@
1 """1 """
2 API des reglages : le profil de la chaine, la recherche de la chaine sur YouTube, la2 API des reglages : le profil de la chaine, la recherche de la chaine sur YouTube, la
3-connexion a ytwatch, l'analyse de la chaine par Claude et le moteur de transcription.3+connexion a ytwatch, l'analyse de la chaine par Claude, la description par defaut des
4+videos et le moteur de transcription.
4 """5 """
5 from fastapi import APIRouter, File, HTTPException, UploadFile6 from fastapi import APIRouter, File, HTTPException, UploadFile
6 from pydantic import BaseModel7 from pydantic import BaseModel
@@ -31,6 +32,16 @@ def search_channels(q: str):
31 raise HTTPException(502, f"recherche YouTube impossible : {e}") from e32 raise HTTPException(502, f"recherche YouTube impossible : {e}") from e
32 33
33 34
35+@router.get("/publish")
36+def get_publish():
37+ return service.publish_settings()
38+
39+
40+@router.put("/publish")
41+def put_publish(values: dict):
42+ return service.save_publish_settings(values)
43+
44+
34 class Ytwatch(BaseModel):45 class Ytwatch(BaseModel):
35 url: str = ""46 url: str = ""
36 token: str = ""47 token: str = ""
modified backend/studio/modules/settings/service.py +23 -0
@@ -245,6 +245,29 @@ def _write_transcription(data: dict) -> None:
245245 TRANSCRIPTION_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")
246246
247247
248+# --- Publication : le bloc commun a toutes les descriptions --------------------------
249+# Les liens, les outils, les reseaux : propose par defaut sous chaque description, a
250+# completer au-dessus (accroche) ou entre les deux (chapitres). Hors du profil : Claude n'en a pas besoin.
251+
252+PUBLISH_FILE = DATA_DIR / "publish.json"
253+
254+
255+def publish_settings() -> dict:
256+ data = json.loads(PUBLISH_FILE.read_text(encoding="utf-8")) if PUBLISH_FILE.is_file() else {}
257+ return {"description": str(data.get("description") or "")}
258+
259+
260+def save_publish_settings(values: dict) -> dict:
261+ data = {"description": str(values.get("description") or "").strip()}
262+ PUBLISH_FILE.parent.mkdir(parents=True, exist_ok=True)
263+ PUBLISH_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
264+ return data
265+
266+
267+def default_description() -> str:
268+ return publish_settings()["description"]
269+
270+
248271 # --- Analyse de la chaine -----------------------------------------------------------
249272 # Un job : Claude lit les stats (MCP ytwatch) et quelques transcriptions, et propose une
250273 # version de chaque champ. Rien n'est ecrit dans le profil : l'auteur accepte champ par champ.
@@ -245,6 +245,29 @@ def _write_transcription(data: dict) -> None:
245 TRANSCRIPTION_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")245 TRANSCRIPTION_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")
246 246
247 247
248+# --- Publication : le bloc commun a toutes les descriptions --------------------------
249+# Les liens, les outils, les reseaux : propose par defaut sous chaque description, a
250+# completer au-dessus (accroche) ou entre les deux (chapitres). Hors du profil : Claude n'en a pas besoin.
251+
252+PUBLISH_FILE = DATA_DIR / "publish.json"
253+
254+
255+def publish_settings() -> dict:
256+ data = json.loads(PUBLISH_FILE.read_text(encoding="utf-8")) if PUBLISH_FILE.is_file() else {}
257+ return {"description": str(data.get("description") or "")}
258+
259+
260+def save_publish_settings(values: dict) -> dict:
261+ data = {"description": str(values.get("description") or "").strip()}
262+ PUBLISH_FILE.parent.mkdir(parents=True, exist_ok=True)
263+ PUBLISH_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
264+ return data
265+
266+
267+def default_description() -> str:
268+ return publish_settings()["description"]
269+
270+
248 # --- Analyse de la chaine -----------------------------------------------------------271 # --- Analyse de la chaine -----------------------------------------------------------
249 # Un job : Claude lit les stats (MCP ytwatch) et quelques transcriptions, et propose une272 # Un job : Claude lit les stats (MCP ytwatch) et quelques transcriptions, et propose une
250 # version de chaque champ. Rien n'est ecrit dans le profil : l'auteur accepte champ par champ.273 # version de chaque champ. Rien n'est ecrit dans le profil : l'auteur accepte champ par champ.
modified web/src/i18n/locales/en/settings.ts +4 -1
@@ -1,6 +1,9 @@
11 export default {
2- tabs: { channel: "Channel", services: "Services", interface: "Interface" },
2+ tabs: { channel: "Channel", publish: "Publishing", services: "Services", interface: "Interface" },
33 unsaved: "Unsaved changes",
4+ defaultDescription: "Default description",
5+ defaultDescriptionHint: "The block shared by all your videos (links, tools, socials…): it fills the description of every new video, long or short. You write above it, and chapters are inserted right before it.",
6+ defaultDescriptionPlaceholder: "e.g.\nUseful resources\n--------------------\nMy blog: https://…\n\nWhere to find me?\nBluesky: https://…",
47 title: "Settings",
58 intro: "Your channel profile is given to Daryl at every writing step (angles, titles, script, assistant) as background: the brief, the kept angle and the skill’s method come first. All fields are optional.",
69 language: "Interface language",
@@ -1,6 +1,9 @@
1 export default {1 export default {
2- tabs: { channel: "Channel", services: "Services", interface: "Interface" },2+ tabs: { channel: "Channel", publish: "Publishing", services: "Services", interface: "Interface" },
3 unsaved: "Unsaved changes",3 unsaved: "Unsaved changes",
4+ defaultDescription: "Default description",
5+ defaultDescriptionHint: "The block shared by all your videos (links, tools, socials…): it fills the description of every new video, long or short. You write above it, and chapters are inserted right before it.",
6+ defaultDescriptionPlaceholder: "e.g.\nUseful resources\n--------------------\nMy blog: https://…\n\nWhere to find me?\nBluesky: https://…",
4 title: "Settings",7 title: "Settings",
5 intro: "Your channel profile is given to Daryl at every writing step (angles, titles, script, assistant) as background: the brief, the kept angle and the skill’s method come first. All fields are optional.",8 intro: "Your channel profile is given to Daryl at every writing step (angles, titles, script, assistant) as background: the brief, the kept angle and the skill’s method come first. All fields are optional.",
6 language: "Interface language",9 language: "Interface language",
modified web/src/i18n/locales/fr/settings.ts +4 -1
@@ -1,6 +1,9 @@
11 export default {
2- tabs: { channel: "Chaîne", services: "Services", interface: "Interface" },
2+ tabs: { channel: "Chaîne", publish: "Publication", services: "Services", interface: "Interface" },
33 unsaved: "Modifications non enregistrées",
4+ defaultDescription: "Description par défaut",
5+ defaultDescriptionHint: "Le bloc commun à toutes tes vidéos (liens, outils, réseaux…) : il remplit la description de chaque nouvelle vidéo, longue ou short. Tu écris au-dessus, et les chapitres s’insèrent juste avant lui.",
6+ defaultDescriptionPlaceholder: "Ex. :\nRessources utiles\n--------------------\nMon blog : https://…\n\nOù me trouver ?\nBluesky : https://…",
47 title: "Réglages",
58 intro: "Le profil de ta chaîne est donné à Daryl à chaque étape d’écriture (angles, titres, script, assistant), comme toile de fond : le brief, l’angle retenu et la méthode du skill passent avant. Tous les champs sont facultatifs.",
69 language: "Langue de l’interface",
@@ -1,6 +1,9 @@
1 export default {1 export default {
2- tabs: { channel: "Chaîne", services: "Services", interface: "Interface" },2+ tabs: { channel: "Chaîne", publish: "Publication", services: "Services", interface: "Interface" },
3 unsaved: "Modifications non enregistrées",3 unsaved: "Modifications non enregistrées",
4+ defaultDescription: "Description par défaut",
5+ defaultDescriptionHint: "Le bloc commun à toutes tes vidéos (liens, outils, réseaux…) : il remplit la description de chaque nouvelle vidéo, longue ou short. Tu écris au-dessus, et les chapitres s’insèrent juste avant lui.",
6+ defaultDescriptionPlaceholder: "Ex. :\nRessources utiles\n--------------------\nMon blog : https://…\n\nOù me trouver ?\nBluesky : https://…",
4 title: "Réglages",7 title: "Réglages",
5 intro: "Le profil de ta chaîne est donné à Daryl à chaque étape d’écriture (angles, titres, script, assistant), comme toile de fond : le brief, l’angle retenu et la méthode du skill passent avant. Tous les champs sont facultatifs.",8 intro: "Le profil de ta chaîne est donné à Daryl à chaque étape d’écriture (angles, titres, script, assistant), comme toile de fond : le brief, l’angle retenu et la méthode du skill passent avant. Tous les champs sont facultatifs.",
6 language: "Langue de l’interface",9 language: "Langue de l’interface",
modified web/src/pages/SettingsPage.vue +44 -2
@@ -31,7 +31,7 @@ const FIELDS: { key: Exclude<Key, "name">; rows?: number }[] = [
3131 const { t, locale } = useI18n();
3232
3333 /** L'onglet est dans l'URL (/settings/services…) : on peut y renvoyer depuis une autre page. */
34-const TABS = ["channel", "services", "interface"] as const;
34+const TABS = ["channel", "publish", "services", "interface"] as const;
3535 type Tab = (typeof TABS)[number];
3636 const route = useRoute();
3737 const router = useRouter();
@@ -130,6 +130,35 @@ function closeSoon() {
130130 setTimeout(() => (open.value = false), 150);
131131 }
132132
133+// --- Description par défaut : le bloc commun à toutes les descriptions (liens, outils, réseaux) ---
134+
135+const publish = reactive({ description: "" });
136+const publishSaved = ref<string | null>(null);
137+const publishBusy = ref(false);
138+const publishDirty = computed(() => publishSaved.value !== null && publish.description !== publishSaved.value);
139+
140+onMounted(async () => {
141+ try {
142+ Object.assign(publish, await api<typeof publish>("/api/settings/publish"));
143+ publishSaved.value = publish.description;
144+ } catch (e) {
145+ toast((e as Error).message, true);
146+ }
147+});
148+
149+async function savePublish() {
150+ publishBusy.value = true;
151+ try {
152+ Object.assign(publish, await putJson<typeof publish>("/api/settings/publish", publish));
153+ publishSaved.value = publish.description;
154+ toast(t("settings.saved"));
155+ } catch (e) {
156+ toast((e as Error).message, true);
157+ } finally {
158+ publishBusy.value = false;
159+ }
160+}
161+
133162 // --- Compte YouTube : publication par l'API (étape Upload) ---
134163
135164 type YoutubeAccount = {
@@ -285,7 +314,7 @@ function followers(n: number | null): string {
285314 <span class="h1">{{ t("settings.title") }}</span>
286315 <div class="tabs">
287316 <button v-for="k in TABS" :key="k" :class="{ on: k === tab }" @click="selectTab(k)">
288- {{ t(`settings.tabs.${k}`) }}<span v-if="k === 'channel' && dirty" class="dot" :title="t('settings.unsaved')" />
317+ {{ t(`settings.tabs.${k}`) }}<span v-if="k === 'channel' ? dirty : k === 'publish' && publishDirty" class="dot" :title="t('settings.unsaved')" />
289318 </button>
290319 </div>
291320
@@ -381,6 +410,19 @@ function followers(n: number | null): string {
381410 </template>
382411 </template>
383412
413+ <template v-else-if="tab === 'publish'">
414+ <div v-if="publishSaved !== null" class="field">
415+ <label>{{ t("settings.defaultDescription") }}</label>
416+ <span class="hint">{{ t("settings.defaultDescriptionHint") }}</span>
417+ <textarea v-model="publish.description" class="textarea" rows="14" :placeholder="t('settings.defaultDescriptionPlaceholder')" />
418+ </div>
419+ <div v-if="publishSaved !== null" class="submit">
420+ <button class="btn xl" :class="publishDirty ? 'red' : 'off'" :disabled="publishBusy || !publishDirty" @click="savePublish">
421+ {{ publishBusy ? t("common.saving") : publishDirty ? t("common.save") : t("settings.savedState") }}
422+ </button>
423+ </div>
424+ </template>
425+
384426 <template v-else-if="tab === 'services'">
385427 <div class="field">
386428 <label>{{ t("settings.account") }}</label>
@@ -31,7 +31,7 @@ const FIELDS: { key: Exclude<Key, "name">; rows?: number }[] = [
31 const { t, locale } = useI18n();31 const { t, locale } = useI18n();
32 32
33 /** L'onglet est dans l'URL (/settings/services…) : on peut y renvoyer depuis une autre page. */33 /** L'onglet est dans l'URL (/settings/services…) : on peut y renvoyer depuis une autre page. */
34-const TABS = ["channel", "services", "interface"] as const;34+const TABS = ["channel", "publish", "services", "interface"] as const;
35 type Tab = (typeof TABS)[number];35 type Tab = (typeof TABS)[number];
36 const route = useRoute();36 const route = useRoute();
37 const router = useRouter();37 const router = useRouter();
@@ -130,6 +130,35 @@ function closeSoon() {
130 setTimeout(() => (open.value = false), 150);130 setTimeout(() => (open.value = false), 150);
131 }131 }
132 132
133+// --- Description par défaut : le bloc commun à toutes les descriptions (liens, outils, réseaux) ---
134+
135+const publish = reactive({ description: "" });
136+const publishSaved = ref<string | null>(null);
137+const publishBusy = ref(false);
138+const publishDirty = computed(() => publishSaved.value !== null && publish.description !== publishSaved.value);
139+
140+onMounted(async () => {
141+ try {
142+ Object.assign(publish, await api<typeof publish>("/api/settings/publish"));
143+ publishSaved.value = publish.description;
144+ } catch (e) {
145+ toast((e as Error).message, true);
146+ }
147+});
148+
149+async function savePublish() {
150+ publishBusy.value = true;
151+ try {
152+ Object.assign(publish, await putJson<typeof publish>("/api/settings/publish", publish));
153+ publishSaved.value = publish.description;
154+ toast(t("settings.saved"));
155+ } catch (e) {
156+ toast((e as Error).message, true);
157+ } finally {
158+ publishBusy.value = false;
159+ }
160+}
161+
133 // --- Compte YouTube : publication par l'API (étape Upload) ---162 // --- Compte YouTube : publication par l'API (étape Upload) ---
134 163
135 type YoutubeAccount = {164 type YoutubeAccount = {
@@ -285,7 +314,7 @@ function followers(n: number | null): string {
285 <span class="h1">{{ t("settings.title") }}</span>314 <span class="h1">{{ t("settings.title") }}</span>
286 <div class="tabs">315 <div class="tabs">
287 <button v-for="k in TABS" :key="k" :class="{ on: k === tab }" @click="selectTab(k)">316 <button v-for="k in TABS" :key="k" :class="{ on: k === tab }" @click="selectTab(k)">
288- {{ t(`settings.tabs.${k}`) }}<span v-if="k === 'channel' && dirty" class="dot" :title="t('settings.unsaved')" />317+ {{ t(`settings.tabs.${k}`) }}<span v-if="k === 'channel' ? dirty : k === 'publish' && publishDirty" class="dot" :title="t('settings.unsaved')" />
289 </button>318 </button>
290 </div>319 </div>
291 320
@@ -381,6 +410,19 @@ function followers(n: number | null): string {
381 </template>410 </template>
382 </template>411 </template>
383 412
413+ <template v-else-if="tab === 'publish'">
414+ <div v-if="publishSaved !== null" class="field">
415+ <label>{{ t("settings.defaultDescription") }}</label>
416+ <span class="hint">{{ t("settings.defaultDescriptionHint") }}</span>
417+ <textarea v-model="publish.description" class="textarea" rows="14" :placeholder="t('settings.defaultDescriptionPlaceholder')" />
418+ </div>
419+ <div v-if="publishSaved !== null" class="submit">
420+ <button class="btn xl" :class="publishDirty ? 'red' : 'off'" :disabled="publishBusy || !publishDirty" @click="savePublish">
421+ {{ publishBusy ? t("common.saving") : publishDirty ? t("common.save") : t("settings.savedState") }}
422+ </button>
423+ </div>
424+ </template>
425+
384 <template v-else-if="tab === 'services'">426 <template v-else-if="tab === 'services'">
385 <div class="field">427 <div class="field">
386 <label>{{ t("settings.account") }}</label>428 <label>{{ t("settings.account") }}</label>
modified web/src/router.ts +1 -1
@@ -9,7 +9,7 @@ export const router = createRouter({
99 { path: "/", name: "home", component: () => import("./pages/HomePage.vue"), meta: { title: "nav.projects" } },
1010 { path: "/nouveau", name: "new", component: () => import("./pages/NewProjectPage.vue"), meta: { title: "nav.newProject" } },
1111 { path: "/bibliotheque", name: "library", component: () => import("./pages/LibraryPage.vue"), meta: { title: "nav.library" } },
12- { path: "/settings/:tab(channel|services|interface)?", name: "settings", component: () => import("./pages/SettingsPage.vue"), meta: { title: "nav.settings" } },
12+ { path: "/settings/:tab(channel|publish|services|interface)?", name: "settings", component: () => import("./pages/SettingsPage.vue"), meta: { title: "nav.settings" } },
1313 { path: "/reglages", redirect: "/settings" },
1414 { path: "/p/:pid", name: "project", component: () => import("./project/OverviewPage.vue") },
1515 { path: "/p/:pid/brief", name: "brief", component: () => import("./pages/NewProjectPage.vue"), meta: { title: "nav.brief" } },
@@ -9,7 +9,7 @@ export const router = createRouter({
9 { path: "/", name: "home", component: () => import("./pages/HomePage.vue"), meta: { title: "nav.projects" } },9 { path: "/", name: "home", component: () => import("./pages/HomePage.vue"), meta: { title: "nav.projects" } },
10 { path: "/nouveau", name: "new", component: () => import("./pages/NewProjectPage.vue"), meta: { title: "nav.newProject" } },10 { path: "/nouveau", name: "new", component: () => import("./pages/NewProjectPage.vue"), meta: { title: "nav.newProject" } },
11 { path: "/bibliotheque", name: "library", component: () => import("./pages/LibraryPage.vue"), meta: { title: "nav.library" } },11 { path: "/bibliotheque", name: "library", component: () => import("./pages/LibraryPage.vue"), meta: { title: "nav.library" } },
12- { path: "/settings/:tab(channel|services|interface)?", name: "settings", component: () => import("./pages/SettingsPage.vue"), meta: { title: "nav.settings" } },12+ { path: "/settings/:tab(channel|publish|services|interface)?", name: "settings", component: () => import("./pages/SettingsPage.vue"), meta: { title: "nav.settings" } },
13 { path: "/reglages", redirect: "/settings" },13 { path: "/reglages", redirect: "/settings" },
14 { path: "/p/:pid", name: "project", component: () => import("./project/OverviewPage.vue") },14 { path: "/p/:pid", name: "project", component: () => import("./project/OverviewPage.vue") },
15 { path: "/p/:pid/brief", name: "brief", component: () => import("./pages/NewProjectPage.vue"), meta: { title: "nav.brief" } },15 { path: "/p/:pid/brief", name: "brief", component: () => import("./pages/NewProjectPage.vue"), meta: { title: "nav.brief" } },