addashboard-cli 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/submission.js +41 -3
- package/dist/commands/submission.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/lib/apiClient.js +4 -0
- package/dist/lib/apiClient.js.map +1 -1
- package/package.json +2 -2
- package/skills/ad-ops/SKILL.md +102 -18
- package/skills/ad-ops/ad-planner.html +175 -86
- package/skills/ad-ops/scripts/adops.sh +106 -0
- package/skills/ad-ops/scripts/plan_lint.py +297 -0
- package/skills/ad-ops//343/202/242/343/202/257/343/203/206/343/202/243/343/203/226/343/203/227/343/203/251/343/203/263.json +6 -0
|
@@ -297,13 +297,9 @@
|
|
|
297
297
|
<body>
|
|
298
298
|
<header class="bar">
|
|
299
299
|
<div class="brand"><span class="logo" id="brandLogo">YDA入稿プランナー</span><span class="pjname" id="pjLabel">—</span></div>
|
|
300
|
-
<!--
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
<button data-pf="meta">Meta</button>
|
|
304
|
-
</div>
|
|
305
|
-
<!-- F-3: アカウント切替はヘッダーのプルダウンに一本化 -->
|
|
306
|
-
<span class="acchdr" title="アカウント切替(+新規追加/フォルダ接続/編集もここから)"><select class="chipin" id="accSelect" style="min-width:12ch"></select></span>
|
|
300
|
+
<!-- S11: 「案件 × 媒体」を1つで選ぶ統合プルダウン。option value = "<platform>::<アカウント名>"。
|
|
301
|
+
旧 #pfSeg(媒体セレクタ)/ #mediaTabs(媒体要件タブ)は廃止し、媒体切替もここに一本化した (2026-08-05) -->
|
|
302
|
+
<span class="acchdr" title="案件×媒体を切り替える(+新規追加/編集もここから)"><select class="chipin" id="accSelect" style="min-width:18ch"></select></span>
|
|
307
303
|
<div class="seg" id="viewSeg">
|
|
308
304
|
<button data-v="req">媒体要件</button>
|
|
309
305
|
<button data-v="sheet" class="active">プラン</button>
|
|
@@ -324,11 +320,8 @@
|
|
|
324
320
|
<!-- ============ 媒体要件ビュー ============ -->
|
|
325
321
|
<section class="view" id="view-req">
|
|
326
322
|
<div class="doc">
|
|
327
|
-
<!--
|
|
328
|
-
|
|
329
|
-
<button data-mt="yda" data-pf="yda" class="active">YDA</button>
|
|
330
|
-
<button data-mt="meta" data-pf="meta">Meta広告 <span class="pill amber" style="margin-left:4px">検証中</span></button>
|
|
331
|
-
</div>
|
|
323
|
+
<!-- S11: 旧・媒体要件タブ (#mediaTabs) は廃止。媒体はヘッダーの統合プルダウンで切り替え、
|
|
324
|
+
このビューは選択中の媒体のドキュメントだけを表示する (showMediaDoc(curPf)) -->
|
|
332
325
|
|
|
333
326
|
<!-- ======== YDA タブ ======== -->
|
|
334
327
|
<div id="mt-yda">
|
|
@@ -462,7 +455,7 @@ finalUrl = baseURL + 記事slug + "?ad=" + paramCode</div>
|
|
|
462
455
|
<details class="sec" open><summary><span class="num">1</span><span class="stitle">画面の入力項目(どこで何を設定するか)</span><span class="chev"></span></summary>
|
|
463
456
|
<div class="body">
|
|
464
457
|
<table class="g"><thead><tr><th>場所</th><th>入力項目</th><th>入稿JSONへの反映</th></tr></thead><tbody>
|
|
465
|
-
<tr><td><b>案件設定チップ</b></td><td>案件名 / <b>アカウントID</b> / <b>目的(goal)</b> / <b
|
|
458
|
+
<tr><td><b>案件設定チップ</b></td><td>案件名 / <b>アカウントID</b> / <b>目的(goal)</b> / <b>配信開始日</b> / CTA / 配信 / タイプ / 入札既定(tCPA・目標CPA) / 計測 / 主体者 / 表示URL / 記事URL / 予算(新規・ASC)</td><td>accountId・goal・startDate・入札既定・予算既定・finalUrlベース</td></tr>
|
|
466
459
|
<tr><td><b>キャンペーン行</b></td><td>キャンペーン名 / 入稿モード / 1日の予算 / 入札戦略 / 目標CV単価(tCPA) / 配信設定 / 検証変数 / 入稿リンク番号 / 戦略メモ</td><td>name・budget・biddingStrategy+targetCpa・status</td></tr>
|
|
467
460
|
<tr><td><b>広告グループ行</b></td><td>広告グループ名 / paramCode / 入札価格 / 配信設定 / <b>ターゲティング列</b>(デバイス・性別・年齢・地域・曜日時間帯・オーディエンス・サーチKW)</td><td>name・status・devices/ages/genders/days(+startHour/endHour)。地域・オーディエンス・サーチKWは未反映</td></tr>
|
|
468
461
|
<tr><td><b>広告行</b></td><td>フォーマット(Card/SQ) / 画像番号 / タイトル番号 / 説明文番号 / 記事slug / 手入稿フラグ</td><td>ads[].adName・mediaIds・headlines・descriptions・finalUrl(手入稿・未解決は除外)</td></tr>
|
|
@@ -481,7 +474,7 @@ finalUrl = baseURL + 記事slug + "?ad=" + paramCode</div>
|
|
|
481
474
|
<table class="g"><thead><tr><th>項目</th><th>画面上の出どころ</th><th>変換・制約</th></tr></thead><tbody>
|
|
482
475
|
<tr><td class="mono">accountId</td><td><b>案件設定「アカウントID」</b></td><td>必須。未入力だと書き出し中止</td></tr>
|
|
483
476
|
<tr><td class="mono">goal</td><td><b>案件設定「目的」</b></td><td><code>CONVERSION</code> / <code>CLICK</code> / <code>BRAND_AWARENESS</code> / <code>VIDEO_VIEW</code> / <code>VIEWABLE_IMPRESSION</code></td></tr>
|
|
484
|
-
<tr><td class="mono">startDate</td><td><b
|
|
477
|
+
<tr><td class="mono">startDate</td><td><b>案件設定「配信開始日」</b></td><td>YYYY-MM-DD。空欄=書き出し日</td></tr>
|
|
485
478
|
<tr><td class="mono">status</td><td>キャンペーン行/広告グループ行の「配信設定」</td><td><code>オン→ACTIVE</code> / <code>オフ→PAUSED</code>(既定オフ=誤配信防止)</td></tr>
|
|
486
479
|
<tr><td class="mono">budget</td><td>キャンペーン行「1日の予算」</td><td>未入力時はモード別既定(新規12,000/ASC50,000)</td></tr>
|
|
487
480
|
<tr><td class="mono">biddingStrategy</td><td>キャンペーン行「入札戦略」+ 目標CV単価</td><td><code>tCPA → MAXIMIZE_CONVERSIONS + targetCpa</code>(tCPA値はキャンペーン行→案件既定8,500の順)/ <code>最大配信 → MAXIMIZE_CONVERSIONS</code>(目標なし <span class="pill amber">要実機確認</span>)</td></tr>
|
|
@@ -517,7 +510,7 @@ finalUrl = baseURL + 記事slug + "?ad=" + paramCode</div>
|
|
|
517
510
|
<li><b>バージョン保存</b>:ヘッダーの「バージョン保存」。保存のたびに<b>常に新バージョン</b>(v1, v2…)として名称・メモ・作成者付きで保存される(上書きなし)。<b>履歴本体はブラウザ内に自動保存</b>されるため、フォルダ設定なしでも一覧・再読込できる。</li>
|
|
518
511
|
<li><b>保存フォルダ接続(初回に1回だけ)</b>:履歴ビューの「📁 保存フォルダに接続」でスキル同梱の <code>入稿計画/</code> フォルダを一度選ぶと、<b>次回からは開くだけで自動接続</b>され、保存のたびにファイルにも書き出される。未接続時はダウンロード動作。</li>
|
|
519
512
|
<li><b>履歴の確認・再利用</b>:履歴ビューで一覧(Ver/日時/媒体/名称)を確認し、「読込」で過去バージョンを復元、「編集」で名称・メモ変更、「削除」で破棄。</li>
|
|
520
|
-
<li><b>入稿JSON書き出し</b>:ヘッダーの「入稿JSON書き出し」をワンクリック。アカウントID
|
|
513
|
+
<li><b>入稿JSON書き出し</b>:ヘッダーの「入稿JSON書き出し」をワンクリック。アカウントID・目的・配信開始日は案件設定チップ、ターゲティングは広告グループ行の列から自動変換される(<code>*_入稿計画_*.json</code>)。</li>
|
|
521
514
|
<li><b>入稿(dry-run → 本実行)</b>:<code>webgram submit-plan --plan-file <入稿計画></code>。dry-run が全キャンペーンを一括検証 → OKなら <code>--no-dry-run --yes</code> で本実行(1キャンペーンずつ入稿・進捗書き戻しで中断後も再開可能)。実行後は管理画面の一覧でOK確認。</li>
|
|
522
515
|
</ol>
|
|
523
516
|
</div></details>
|
|
@@ -714,6 +707,8 @@ const KEY_NS = 'ad_planner_';
|
|
|
714
707
|
const LEGACY_KEY_NS = 'yda_planner_';
|
|
715
708
|
const AUTHOR_KEY = KEY_NS + 'author';
|
|
716
709
|
const MIGRATED_KEY = KEY_NS + 'migrated_v1';
|
|
710
|
+
/* ad-ops スキルが書き出す「いま作業中のプラン」の指定ファイル (媒体 + 案件名)。起動時の既定選択に使う */
|
|
711
|
+
const ACTIVE_PLAN_FILE = 'アクティブプラン.json';
|
|
717
712
|
function stateKey(){ return KEY_NS + 'state_v1:' + curPf; }
|
|
718
713
|
function histKey(){ return KEY_NS + 'history_v1:' + curPf; }
|
|
719
714
|
function bodyKey(vid){ return KEY_NS + 'histbody_' + curPf + ':' + vid; }
|
|
@@ -770,18 +765,11 @@ let BUNDLED_ACCOUNT_NAME = '';
|
|
|
770
765
|
var PLATFORMS = {};
|
|
771
766
|
var curPf = 'yda';
|
|
772
767
|
function pf(){ return PLATFORMS[curPf]; }
|
|
773
|
-
/* ----
|
|
774
|
-
|
|
768
|
+
/* ---- S11: 媒体切替はヘッダーの統合プルダウン (#accSelect) に一本化 ----
|
|
769
|
+
媒体要件ビューは選択中の媒体のドキュメントだけを表示する。
|
|
775
770
|
媒体を切り替えると state は当該媒体の既定値で作り直す (プランは媒体をまたげない)。 */
|
|
776
771
|
function showMediaDoc(id){ ['yda','meta'].forEach(k=>{ const el=document.getElementById('mt-'+k); if(el) el.style.display = (k===id?'':'none'); }); }
|
|
777
772
|
function renderPlatformTabs(){
|
|
778
|
-
document.querySelectorAll('#pfSeg button[data-pf], #mediaTabs button[data-pf]').forEach(b=>{
|
|
779
|
-
const id=b.dataset.pf, ok=!!PLATFORMS[id];
|
|
780
|
-
b.classList.toggle('pfoff', !ok); /* 未登録媒体 = 選択不可の見え方 (要件ドキュメントは開ける) */
|
|
781
|
-
b.dataset.pfstate = ok?'ready':'disabled';
|
|
782
|
-
b.title = ok ? (PLATFORMS[id].displayName+' に切り替える') : '準備中(媒体スキーマ未登録): 媒体要件の閲覧のみ';
|
|
783
|
-
b.classList.toggle('active', id===curPf);
|
|
784
|
-
});
|
|
785
773
|
showMediaDoc(curPf);
|
|
786
774
|
renderChrome();
|
|
787
775
|
}
|
|
@@ -817,6 +805,15 @@ window.switchPlatform=function(id){
|
|
|
817
805
|
if(!PLATFORMS[id]){ toast('この媒体はまだ選択できません(媒体スキーマ未登録)'); return false; }
|
|
818
806
|
if(id===curPf){ renderPlatformTabs(); return true; }
|
|
819
807
|
curPf=id;
|
|
808
|
+
/* 接続中のアカウントフォルダは媒体別 (idbKeyAccountsDir() が 'accountsDir:'+curPf)。
|
|
809
|
+
切替時に前媒体のハンドルを掴んだままにすると、前媒体のファイル一覧を新媒体の
|
|
810
|
+
アカウント候補として出し、autoSaveAccountJson が同名ファイル (例: 双方にある
|
|
811
|
+
sample_account) を **別媒体の内容で上書き** してしまう。必ずクリアする。
|
|
812
|
+
新媒体のハンドル復元はここでは行わない (restoreAccountFolder() を切替のたびに
|
|
813
|
+
呼ぶと IndexedDB アクセスで描画が詰まり E2E が軒並みタイムアウトした)。
|
|
814
|
+
切替後にフォルダを使うときは、ユーザーが明示的に再接続する。
|
|
815
|
+
リロード時は boot 経路の restoreAccountFolder() が curPf のぶんを復元する。 */
|
|
816
|
+
accDirHandle=null; pendingAccDirHandle=null; accountFiles=[];
|
|
820
817
|
applyBundle(id); /* S10: 同梱アカウント / サンプルプランは媒体別 */
|
|
821
818
|
refTab=firstRefTabKey(); /* 参照データタブは媒体の宣言順で先頭に戻す */
|
|
822
819
|
st=initialState();
|
|
@@ -975,7 +972,7 @@ PLATFORMS.yda = {
|
|
|
975
972
|
{key:'goal', label:'目的', kind:'selectv', fallback:'CONVERSION',
|
|
976
973
|
options:[{v:'CONVERSION',t:'コンバージョン'},{v:'CLICK',t:'クリック'},{v:'BRAND_AWARENESS',t:'ブランド認知'},
|
|
977
974
|
{v:'VIDEO_VIEW',t:'動画再生'},{v:'VIEWABLE_IMPRESSION',t:'vインプ'}]},
|
|
978
|
-
{key:'startDate', label:'
|
|
975
|
+
{key:'startDate', label:'配信開始日', kind:'text', width:'10ch', fallback:''},
|
|
979
976
|
{key:'cta', label:'CTA', kind:'text', width:'7ch'},
|
|
980
977
|
{key:'deliveryStatus', label:'配信', kind:'select', options:['オフ','オン']},
|
|
981
978
|
{key:'attributionDays', label:'計測', kind:'number', width:'3ch', suffix:'日'},
|
|
@@ -1124,8 +1121,12 @@ PLATFORMS.yda = {
|
|
|
1124
1121
|
const imgNoStr=String(ad.imageNo||'').trim();
|
|
1125
1122
|
const pendingImage = (imgNoStr!=='' && mediaId===undefined); // アカウントのメディアライブラリに未登録→先にアップロードして新規作成
|
|
1126
1123
|
if(imgNoStr==='') pushErr('画像未解決', EC.IMAGE_UNRESOLVED);
|
|
1127
|
-
|
|
1128
|
-
|
|
1124
|
+
// 見出し/説明文は「番号空欄=意図的に使わない」を許容 (エラーにしない)。
|
|
1125
|
+
// 番号が入っているのに参照表で引けない場合のみ従来どおり未解決エラー。
|
|
1126
|
+
const titleNoStr=String(ad.titleNo||'').trim();
|
|
1127
|
+
const descNoStr=String(ad.descNo||'').trim();
|
|
1128
|
+
if(titleNoStr!=='' && titleText===undefined) pushErr('タイトル未解決', EC.TITLE_UNRESOLVED);
|
|
1129
|
+
if(descNoStr!=='' && descText===undefined) pushErr('説明文未解決', EC.DESC_UNRESOLVED);
|
|
1129
1130
|
if(titleText!==undefined && glen(titleText)>35) pushErr('タイトル35字超', EC.TITLE_TOO_LONG);
|
|
1130
1131
|
else if(titleText!==undefined && glen(titleText)>20) pushWarn('タイトル20字超', EC.TITLE_LONG_WARN);
|
|
1131
1132
|
// B11: paramCode空はエラー(export対象外・URL生成に必須)。F-6: refs.groups 払出判定は廃止。
|
|
@@ -1183,8 +1184,9 @@ PLATFORMS.yda = {
|
|
|
1183
1184
|
adName:r.adName,
|
|
1184
1185
|
status:P.mappers.status(g.deliveryStatus||p.deliveryStatus, P),
|
|
1185
1186
|
mediaType:'IMAGE', // レスポンシブ画像広告 (backend ydaAdSchema IMAGE)
|
|
1186
|
-
|
|
1187
|
-
|
|
1187
|
+
// 空欄許容: 空文字を要素に持つ配列を送らない (空なら [])
|
|
1188
|
+
headlines:String(r.titleText||'')===''?[]:[String(r.titleText)],
|
|
1189
|
+
descriptions:String(r.descText||'')===''?[]:[String(r.descText)],
|
|
1188
1190
|
principal:p.principal||'', // E-1: 主体者はアカウントJSON由来 (st.project.principal)
|
|
1189
1191
|
buttonText:P.mappers.cta(p.cta, P), // CTA(案件設定)→ YDA buttonText enum。入稿ADD必須
|
|
1190
1192
|
finalUrl:r.finalUrl // 実データ準拠 (?ad=paramCode&creative=...)
|
|
@@ -1256,7 +1258,7 @@ PLATFORMS.yda = {
|
|
|
1256
1258
|
strategy(){
|
|
1257
1259
|
return ['YDA(Yahoo!ディスプレイ広告)の入稿プランを一緒に作りたいです。まず戦略を壁打ちさせてください。','',
|
|
1258
1260
|
'# 前提','- アカウント: <アカウント名(日本語OK)>','- 商材 / LP: <商材名・URL>',
|
|
1259
|
-
'- 目的: <コンバージョン獲得 / 認知 など>','- 予算・期間: <月予算 /
|
|
1261
|
+
'- 目的: <コンバージョン獲得 / 認知 など>','- 予算・期間: <月予算 / 配信開始日・配信終了日>',
|
|
1260
1262
|
'- 訴求したいポイント: <強み・オファー・ターゲット層>','',
|
|
1261
1263
|
'# お願い','1. 上記をもとにターゲティング方針・キャンペーン/広告グループ構成・訴求軸を提案してください。',
|
|
1262
1264
|
'2. 私が「OK」と言ったら、その戦略を ad-ops Skill の「サンプルプラン.json」に書き出して(上書きして)ください。',
|
|
@@ -1420,6 +1422,7 @@ function snapshot(){ undoStack.push(JSON.stringify({project:st.project,campaigns
|
|
|
1420
1422
|
let _dirty=false; // 未保存の編集があるか (離脱時警告用)
|
|
1421
1423
|
function markDirty(){ _dirty=true; }
|
|
1422
1424
|
function markSaved(){ _dirty=false; }
|
|
1425
|
+
window.isDirty=function(){ return _dirty; }; // 未保存フラグの読み取り口 (E2E から参照。_dirty は let のため window に載らない)
|
|
1423
1426
|
function mutate(fn){ snapshot(); fn(); _dirty=true; persist(); render(); }
|
|
1424
1427
|
function undo(){ const s=undoStack.pop(); if(!s){toast('これ以上戻せません'); return;} const j=JSON.parse(s); st.project=j.project; st.campaigns=j.campaigns; if(j.refs) st.refs=j.refs; _dirty=true; persist(); if(typeof buildDatalists==='function') buildDatalists(); render(); toast('元に戻しました'); }
|
|
1425
1428
|
|
|
@@ -1608,7 +1611,7 @@ function renderCell(col, item, ri, ci, ctx){
|
|
|
1608
1611
|
/* text / mono / number */
|
|
1609
1612
|
const cls='cell-in'+(col.kind==='mono'?' mono':'')+(col.kind==='number'?' num':'')+((col.required&&val==='')?' warnempty':'');
|
|
1610
1613
|
return '<td><input class="'+cls+'"'+(col.kind==='number'?' type="number"':'')+' '+sel+' value="'+esc(val)+'"'+
|
|
1611
|
-
(col.placeholder?(' placeholder="'+esc(col.placeholder())+'"'):'')+
|
|
1614
|
+
(col.placeholder?(' placeholder="'+esc(col.placeholder(ctx, row))+'"'):'')+
|
|
1612
1615
|
(col.title?(' title="'+esc(col.title)+'"'):'')+sty+'></td>';
|
|
1613
1616
|
}
|
|
1614
1617
|
|
|
@@ -1981,7 +1984,14 @@ const META_COLS = {
|
|
|
1981
1984
|
{field:'ageMin', label:'年齢下限', kind:'number', width:'56px', coerce:v=>clampInt(v,13,65),
|
|
1982
1985
|
readonlyOnExisting:true, roEmptyDash:true},
|
|
1983
1986
|
{field:'ageMax', label:'年齢上限', kind:'number', width:'56px', coerce:v=>clampInt(v,13,65),
|
|
1984
|
-
readonlyOnExisting:true, roEmptyDash:true
|
|
1987
|
+
readonlyOnExisting:true, roEmptyDash:true,
|
|
1988
|
+
/* 空欄 = 上限なし (age_max を送らない)。特別広告カテゴリ時は 18-65 固定なので上限なしは無視される */
|
|
1989
|
+
placeholder:ctx=>{
|
|
1990
|
+
const K=PLATFORMS.meta.enums.specialAdCategoryConstraints;
|
|
1991
|
+
const c=ctx&&ctx.item&&ctx.item.c, cat=String((c&&c.specialAdCategory)||'NONE');
|
|
1992
|
+
return K.appliesTo.indexOf(cat)>=0 ? (String(K.forcedAgeMax)+'(固定)') : '上限なし';
|
|
1993
|
+
},
|
|
1994
|
+
title:'空欄にすると上限なし(age_max を送りません)。特別広告カテゴリ選択時は 65 固定となり上限なしは無視されます'},
|
|
1985
1995
|
{field:'tg_gender', label:'性別', kind:'tgpick', tgKey:'tg_gender', readonlyOnExisting:true, roEmptyDash:true},
|
|
1986
1996
|
{field:'tg_device', label:'デバイス', kind:'tgpick', tgKey:'tg_device', readonlyOnExisting:true, roEmptyDash:true},
|
|
1987
1997
|
{field:'tg_placement', label:'配置', kind:'tgpick', tgKey:'tg_placement', readonlyOnExisting:true, roEmptyDash:true},
|
|
@@ -2071,7 +2081,8 @@ PLATFORMS.meta = {
|
|
|
2071
2081
|
{key:'cta', label:'CTA', kind:'select',
|
|
2072
2082
|
options:['詳しくはこちら','購入する','登録する','ダウンロード','お問い合わせ','今すぐ申し込む',
|
|
2073
2083
|
'予約する','見積もりを取得','購読する','動画を見る','ボタンなし']},
|
|
2074
|
-
{key:'startDate', label:'
|
|
2084
|
+
{key:'startDate', label:'配信開始日', kind:'text', width:'10ch', fallback:''},
|
|
2085
|
+
{key:'endDate', label:'配信終了日', kind:'text', width:'10ch', fallback:''},
|
|
2075
2086
|
{key:'deliveryStatus', label:'配信', kind:'select', options:['オフ','オン']},
|
|
2076
2087
|
{key:'baseArticleUrl', label:'記事URL', kind:'text', width:'16ch', fallback:''}
|
|
2077
2088
|
],
|
|
@@ -2119,7 +2130,10 @@ PLATFORMS.meta = {
|
|
|
2119
2130
|
status(s, P){ return s==='オン'?P.enums.statusOn:P.enums.statusOff; },
|
|
2120
2131
|
cta(label, P){ return P.enums.cta[String(label||'').trim()] || P.enums.ctaFallback; },
|
|
2121
2132
|
/* B13: JST 00:00 を明示した ISO8601 (日付のみだと UTC 解釈で前日にずれる) */
|
|
2122
|
-
startTime(dateStr){ return (dateStr||today())+'T00:00:00+0900'; }
|
|
2133
|
+
startTime(dateStr){ return (dateStr||today())+'T00:00:00+0900'; },
|
|
2134
|
+
/* 終了日は「その日の終わりまで配信」の解釈。日付のみの入力を JST 23:59:59 に寄せる。
|
|
2135
|
+
空欄なら null を返し、呼び出し側で endTime キー自体を出さない (無期限配信)。 */
|
|
2136
|
+
endTime(dateStr){ const s=String(dateStr||'').trim(); return s? s+'T23:59:59+0900' : null; }
|
|
2123
2137
|
},
|
|
2124
2138
|
|
|
2125
2139
|
defaults:{
|
|
@@ -2128,7 +2142,7 @@ PLATFORMS.meta = {
|
|
|
2128
2142
|
cta:'詳しくはこちら', pageId:'', instagramUserId:'', pixelId:'', customEventType:'PURCHASE',
|
|
2129
2143
|
optimizationGoal:'OFFSITE_CONVERSIONS', billingEvent:'IMPRESSIONS',
|
|
2130
2144
|
bidStrategy:'LOWEST_COST_WITHOUT_CAP', buyingType:'AUCTION',
|
|
2131
|
-
deliveryStatus:'オフ', startDate:''
|
|
2145
|
+
deliveryStatus:'オフ', startDate:'', endDate:''
|
|
2132
2146
|
},
|
|
2133
2147
|
ad:{format:'IMAGE', imageNo:'', titleNo:'', descNo:'', articleSlug:''},
|
|
2134
2148
|
adGroup:{name:'', paramCode:'', isExisting:false, dailyBudget:1000, bidAmount:null,
|
|
@@ -2203,8 +2217,9 @@ PLATFORMS.meta = {
|
|
|
2203
2217
|
const pendingImage = (noStr!=='' && (mediaId===undefined || mediaId===''));
|
|
2204
2218
|
const T=P.refTables.titles.lengthLimit, D=P.refTables.descriptions.lengthLimit;
|
|
2205
2219
|
if(noStr==='') pushErr('クリエイティブ未解決', EC.IMAGE_UNRESOLVED);
|
|
2206
|
-
|
|
2207
|
-
if(String(ad.
|
|
2220
|
+
/* YDA と同仕様: 番号未入力は許容 (見出し・本文なしの広告)。番号ありで参照表に無い場合のみ未解決エラー */
|
|
2221
|
+
if(String(ad.titleNo||'').trim()!=='' && titleText===undefined) pushErr('見出し未解決', EC.TITLE_UNRESOLVED);
|
|
2222
|
+
if(String(ad.descNo||'').trim()!=='' && descText===undefined) pushErr('本文未解決', EC.DESC_UNRESOLVED);
|
|
2208
2223
|
if(titleText!==undefined && glen(titleText)>T.err) pushErr('見出し'+T.err+'字超', EC.TITLE_TOO_LONG);
|
|
2209
2224
|
else if(titleText!==undefined && glen(titleText)>T.warn) pushWarn('見出し'+T.warn+'字超', EC.TITLE_LONG_WARN);
|
|
2210
2225
|
if(descText!==undefined && glen(descText)>D.err) pushErr('本文'+D.err+'字超', EC.DESC_TOO_LONG);
|
|
@@ -2268,7 +2283,12 @@ PLATFORMS.meta = {
|
|
|
2268
2283
|
startTime:P.mappers.startTime(p.startDate), /* JST 明示 ISO8601 */
|
|
2269
2284
|
/* 特別カテゴリ時は 18-65 固定・性別指定不可 (公式制約) */
|
|
2270
2285
|
ageMin: constrained?K.forcedAgeMin:(Number(g.ageMin)||18),
|
|
2271
|
-
ageMax
|
|
2286
|
+
/* 年齢上限は空欄 = 上限なし → ageMax キー自体を出さない (age_max 省略 = Meta 側で上限なし)。
|
|
2287
|
+
ただし特別広告カテゴリ時は forcedAgeMax 固定が最優先 (ポリシー制約なので上限なしは無視する)。 */
|
|
2288
|
+
...(constrained
|
|
2289
|
+
? {ageMax:K.forcedAgeMax}
|
|
2290
|
+
: (String(g.ageMax==null?'':g.ageMax).trim()!=='' && Number(g.ageMax)>0
|
|
2291
|
+
? {ageMax:Number(g.ageMax)} : {})),
|
|
2272
2292
|
genders: constrained?[]:P.mappers.genders(g.tg_gender, P),
|
|
2273
2293
|
devicePlatforms:P.mappers.devices(g.tg_device, P),
|
|
2274
2294
|
placements:P.mappers.placements(g.tg_placement, P),
|
|
@@ -2277,6 +2297,8 @@ PLATFORMS.meta = {
|
|
|
2277
2297
|
pixelId:String(p.pixelId||''), /* promoted_object 用 */
|
|
2278
2298
|
customEventType:p.customEventType||'PURCHASE'
|
|
2279
2299
|
};
|
|
2300
|
+
/* 終了日は任意。空欄なら endTime キー自体を出さない (= 無期限配信) */
|
|
2301
|
+
const endT=P.mappers.endTime(p.endDate); if(endT) ag.endTime=endT;
|
|
2280
2302
|
if(P.enums.bidAmountRequiredFor.indexOf(ag.bidStrategy)>=0 || Number(g.bidAmount)>0)
|
|
2281
2303
|
ag.bidAmount=Number(g.bidAmount)||0;
|
|
2282
2304
|
const aid=resolveAdGroupIdOf(g); if(aid) ag.adSetId=aid;
|
|
@@ -2358,7 +2380,7 @@ PLATFORMS.meta = {
|
|
|
2358
2380
|
strategy(){
|
|
2359
2381
|
return ['Meta広告(Facebook / Instagram)の入稿プランを一緒に作りたいです。まず戦略を壁打ちさせてください。','',
|
|
2360
2382
|
'# 前提','- アカウント: <アカウント名(日本語OK)>','- 商材 / LP: <商材名・URL>',
|
|
2361
|
-
'- 目的(objective): <OUTCOME_SALES / OUTCOME_LEADS など>','- 予算・期間: <広告セットの日予算 /
|
|
2383
|
+
'- 目的(objective): <OUTCOME_SALES / OUTCOME_LEADS など>','- 予算・期間: <広告セットの日予算 / 配信開始日・配信終了日>',
|
|
2362
2384
|
'- 訴求したいポイント: <強み・オファー・ターゲット層>','',
|
|
2363
2385
|
'# お願い',
|
|
2364
2386
|
'1. 上記をもとにターゲティング方針(年齢・性別・配置)・キャンペーン/広告セット構成・訴求軸を提案してください。',
|
|
@@ -2995,12 +3017,7 @@ let httpAccountFiles=[]; // <account.dir>/index.json から取得した <名前>
|
|
|
2995
3017
|
const LEGACY_ACCOUNT_DIR='アカウント';
|
|
2996
3018
|
function accountDir(){ const P=pf(); return (P&&P.account&&P.account.dir)||LEGACY_ACCOUNT_DIR; }
|
|
2997
3019
|
function legacyAccountDir(){ return curPf==='yda' ? LEGACY_ACCOUNT_DIR : null; }
|
|
2998
|
-
|
|
2999
|
-
const idx=await fetchJson(accountDir()+'/index.json');
|
|
3000
|
-
if(idx) return idx;
|
|
3001
|
-
const legacy=legacyAccountDir();
|
|
3002
|
-
return legacy ? await fetchJson(legacy+'/index.json') : null;
|
|
3003
|
-
}
|
|
3020
|
+
/* (旧 fetchAccountIndex は fetchAccountNamesFor に統合済み。呼び出し元が無くなったため削除) */
|
|
3004
3021
|
async function fetchAccountJsonByName(name){
|
|
3005
3022
|
const rel=encodeURIComponent(name)+'.json';
|
|
3006
3023
|
const obj=await fetchJson(accountDir()+'/'+rel);
|
|
@@ -3008,16 +3025,34 @@ async function fetchAccountJsonByName(name){
|
|
|
3008
3025
|
const legacy=legacyAccountDir();
|
|
3009
3026
|
return legacy ? await fetchJson(legacy+'/'+rel) : null;
|
|
3010
3027
|
}
|
|
3011
|
-
|
|
3028
|
+
/* S11: 媒体別のアカウント名一覧。統合プルダウン (案件×媒体) はこれを全媒体ぶん結合して描画する。
|
|
3029
|
+
ACCOUNTS_BY_PF[<platform>] = ['<アカウント名>', ...] */
|
|
3030
|
+
const ACCOUNTS_BY_PF={};
|
|
3031
|
+
/* 1媒体ぶんのアカウント名一覧を取得 (index.json → 無ければディレクトリ一覧HTML)。 */
|
|
3032
|
+
async function fetchAccountNamesFor(pfId){
|
|
3033
|
+
const P=PLATFORMS[pfId]; if(!P) return [];
|
|
3034
|
+
const dir=(P.account&&P.account.dir)||LEGACY_ACCOUNT_DIR;
|
|
3035
|
+
const legacy=(pfId==='yda')?LEGACY_ACCOUNT_DIR:null;
|
|
3012
3036
|
const set=new Set();
|
|
3013
3037
|
// (1) マニフェスト <account.dir>/index.json (file://・autoindex無しでも読める。旧レイアウトへフォールバック)
|
|
3014
|
-
try{
|
|
3038
|
+
try{
|
|
3039
|
+
let idx=await fetchJson(dir+'/index.json');
|
|
3040
|
+
if(!idx && legacy) idx=await fetchJson(legacy+'/index.json');
|
|
3041
|
+
if(idx){ const arr=Array.isArray(idx)?idx:(idx.accounts||[]); arr.map(String).filter(Boolean).forEach(n=>set.add(n)); }
|
|
3042
|
+
}catch(e){}
|
|
3015
3043
|
// (2) ディレクトリ一覧(HTML) — http配信時は index.json に無い新規ファイルも自動検出してマージ
|
|
3016
|
-
for(const
|
|
3017
|
-
if(!
|
|
3018
|
-
try{ const r=await fetch(
|
|
3044
|
+
for(const d of [dir, legacy]){
|
|
3045
|
+
if(!d) continue;
|
|
3046
|
+
try{ const r=await fetch(d+'/',{cache:'no-store'}); if(r&&r.ok){ const html=await r.text(); const re=/href="([^"]+\.json)"/gi; let m; while((m=re.exec(html))){ let n=decodeURIComponent(m[1]).replace(/^.*\//,'').replace(/\.json$/i,''); if(n&&n!=='index') set.add(n); } } }catch(e){}
|
|
3019
3047
|
}
|
|
3020
|
-
|
|
3048
|
+
return [...set].sort();
|
|
3049
|
+
}
|
|
3050
|
+
/* 全媒体のアカウント一覧を取得する。1媒体が読めなくても他媒体は出す (Promise ごとに握り潰す)。 */
|
|
3051
|
+
async function fetchAccountList(){
|
|
3052
|
+
await Promise.all(Object.keys(PLATFORMS).map(async id=>{
|
|
3053
|
+
try{ ACCOUNTS_BY_PF[id]=await fetchAccountNamesFor(id); }catch(e){ if(!ACCOUNTS_BY_PF[id]) ACCOUNTS_BY_PF[id]=[]; }
|
|
3054
|
+
}));
|
|
3055
|
+
httpAccountFiles=(ACCOUNTS_BY_PF[curPf]||[]).slice(); // 既存コード互換: 現在媒体の一覧
|
|
3021
3056
|
}
|
|
3022
3057
|
function accStatusText(){
|
|
3023
3058
|
if(!fsSupported) return 'このブラウザはフォルダ連携非対応';
|
|
@@ -3025,13 +3060,42 @@ function accStatusText(){
|
|
|
3025
3060
|
if(pendingAccDirHandle) return 'フォルダ再接続待ち(📁 フォルダ を押す)';
|
|
3026
3061
|
return 'フォルダ接続してください(現在は同梱の'+BUNDLED_ACCOUNT_NAME+'を使用中)';
|
|
3027
3062
|
}
|
|
3063
|
+
/* S11: 統合プルダウン用ヘルパ。option の value は "<platform>::<アカウント名>"。 */
|
|
3064
|
+
function accOptionValue(pfId, name){ return pfId+'::'+name; }
|
|
3065
|
+
function parseAccOptionValue(v){ const i=String(v).indexOf('::'); return i<0 ? {pf:curPf, name:String(v)} : {pf:String(v).slice(0,i), name:String(v).slice(i+2)}; }
|
|
3066
|
+
/* 媒体 pfId のアカウント名一覧。現在媒体でフォルダ接続中はフォルダ内ファイルを正とする。 */
|
|
3067
|
+
function accountNamesForPf(pfId){
|
|
3068
|
+
if(pfId===curPf && accDirHandle) return accountFiles.slice();
|
|
3069
|
+
const names=(ACCOUNTS_BY_PF[pfId]||[]).slice();
|
|
3070
|
+
const b=BUNDLES[pfId]||{};
|
|
3071
|
+
if(b.accountName && !names.includes(b.accountName)) names.unshift(b.accountName);
|
|
3072
|
+
return names;
|
|
3073
|
+
}
|
|
3028
3074
|
function renderAccountBar(){
|
|
3029
3075
|
const sel=document.getElementById('accSelect'); if(!sel) return;
|
|
3030
3076
|
const cur=st.project.accountName||BUNDLED_ACCOUNT_NAME;
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3077
|
+
const curVal=accOptionValue(curPf, cur);
|
|
3078
|
+
// S11: 「案件 × 媒体」を全媒体ぶん列挙する (片方の媒体の一覧が読めなくても、もう片方は出る)
|
|
3079
|
+
let opts='';
|
|
3080
|
+
Object.keys(PLATFORMS).forEach(pid=>{
|
|
3081
|
+
const names=accountNamesForPf(pid);
|
|
3082
|
+
if(pid===curPf && cur && !names.includes(cur)) names.unshift(cur);
|
|
3083
|
+
const label=(PLATFORMS[pid]&&PLATFORMS[pid].displayName)||pid;
|
|
3084
|
+
/* S11-fix: アカウント一覧が1件も取れない媒体 (file:// / アカウントディレクトリ未配置 / http未起動 で
|
|
3085
|
+
fetch が全滅した状態) でも、媒体そのものには到達できるようにプレースホルダを1件必ず出す。
|
|
3086
|
+
旧 #pfSeg / #mediaTabs が担っていた「媒体切替と媒体要件ドキュメントの閲覧」を統合プルダウンで維持する。
|
|
3087
|
+
value は "<platform>::"(アカウント名が空)で、selectAccount は媒体切替だけ行う。 */
|
|
3088
|
+
if(!names.length){
|
|
3089
|
+
const v=accOptionValue(pid,'');
|
|
3090
|
+
opts+='<option value="'+esc(v)+'"'+(pid===curPf?' selected':'')+'>'+esc(label)+'(アカウント未読込)</option>';
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
names.forEach(n=>{
|
|
3094
|
+
const v=accOptionValue(pid,n);
|
|
3095
|
+
const bundled=(BUNDLES[pid]&&BUNDLES[pid].accountName===n && !(pid===curPf&&accDirHandle));
|
|
3096
|
+
opts+='<option value="'+esc(v)+'"'+(v===curVal?' selected':'')+'>'+esc(n)+' '+esc(label)+(bundled?'(同梱)':'')+'</option>';
|
|
3097
|
+
});
|
|
3098
|
+
});
|
|
3035
3099
|
// F-3: プルダウン内にアクション項目 (新規追加/このアカウントを編集)。フォルダ接続オプションは廃止
|
|
3036
3100
|
opts+='<option disabled>──────────</option>';
|
|
3037
3101
|
opts+='<option value="__add__">+ 新規追加…</option>';
|
|
@@ -3079,26 +3143,54 @@ window.applyAccountJson=function(obj){
|
|
|
3079
3143
|
snapshot();
|
|
3080
3144
|
st.refs=normalizeRefs(obj.refs);
|
|
3081
3145
|
pf().account.apply(obj, st.project); // S7: project への写し込みは媒体スキーマ (accountId書式/追加項目)
|
|
3146
|
+
/* S11-fix2: switchPlatform 内の initialState() は「同梱デフォルトアカウント」を基準に
|
|
3147
|
+
サンプルプランの accountId を照合するため、別アカウントを選ぶと必ず不一致になり
|
|
3148
|
+
campaigns=[] のまま残る。ここでアカウントが確定したので、プランが空のときに限り
|
|
3149
|
+
サンプルプランを読み直す (既存プランがあるときは触らない)。 */
|
|
3150
|
+
if((!st.campaigns || !st.campaigns.length) && samplePlan && Array.isArray(samplePlan.campaigns)
|
|
3151
|
+
&& samplePlan.campaigns.length && String(samplePlan.accountId||'')===String(st.project.accountId||'')){
|
|
3152
|
+
st.campaigns=JSON.parse(JSON.stringify(samplePlan.campaigns));
|
|
3153
|
+
st.campaigns.forEach(c=>{ c.uid=uid('c'); (c.adGroups||[]).forEach(g=>{ g.uid=uid('g'); (g.ads||[]).forEach(a=>{ a.uid=uid('a'); }); }); });
|
|
3154
|
+
planMismatchMsg='';
|
|
3155
|
+
if(typeof renderPlanWarn==='function') renderPlanWarn();
|
|
3156
|
+
}
|
|
3082
3157
|
persist(); buildDatalists(); render(); renderAccountBar();
|
|
3083
3158
|
};
|
|
3084
|
-
|
|
3159
|
+
/* S11: 統合プルダウンの選択 = 「案件 × 媒体」。
|
|
3160
|
+
媒体が変わる場合は **先に** switchPlatform してから、その媒体のアカウントJSONを読む
|
|
3161
|
+
(逆順だと applyAccountJson の platform 検証・accountId 不一致の安全弁に弾かれる)。 */
|
|
3162
|
+
async function selectAccount(name, pfId){
|
|
3085
3163
|
if(String(name).startsWith('__')){ renderAccountBar(); return; } // アクション項目は change リスナーで処理
|
|
3164
|
+
const target=pfId||curPf;
|
|
3165
|
+
const prevPf=curPf;
|
|
3166
|
+
if(target!==curPf){
|
|
3167
|
+
if(!PLATFORMS[target]){ toast('この媒体はまだ選択できません(媒体スキーマ未登録)'); renderAccountBar(); return; }
|
|
3168
|
+
if(!window.switchPlatform(target)){ renderAccountBar(); return; }
|
|
3169
|
+
}
|
|
3170
|
+
// S11-fix: 「<媒体>(アカウント未読込)」プレースホルダ = 媒体切替のみ (アカウントJSONは読まない)
|
|
3171
|
+
if(!name){ renderAccountBar(); return; }
|
|
3086
3172
|
if(name===st.project.accountName){ renderAccountBar(); return; }
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
}
|
|
3094
|
-
|
|
3095
|
-
|
|
3173
|
+
/* (1) フォルダ接続中で、そのフォルダにあるアカウント。
|
|
3174
|
+
S11-fix: 接続フォルダは媒体別ではないので、媒体をまたぐ選択のときは参照しない
|
|
3175
|
+
(別媒体の同名JSONを掴んで applyAccountJson の platform 検証で媒体が巻き戻る) */
|
|
3176
|
+
if(accDirHandle && target===prevPf && accountFiles.includes(name)){
|
|
3177
|
+
const obj=await readAccountJson(name);
|
|
3178
|
+
if(!obj){ toast('アカウントJSONを読み込めません: '+name); renderAccountBar(); return; }
|
|
3179
|
+
try{ window.applyAccountJson(obj); toast('アカウント '+name+' に切替(参照データを差し替え)'); }
|
|
3180
|
+
catch(e){ toast(e.message); renderAccountBar(); }
|
|
3181
|
+
return;
|
|
3182
|
+
}
|
|
3183
|
+
// (2) index.json / ディレクトリ一覧に載っているアカウントを fetch で読み込み(読み取り専用切替)
|
|
3184
|
+
if((ACCOUNTS_BY_PF[curPf]||[]).includes(name)){
|
|
3185
|
+
const obj=await fetchAccountJsonByName(name);
|
|
3186
|
+
if(obj){ try{ window.applyAccountJson(obj); toast('アカウント '+name+' に切替(参照データを差し替え)'); }catch(e){ toast(e.message); renderAccountBar(); } }
|
|
3187
|
+
else { toast('アカウントJSONを読み込めません: '+name); renderAccountBar(); }
|
|
3096
3188
|
return;
|
|
3097
3189
|
}
|
|
3098
|
-
|
|
3099
|
-
if(
|
|
3100
|
-
|
|
3101
|
-
|
|
3190
|
+
// (3) 同梱デフォルトアカウント
|
|
3191
|
+
if(name===BUNDLED_ACCOUNT_NAME){ window.applyAccountJson({accountJsonVersion:ACC_JSON_VERSION, platform:curPf, accountId:BUNDLED_ACCOUNT_ID, name:BUNDLED_ACCOUNT_NAME, principal:bundledPrincipal, refs:buildRefs()}); toast('同梱アカウント '+BUNDLED_ACCOUNT_NAME+' に切替'); return; }
|
|
3192
|
+
if(!accDirHandle){ toast('先にアカウントフォルダに接続してください'); renderAccountBar(); return; }
|
|
3193
|
+
toast('アカウントJSONを読み込めません: '+name); renderAccountBar();
|
|
3102
3194
|
}
|
|
3103
3195
|
/* refAdd/refEdit/refDel 後にアクティブアカウントJSONへ自動保存 (フォルダ接続 & フォルダ内アカウント時) */
|
|
3104
3196
|
async function saveActiveAccountJson(){
|
|
@@ -3507,22 +3599,7 @@ document.getElementById('viewSeg').addEventListener('click',e=>{ const b=e.targe
|
|
|
3507
3599
|
document.getElementById('sheetFilter').addEventListener('change',()=>{ clearSel(); renderSheet(); });
|
|
3508
3600
|
/* プランビュー: シート / ツリー モード切替 (1階層下) */
|
|
3509
3601
|
document.getElementById('modeSeg').addEventListener('click',e=>{ const b=e.target.closest('button[data-m]'); if(!b) return; clearSel(); closeDrawer(); planMode=b.dataset.m; document.querySelectorAll('#modeSeg button').forEach(x=>x.classList.toggle('active',x===b)); render(); });
|
|
3510
|
-
/*
|
|
3511
|
-
document.getElementById('pfSeg').addEventListener('click',e=>{
|
|
3512
|
-
const b=e.target.closest('button[data-pf]'); if(!b) return;
|
|
3513
|
-
const id=b.dataset.pf;
|
|
3514
|
-
if(PLATFORMS[id]){ if(id!==curPf) window.switchPlatform(id); else renderPlatformTabs(); }
|
|
3515
|
-
else toast('この媒体はまだ選択できません(媒体スキーマ未登録・媒体要件の閲覧のみ)');
|
|
3516
|
-
});
|
|
3517
|
-
/* 媒体セレクタ: 媒体要件ドキュメントのタブ切替 + 登録済み媒体なら媒体そのものを切り替える (S9) */
|
|
3518
|
-
document.getElementById('mediaTabs').addEventListener('click',e=>{
|
|
3519
|
-
const b=e.target.closest('button[data-mt]'); if(!b) return;
|
|
3520
|
-
document.querySelectorAll('#mediaTabs button').forEach(x=>x.classList.toggle('active',x===b));
|
|
3521
|
-
showMediaDoc(b.dataset.mt);
|
|
3522
|
-
const id=b.dataset.pf; if(!id) return;
|
|
3523
|
-
if(PLATFORMS[id]){ if(id!==curPf) window.switchPlatform(id); }
|
|
3524
|
-
else toast('この媒体はまだ選択できません(媒体スキーマ未登録・媒体要件の閲覧のみ)');
|
|
3525
|
-
});
|
|
3602
|
+
/* S11: 媒体セレクタ (#pfSeg) / 媒体要件タブ (#mediaTabs) は廃止。媒体切替はヘッダーの統合プルダウンに一本化 */
|
|
3526
3603
|
/* YDA内: 概要 / レスポンシブ / バナー / パラメータ / 使い方 タブ切替 */
|
|
3527
3604
|
document.getElementById('ydaTabs').addEventListener('click',e=>{ const b=e.target.closest('button[data-yt]'); if(!b) return; document.querySelectorAll('#ydaTabs button').forEach(x=>x.classList.toggle('active',x===b)); ['overview','resp','banner','param','guide'].forEach(k=>{ document.getElementById('yt-'+k).style.display = b.dataset.yt===k?'':'none'; }); });
|
|
3528
3605
|
/* 案件テンプレ: チップ直接編集 */
|
|
@@ -3538,7 +3615,8 @@ document.getElementById('accFileInput').addEventListener('change',e=>{ const f=e
|
|
|
3538
3615
|
document.getElementById('accSelect').addEventListener('change',e=>{ const v=e.target.value;
|
|
3539
3616
|
if(v==='__add__'){ renderAccountBar(); openAddAccountModal(); return; }
|
|
3540
3617
|
if(v==='__edit__'){ renderAccountBar(); openEditAccountModal(); return; }
|
|
3541
|
-
|
|
3618
|
+
const t=parseAccOptionValue(v); // S11: "<platform>::<アカウント名>"
|
|
3619
|
+
selectAccount(t.name, t.pf);
|
|
3542
3620
|
});
|
|
3543
3621
|
/* E-1: キャンペーン追加導線はシート/ツリー内(data-act="add-campaign"/"add-campaign-existing")に配置。ツールバー独立ボタンは廃止 */
|
|
3544
3622
|
document.getElementById('histHost').addEventListener('click',async e=>{
|
|
@@ -3649,6 +3727,17 @@ window.addEventListener('beforeunload', function(e){ if(_dirty){ e.preventDefaul
|
|
|
3649
3727
|
if(bundledRefs || samplePlan){ st=initialState(); st.project.accountId=BUNDLED_ACCOUNT_ID; st.project.accountName=BUNDLED_ACCOUNT_NAME; if(typeof buildDatalists==='function') buildDatalists(); render(); _dirty=false; }
|
|
3650
3728
|
else { toast('初期データ(アカウント/サンプルプラン)を読み込めません。httpサーバー経由で開くか、アカウントを接続してください'); }
|
|
3651
3729
|
renderAccountBar();
|
|
3730
|
+
/* S11-fix3: 「いま作業中のプラン」を アクティブプラン.json (ad-ops スキルが書き出す) から適用する。
|
|
3731
|
+
これが無いと同梱デフォルト (sample_account) で開くため、毎回プルダウンで案件×媒体を選び直す
|
|
3732
|
+
ことになり、さらにサンプルプランの accountId 不一致でプランが読み込まれない。 */
|
|
3733
|
+
try{
|
|
3734
|
+
const r=await fetch(ACTIVE_PLAN_FILE, {cache:'no-store'});
|
|
3735
|
+
if(r.ok){
|
|
3736
|
+
const a=await r.json();
|
|
3737
|
+
const tp=String(a.platform||'yda'), tn=String(a.accountName||'');
|
|
3738
|
+
if(tn && PLATFORMS[tp] && !(tp===curPf && tn===st.project.accountName)) await selectAccount(tn, tp);
|
|
3739
|
+
}
|
|
3740
|
+
}catch(e){}
|
|
3652
3741
|
})();
|
|
3653
3742
|
</script>
|
|
3654
3743
|
</body>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ad-ops スキルの単一エントリポイント。
|
|
3
|
+
#
|
|
4
|
+
# adops.sh preflight <platform> <accountId> フェーズ0: ログイン + アカウント疎通確認
|
|
5
|
+
# adops.sh planner [--stop] フェーズ2: プランナーをHTTPで開く (file:// 禁止)
|
|
6
|
+
# adops.sh plan [<入稿計画.json>] フェーズ3: DL回収 + 要約 + lint + 合意との差分
|
|
7
|
+
#
|
|
8
|
+
# 覚えるコマンドを1本に畳むためのラッパー。個別の python3 -c / curl / ls -t を書かない。
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
|
|
11
|
+
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
12
|
+
PORT=8799 # cli/playwright.config.ts の webServer と同じポートを共用する
|
|
13
|
+
URL="http://127.0.0.1:${PORT}/ad-planner.html"
|
|
14
|
+
CLI="npx -y addashboard-cli@latest"
|
|
15
|
+
|
|
16
|
+
die() { echo "✗ $*" >&2; exit 1; }
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------- preflight
|
|
19
|
+
# フェーズ4の本実行で初めて 401 Account not found に気づく事故を防ぐ。
|
|
20
|
+
# 戦略壁打ちを始める前にこれを通し、落ちたら戦略を練らずに止める。
|
|
21
|
+
cmd_preflight() {
|
|
22
|
+
local platform="${1:-}" account="${2:-}"
|
|
23
|
+
[ -n "$platform" ] && [ -n "$account" ] \
|
|
24
|
+
|| die "usage: adops.sh preflight <platform> <accountId>"
|
|
25
|
+
|
|
26
|
+
echo "▸ ログイン確認..."
|
|
27
|
+
local who
|
|
28
|
+
who="$($CLI auth whoami 2>&1)" || {
|
|
29
|
+
echo "$who" >&2
|
|
30
|
+
die "未ログイン、または認証が期限切れです。ターミナルで '$CLI auth login' を実行してください(Google SSO は本人が操作)"
|
|
31
|
+
}
|
|
32
|
+
python3 -c 'import json,sys; d=json.load(sys.stdin); print(" ✓", d.get("email"), "/ role:", d.get("role"))' <<<"$who" \
|
|
33
|
+
|| die "whoami の応答を解釈できません: $who"
|
|
34
|
+
|
|
35
|
+
echo "▸ アカウント疎通確認 (${platform} / ${account})..."
|
|
36
|
+
local accounts
|
|
37
|
+
accounts="$($CLI list --platform "$platform" --resource accounts 2>&1)" \
|
|
38
|
+
|| { echo "$accounts" >&2; die "アカウント一覧の取得に失敗しました"; }
|
|
39
|
+
|
|
40
|
+
ACCOUNT_ID="$account" python3 - <<'PY' <<<"$accounts"
|
|
41
|
+
import json, os, sys
|
|
42
|
+
want = os.environ["ACCOUNT_ID"]
|
|
43
|
+
try:
|
|
44
|
+
accounts = json.load(sys.stdin).get("accounts", [])
|
|
45
|
+
except Exception as e:
|
|
46
|
+
sys.exit(f"✗ アカウント一覧を解釈できません: {e}")
|
|
47
|
+
hit = next((a for a in accounts if str(a.get("id")) == want), None)
|
|
48
|
+
if hit:
|
|
49
|
+
print(f" ✓ {want} — {hit.get('name')} (アクセス可能: 全{len(accounts)}件中)")
|
|
50
|
+
sys.exit(0)
|
|
51
|
+
print(f" ✗ accountId {want} は接続中の認証情報のスコープ外です(このまま入稿すると 401 Account not found)")
|
|
52
|
+
print(f" アクセス可能な {len(accounts)} 件:")
|
|
53
|
+
for a in accounts:
|
|
54
|
+
print(f" {a.get('id')} {a.get('name')}")
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
PY
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# ------------------------------------------------------------------ planner
|
|
60
|
+
# file:// で開くと fetch('サンプルプラン.json') がブロックされ、
|
|
61
|
+
# エラーも出ないまま空のプランナーが表示される。必ずHTTPで開く。
|
|
62
|
+
cmd_planner() {
|
|
63
|
+
if [ "${1:-}" = "--stop" ]; then
|
|
64
|
+
pkill -f "http.server ${PORT}" && echo "✓ プランナーの配信を停止しました" \
|
|
65
|
+
|| echo "(起動中のサーバーはありませんでした)"
|
|
66
|
+
return 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
if curl -sf -o /dev/null "$URL"; then
|
|
70
|
+
echo "✓ 配信は起動済み(再利用)"
|
|
71
|
+
else
|
|
72
|
+
echo "▸ http.server を起動中 (port ${PORT})..."
|
|
73
|
+
# nohup でデタッチしないと、呼び出し元シェルの終了に巻き込まれて死ぬ
|
|
74
|
+
nohup python3 -m http.server "$PORT" --directory "$SKILL_DIR" \
|
|
75
|
+
>/tmp/adops-planner.log 2>&1 &
|
|
76
|
+
for _ in $(seq 1 20); do
|
|
77
|
+
curl -sf -o /dev/null "$URL" && break
|
|
78
|
+
sleep 0.3
|
|
79
|
+
done
|
|
80
|
+
curl -sf -o /dev/null "$URL" || die "配信を起動できませんでした(/tmp/adops-planner.log を確認)"
|
|
81
|
+
echo "✓ 起動しました"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
open "$URL"
|
|
85
|
+
echo " $URL"
|
|
86
|
+
echo " 停止: adops.sh planner --stop"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# --------------------------------------------------------------------- plan
|
|
90
|
+
cmd_plan() {
|
|
91
|
+
python3 "${SKILL_DIR}/scripts/plan_lint.py" "$@"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case "${1:-}" in
|
|
95
|
+
preflight) shift; cmd_preflight "$@" ;;
|
|
96
|
+
planner) shift; cmd_planner "$@" ;;
|
|
97
|
+
plan) shift; cmd_plan "$@" ;;
|
|
98
|
+
*)
|
|
99
|
+
cat >&2 <<EOF
|
|
100
|
+
usage:
|
|
101
|
+
adops.sh preflight <platform> <accountId> フェーズ0: ログイン + アカウント疎通確認
|
|
102
|
+
adops.sh planner [--stop] フェーズ2: プランナーをHTTPで開く
|
|
103
|
+
adops.sh plan [<入稿計画.json>] フェーズ3: DL回収 + 要約 + lint + 合意との差分
|
|
104
|
+
EOF
|
|
105
|
+
exit 2 ;;
|
|
106
|
+
esac
|