"""事業ダッシュボードの見本ジェネレーター（Python標準ライブラリだけ・読み取り専用・チャートはインラインSVG）。

使い方:
  python build_dashboard.py                    sample/ を読んで dashboard_sample.html を作る
  python build_dashboard.py 自分のフォルダ       自分のデータのフォルダを読む
  python build_dashboard.py --today 2026-10-05 基準日を指定する（省略時は company.md の as_of、なければ今日）

フォルダの形（1ファイル＝1件。先頭の --- で囲んだ項目欄が元データ）:
  company.md          name / owner / as_of
  members/*.md        kind(ai|partner|outsource|arbeit|part) / name / role / business / email(人。省略時は example.com) / glyph・color(ai) / contract_hours(arbeit・part)
                      partner=業務委託（継続）・outsource=外注（単発）・arbeit=アルバイト・part=パート
  businesses/*.md     name / member(担当partnerのファイル名) / metrics(見る数字を | 区切りで。先頭が折れ線の主役)
  reports/*.md        business / date / kind   ← 報告が届いたら1ファイル足す
  metrics/metrics.csv week_end,business,metric,value   ← アクセス・申込・クリックなど外部の数字（週ごと・日曜締め）
  actionlog.csv       date,time,agent,business,what     ← AIエージェントの作業記録
  deliveries/*.md     member / business / title / due / status / pay_due / amount   ← 業務委託・外注の納品物（納期・検収・支払で見る）
  shifts.csv          date,member,hours                  ← アルバイト・パートのシフトの実績（雇用の人だけ）
  tasks/*.md          owner / status / due / title / business   ← 載せるのは owner が「自分」の行だけ

データの設計 → チャートの形（何を知りたいかで形を決める）:
  現在の数字＋前週比＋12週の流れ  → ステータスタイル＋スパークライン、主役の数字は折れ線
  前週からどの数字がどれだけ動いたか → 左右に伸びる棒（増=青・減=赤・中央は灰）
  AIの稼働の波                     → 14日の縦棒（今日だけ色、過去は灰）
  報告のリズム                     → 直近8週の点（届いた週は塗り、抜けた週は輪）＋ 7日の目安のメーター
  どれも『表で見る』が付く（色や形だけに頼らない）

設計の決まり（S6 第3回ワーク）:
  ・ファーストビューに、各事業の状態・数字・最終更新を全部出す。並びは『更新が新しい順』だけ
  ・見るのは『動く数字』だけ。仕様（種類の数・設定済みか）は監視しない
  ・報告の最終日と経過日数を、事業カードの中に出す。7日を超えたら赤
  ・台帳に載せるのは、自分にボールがある行だけ。相手の宿題は載せない・順位や点数は作らない
  ・読めない数字は『未取得』、記録がなければ『記録なし』。推測で埋めない
  ・止まったデータは、線が途中で終わる（週の軸はそろえる）。直近の週まで線をつなげて隠さない
  ・メンバーは契約形態ごとに見る数字を変える。業務委託・外注は『納期・検収・支払』（時間は管理しない）、
    アルバイト・パートは『シフトの実績と契約の上限』（雇用は労働時間の管理が要る。給与・社会保険は専用のサービスと専門家へ）
  ・連絡のボタンは、メンバーの状況（検収待ち・納期超過・上限超過・待機など）から案を出す。押すと
    人にはメールの作成画面（下書き）、AIにはToDoの指示文のコピー。送信はしない（本文を読んで自分で送る）
  ・出力は生成物。手で編集せず、元データを直して作り直す
"""
import argparse
import csv
import datetime as dt
import html
import json
import math
import re
from pathlib import Path

REPORT_DAYS = 7
WEEKS = 12
KIND_LABEL = {"ai": "AIエージェント", "partner": "業務委託（継続）", "outsource": "外注（単発）", "arbeit": "アルバイト", "part": "パート"}
HUMAN_KINDS = ("partner", "outsource", "arbeit", "part")
HERE = Path(__file__).resolve().parent


def e(x):
    return html.escape(str(x))


def clip(text, n):
    text = str(text)
    return text if len(text) <= n else text[: n - 1] + "…"


def front(path):
    """先頭の --- で囲んだ『項目: 値』を読む。戻り値は (項目の辞書, 本文)。"""
    text = Path(path).read_text(encoding="utf-8-sig")
    m = re.match(r"^---\r?\n(.*?)\r?\n---\r?\n?(.*)$", text, re.S)
    meta, body = {}, text
    if m:
        for line in m.group(1).splitlines():
            if ":" in line:
                k, v = line.split(":", 1)
                meta[k.strip()] = v.strip()
        body = m.group(2)
    meta["_id"] = Path(path).stem
    return meta, body


def load(folder):
    d = Path(folder)
    return [front(p)[0] for p in sorted(d.glob("*.md"))] if d.exists() else []


def day(text):
    try:
        return dt.date.fromisoformat(str(text)[:10])
    except ValueError:
        return None


def fmt(v):
    return f"{v:,.0f}" if abs(v - round(v)) < 1e-9 else f"{v:,.1f}"


def nice_max(v):
    """軸の上端を、きりのよい数にする（1・2・2.5・5 × 10のべき）。"""
    if v <= 0:
        return 1
    p = 10 ** math.floor(math.log10(v))
    for m in (1, 2, 2.5, 5, 10):
        if v <= m * p:
            return m * p
    return 10 * p


def week_axis(today):
    """直近の日曜（週の終わり）から数えて12週の軸。止まったデータは、この軸の途中で線が終わる。"""
    last = today - dt.timedelta(days=(today.weekday() + 1) % 7)
    return [last - dt.timedelta(days=7 * i) for i in range(WEEKS - 1, -1, -1)]


# ---------------- 顔（人物は似顔、AIは役割ごとの形。色は s1〜s4 の固定順） ----------------
SKIN = ("#f3d3b8", "#e8b996", "#f6dcc6", "#d9a47f")
HAIR = ("#2b2320", "#4a3328", "#1f2430", "#6b4a35")
SHIRT = ("#5b7be0", "#e07b9b", "#4fb98f", "#e0a84f", "#8c6be0", "#4fa9c9")


def person_svg(name):
    h = sum(ord(c) * (i + 3) for i, c in enumerate(name))
    skin, hair, shirt = SKIN[h % 4], HAIR[(h // 3) % 4], SHIRT[(h // 5) % 6]
    long_hair = (h // 11) % 2 == 1
    back = f'<path d="M20 48 Q18 18 40 16 Q62 18 60 48 L60 64 L52 64 L52 40 L28 40 L28 64 L20 64Z" fill="{hair}"/>' if long_hair else ""
    return (f'<svg viewBox="0 0 80 80" aria-hidden="true"><rect width="80" height="80" fill="#243049"/>{back}'
            f'<path d="M10 80 Q12 58 40 58 Q68 58 70 80Z" fill="{shirt}"/><ellipse cx="40" cy="38" rx="16" ry="18" fill="{skin}"/>'
            f'<path d="M24 36 Q22 17 40 17 Q58 17 56 36 Q54 26 40 25 Q28 26 24 36Z" fill="{hair}"/>'
            f'<circle cx="34" cy="39" r="1.9" fill="#2b2320"/><circle cx="46" cy="39" r="1.9" fill="#2b2320"/>'
            f'<path d="M35 47 Q40 51 45 47" stroke="#a5604a" stroke-width="1.8" fill="none" stroke-linecap="round"/></svg>')


def agent_svg(glyph, slot):
    if not re.fullmatch("s[1-4]", slot):
        slot = "s1"
    c = f"var(--{slot})"
    shapes = {
        "spark": "".join(f'<line x1="40" y1="40" x2="{40 + 26 * math.cos(i * 0.7854):.1f}" y2="{40 + 26 * math.sin(i * 0.7854):.1f}" stroke="{c}" stroke-width="6" stroke-linecap="round"/>' for i in range(8)),
        "hex": f'<polygon points="40,10 66,25 66,55 40,70 14,55 14,25" fill="none" stroke="{c}" stroke-width="5" stroke-linejoin="round"/><circle cx="40" cy="40" r="7" fill="{c}"/>',
        "star": f'<path d="M40 8 Q43 37 72 40 Q43 43 40 72 Q37 43 8 40 Q37 37 40 8Z" fill="{c}"/>',
        "ring": f'<circle cx="40" cy="40" r="24" fill="none" stroke="{c}" stroke-width="6"/><line x1="18" y1="62" x2="62" y2="18" stroke="{c}" stroke-width="6" stroke-linecap="round"/>',
    }
    return f'<svg viewBox="0 0 80 80" aria-hidden="true"><rect width="80" height="80" fill="var(--plane)"/>{shapes.get(glyph, shapes["ring"])}</svg>'


# ---------------- チャート部品（線は2px・端点は8px＋面色の輪・面は10%・格子は髪の毛の細さ） ----------------
def spark(points, axis, w=108, h=30, pad=4, color="var(--s1)"):
    """12週のスパークライン。止まったデータは、軸の途中で線が終わる。"""
    xs = {d: pad + i * (w - 2 * pad) / (WEEKS - 1) for i, d in enumerate(axis)}
    vals = [(d, points[d]) for d in axis if d in points]
    if not vals:
        return f'<svg class="spark" viewBox="0 0 {w} {h}" aria-hidden="true"><line x1="{pad}" y1="{h / 2}" x2="{w - pad}" y2="{h / 2}" stroke="var(--axis)" stroke-width="1"/></svg>'
    lo, hi = min(v for _, v in vals), max(v for _, v in vals)
    span = (hi - lo) or 1
    pts = [(xs[d], h - pad - (v - lo) / span * (h - 2 * pad)) for d, v in vals]
    line = " ".join(f"{x:.1f},{y:.1f}" for x, y in pts)
    area = f"M{pts[0][0]:.1f},{h - pad} " + " ".join(f"L{x:.1f},{y:.1f}" for x, y in pts) + f" L{pts[-1][0]:.1f},{h - pad} Z"
    x, y = pts[-1]
    return (f'<svg class="spark" viewBox="0 0 {w} {h}" aria-hidden="true"><path d="{area}" fill="{color}" opacity=".10"/>'
            f'<polyline points="{line}" fill="none" stroke="{color}" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>'
            f'<circle cx="{x:.1f}" cy="{y:.1f}" r="4" fill="{color}" stroke="var(--surface)" stroke-width="2"/></svg>')


def line_chart(name, points, axis, w=520, h=150):
    """主役の数字の折れ線（ゼロ始まり・格子3本・端点だけ値のラベル・ホバーで十字線と値）。"""
    ml, mr, mt, mb = 40, 22, 16, 24
    vals = [(d, points[d]) for d in axis if d in points]
    if not vals:
        return '<div class="empty">データなし（metrics.csv に記録がありません）</div>'
    ymax = nice_max(max(v for _, v in vals))
    X = {d: ml + i * (w - ml - mr) / (WEEKS - 1) for i, d in enumerate(axis)}

    def Y(v):
        return mt + (1 - v / ymax) * (h - mt - mb)
    grid = "".join(f'<line x1="{ml}" y1="{Y(t):.1f}" x2="{w - mr}" y2="{Y(t):.1f}" stroke="var(--grid)" stroke-width="1"/>'
                   f'<text x="{ml - 6}" y="{Y(t) + 3.5:.1f}" text-anchor="end" class="ax">{fmt(t)}</text>' for t in (0, ymax / 2, ymax))
    xl = "".join(f'<text x="{X[d]:.1f}" y="{h - 6}" text-anchor="{"start" if i == 0 else "end" if i == WEEKS - 1 else "middle"}" class="ax">{d:%m/%d}</text>'
                 for i, d in ((0, axis[0]), (WEEKS // 2, axis[WEEKS // 2]), (WEEKS - 1, axis[-1])))
    pts = [(X[d], Y(v)) for d, v in vals]
    line = " ".join(f"{x:.1f},{y:.1f}" for x, y in pts)
    area = f"M{pts[0][0]:.1f},{Y(0):.1f} " + " ".join(f"L{x:.1f},{y:.1f}" for x, y in pts) + f" L{pts[-1][0]:.1f},{Y(0):.1f} Z"
    x, y = pts[-1]
    data = json.dumps([[round(X[d], 1), round(Y(v), 1), f"{d:%m/%d}", v] for d, v in vals], ensure_ascii=False)
    return (f'<svg class="lc" viewBox="0 0 {w} {h}" data-w="{w}" data-name="{e(name)}" data-pts="{e(data)}" role="img" aria-label="{e(name)}の12週の推移（下に表あり）">'
            f'{grid}<line x1="{ml}" y1="{Y(0):.1f}" x2="{w - mr}" y2="{Y(0):.1f}" stroke="var(--axis)" stroke-width="1"/>{xl}'
            f'<path d="{area}" fill="var(--s1)" opacity=".10"/><polyline points="{line}" fill="none" stroke="var(--s1)" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>'
            f'<line class="xh" x1="0" x2="0" y1="{mt}" y2="{h - mb}" stroke="var(--muted)" stroke-width="1" visibility="hidden"/>'
            f'<circle class="hd" r="4" fill="var(--s1)" stroke="var(--surface)" stroke-width="2" visibility="hidden"/>'
            f'<circle cx="{x:.1f}" cy="{y:.1f}" r="4" fill="var(--s1)" stroke="var(--surface)" stroke-width="2"/>'
            f'<text x="{x:.1f}" y="{y - 9:.1f}" text-anchor="{"end" if x > w - 40 else "middle"}" class="endv">{fmt(vals[-1][1])}</text></svg>')


def top_rounded(x, base, w, h, r=4):
    """基線は四角、データ側（上）は4pxの丸み。"""
    r = min(r, w / 2, h)
    y = base - h
    return f"M{x:.1f},{base:.1f} L{x:.1f},{y + r:.1f} Q{x:.1f},{y:.1f} {x + r:.1f},{y:.1f} L{x + w - r:.1f},{y:.1f} Q{x + w:.1f},{y:.1f} {x + w:.1f},{y + r:.1f} L{x + w:.1f},{base:.1f} Z"


def day_bars(counts, labels, color, bw=10, gap=3, h=38, unit="件"):
    """14日の縦棒。今日だけ色、過去は灰（強調）。0件の日は基線の小さな印だけ。"""
    n = len(counts)
    W = n * (bw + gap) - gap
    top = max(max(counts), 3)
    out = []
    for i, (c, lab) in enumerate(zip(counts, labels)):
        x = i * (bw + gap)
        if c:
            bh = max(4, c / top * (h - 6))
            fill = color if i == n - 1 else "var(--muted)"
            out.append(f'<path d="{top_rounded(x, h, bw, bh)}" fill="{fill}" opacity="{1 if i == n - 1 else .55}"><title>{e(lab)}: {c:g}{unit}</title></path>')
        else:
            out.append(f'<rect x="{x}" y="{h - 2}" width="{bw}" height="2" fill="var(--axis)"><title>{e(lab)}: 0{unit}</title></rect>')
    return f'<svg class="bars" viewBox="0 0 {W} {h}" width="{W}" height="{h}" role="img" aria-label="直近{n}日の{"作業件数" if unit == "件" else "勤務時間"}">{"".join(out)}</svg>'


def report_dots(report_days, axis):
    """直近8週の、報告が届いた週（塗り）と抜けた週（輪）。"""
    weeks = axis[-8:]
    cells = []
    for wk in weeks:
        got = [d for d in report_days if wk - dt.timedelta(days=6) <= d <= wk]
        cells.append(f'<circle cx="{len(cells) * 16 + 7}" cy="7" r="5" fill="{"var(--s1)" if got else "none"}" stroke="{"var(--surface)" if got else "var(--muted)"}" stroke-width="{2 if got else 1.5}">'
                     f'<title>{wk - dt.timedelta(days=6):%m/%d}〜{wk:%m/%d}の週: {"報告あり" if got else "報告なし"}</title></circle>')
    return f'<svg class="dots" viewBox="0 0 {len(weeks) * 16} 14" width="{len(weeks) * 16}" height="14" role="img" aria-label="直近8週の報告の有無">{"".join(cells)}</svg>'


def meter(age):
    """報告の経過日数。目安の7日に目盛り。状態の色は『状態』だけに使う。"""
    if age is None:
        return '<div class="meter"><i style="width:100%" class="bad"></i></div>'
    pct = min(age, 14) / 14 * 100
    return f'<div class="meter" title="報告から{age}日（目安{REPORT_DAYS}日）"><i style="width:{pct:.0f}%" class="{"ok" if age <= REPORT_DAYS else "bad"}"></i><b></b></div>'


def cap_meter(used, cap):
    """今週の勤務時間と、契約の上限（アルバイト・パートだけ）。上限を超えたら赤。"""
    pct = min(used / (cap * 1.25), 1) * 100 if cap else 0
    return f'<div class="meter" title="今週{used:g}時間／上限{cap:g}時間"><i style="width:{pct:.0f}%" class="{"ok" if used <= cap else "bad"}"></i><b style="left:80%"></b></div>'


def contact_menu(opts):
    """カードの中の『連絡する』。状況に当てはまる案だけを並べる。送信はしない。"""
    if not opts:
        return ""
    rows = ""
    for o in opts:
        main = ('<button data-ck="copy" data-id="%s">指示文をコピー</button>' % e(o["id"])) if o["to"] is None else (
            '<button data-ck="mail" data-id="%s">メールの下書きを開く</button><button class="ghost" data-ck="copy" data-id="%s">本文をコピー</button>' % (e(o["id"]), e(o["id"])))
        rows += f'<div class="px"><b>{e(o["kind"])}</b><small>{e(o["why"])}</small><div class="btns">{main}</div></div>'
    return f'<details class="cmd"><summary>{"指示を出す" if opts[0]["to"] is None else "連絡する"}（案{len(opts)}件）</summary>{rows}</details>'


# ---------------- 読み込みと組み立て ----------------
def build(root, today=None):
    root = Path(root)
    company = front(root / "company.md")[0]
    today = today or day(company.get("as_of")) or dt.date.today()
    axis = week_axis(today)
    members = {m["_id"]: m for m in load(root / "members")}
    businesses = load(root / "businesses")
    reports = load(root / "reports")
    tasks = load(root / "tasks")
    deliveries = load(root / "deliveries")
    shifts = {}
    sp = root / "shifts.csv"
    if sp.exists():
        with open(sp, encoding="utf-8-sig") as fh:
            for r in csv.DictReader(fh):
                shifts.setdefault(r["member"], {})[day(r["date"])] = float(r["hours"])
    metrics = []
    mp = root / "metrics/metrics.csv"
    if mp.exists():
        with open(mp, encoding="utf-8-sig") as fh:
            metrics = list(csv.DictReader(fh))
    logs = []
    lp = root / "actionlog.csv"
    if lp.exists():
        with open(lp, encoding="utf-8-sig") as fh:
            logs = sorted(csv.DictReader(fh), key=lambda r: (r["date"], r["time"]))

    def series_of(bid, name):
        return {day(r["week_end"]): float(r["value"]) for r in metrics if r["business"] == bid and r["metric"] == name and day(r["week_end"]) and day(r["week_end"]) <= today}

    days14 = [today - dt.timedelta(days=i) for i in range(13, -1, -1)]
    ck = {}

    def opt(mid, key, kind, why, subject, lines, to="", biz=""):
        """連絡案を1件つくる。to=None はAIへの指示（ToDoに入れる文）。"""
        oid = f"{mid}:{key}"
        ck[oid] = {"to": to, "subject": subject, "body": "\n".join(lines)}
        return {"id": oid, "kind": kind, "why": why, "to": to}
    labels14 = [f"{d:%m/%d}" for d in days14]

    # ---- AIエージェント: 今日動けば稼働中、7日以内なら待機、それ以外は休止。直近4件を省略せず出す
    agent_cards = ""
    all_counts = [0] * 14
    for a in (m for m in members.values() if m.get("kind") == "ai"):
        mine = [r for r in logs if r["agent"] == a["name"]]
        counts = [sum(1 for r in mine if day(r["date"]) == d) for d in days14]
        all_counts = [x + y for x, y in zip(all_counts, counts)]
        td, wk = counts[-1], sum(counts[-7:])
        last = day(mine[-1]["date"]) if mine else None
        state, cls = ("稼働中", "on") if td else (("待機", "idle") if last and (today - last).days < 7 else ("休止", "off"))
        works = "".join(f'<li><time>{e(r["date"][5:] if day(r["date"]) != today else "")}{e(r["time"])}</time>{e(clip(r["what"], 46))}</li>' for r in reversed(mine[-4:])) or "<li>記録なし</li>"
        slot = a.get("color", "s1")
        why = {"稼働中": "いま動いている。追加の依頼があるとき", "待機": "動きが止まっている。次の仕事を渡すとき", "休止": "7日以上動いていない。再開するとき"}[state]
        a_opts = [opt(a["_id"], "todo", "次の仕事を渡す", why, "", [
            f"# 【仕事の名前】", "", f"担当: {a['name']}", "",
            "## Goal（結果）", "【何ができていればよいか】", "",
            "## Context（読むもの）", "- 【参照するファイル・資料】", "",
            "## Always / Ask first / Never", "- Always: 読み取りと下書きの作成", "- Ask first: 元データの書き換え、外部への共有",
            "- Never: 送信・公開・削除・支払い", "",
            "## Done when（完了条件）", "- [ ] 【確かめられる条件】", "",
            "## Evidence（証拠）", "根拠にした資料の場所を示す。確認できていないことは『未確認』と書く。"], to=None)]
        agent_cards += (f'<div class="card {cls}"><div class="av">{agent_svg(a.get("glyph", "ring"), slot)}<i class="dot"></i></div>'
                        f'<b>{e(a["name"])}</b><span class="st">{state}</span><small>{e(a.get("role", ""))}</small>'
                        f'<span class="num">今日{td}件 ／ 週{wk}件</span>{day_bars(counts, labels14, f"var(--{slot})")}<small class="cap">直近14日の作業件数</small>'
                        f'<ul class="aw">{works}</ul>{contact_menu(a_opts)}</div>')

    # ---- メンバー（人）: 契約形態ごとに、見る数字を変える
    biz_short = {b["_id"]: b["name"].split("（")[0] for b in businesses}
    last_rep = {}
    for r in reports:
        d0 = day(r.get("date"))
        if d0 and (r.get("business") not in last_rep or d0 > last_rep[r["business"]]):
            last_rep[r["business"]] = d0
    humans = sorted((m for m in members.values() if m.get("kind") in HUMAN_KINDS), key=lambda m: (HUMAN_KINDS.index(m["kind"]), m["name"]))
    member_cards, waiting = "", 0
    for m in humans:
        kind, mid = m["kind"], m["_id"]
        ring, body, note = "on", "", ""
        h_opts, to_addr, fam, mname = [], m.get("email") or f"{m['_id']}@example.com", m["name"].split()[0] if " " in m["name"] else m["name"], m["name"]
        greet = [f"{mname}さん", "", "お疲れさまです。"]
        sign = ["", "よろしくお願いします。", "【自分の名前】"]
        if kind in ("partner", "outsource"):
            items = ""
            for dv_ in sorted((d for d in deliveries if d.get("member") == mid), key=lambda d: d.get("due") or "9999"):
                due = day(dv_.get("due"))
                status = dv_.get("status", "")
                over = bool(due and due < today and status not in ("検収待ち", "検収済み", "支払済み"))
                title = dv_.get("title", "")
                if status == "検収待ち":
                    h_opts.append(opt(mid, "thanks" + str(len(h_opts)), "納品のお礼と次の依頼", f"「{clip(title, 18)}」が検収待ち", f"【お礼と次のお願い】{title}", greet + [
                        f"「{title}」を納品いただき、ありがとうございました。内容を確認しています。",
                        f"検収の結果は{day(dv_.get('accept_due')):%m/%d}までにお伝えします。" if day(dv_.get("accept_due")) else "検収の結果は、あらためてお伝えします。",
                        f"お支払いは{day(dv_['pay_due']):%m/%d}の予定です。" if day(dv_.get("pay_due")) else "お支払いの時期は、あらためてお伝えします。",
                        "", "続けて、次の仕事をお願いできればと考えています。", "・【次にお願いしたい仕事】", "・【納期の希望】", "ご都合や難しい点があれば、遠慮なく教えてください。"] + sign, to_addr))
                    waiting += 1
                    ring = "idle" if ring == "on" else ring
                    flag = f'<b class="wait">検収待ち（検収期限 {day(dv_.get("accept_due")):%m/%d}）</b>' if day(dv_.get("accept_due")) else '<b class="wait">検収待ち</b>'
                elif over:
                    h_opts.append(opt(mid, "late" + str(len(h_opts)), "納期の確認", f"「{clip(title, 18)}」が納期を{(today - due).days}日超過", f"【ご状況の確認】{title}", greet + [
                        f"「{title}」の納期（{due:%m/%d}）を過ぎています。状況を教えてください。",
                        "間に合わない見込みの場合は、いつ頃になりそうか、また、先に進められない点があればお知らせください。", "こちらで調整できることがあれば対応します。"] + sign, to_addr))
                    ring = "warn"
                    flag = f'<b class="over">納期を{(today - due).days}日超過</b>'
                else:
                    if due and (due - today).days <= 3:
                        h_opts.append(opt(mid, "soon" + str(len(h_opts)), "納期前のひとこと", f"「{clip(title, 18)}」の納期まで{(due - today).days}日", f"【進み具合の確認】{title}", greet + [
                            f"「{title}」の納期（{due:%m/%d}）が近づいています。進み具合はいかがでしょうか。",
                            "途中のものでも見せていただければ、先に確認できます。困りごとがあれば教えてください。"] + sign, to_addr))
                    flag = f'<span>納期 {due:%m/%d}（あと{(due - today).days}日）</span>' if due else ""
                pay = f"・支払期日 {day(dv_['pay_due']):%m/%d}" if day(dv_.get("pay_due")) else ""
                amt = f"・¥{int(dv_['amount']):,}" if dv_.get("amount", "").isdigit() else ""
                items += f'<li><span class="tt">{e(dv_.get("title", ""))}</span><small>{e(status)}・{flag}{e(pay)}{e(amt)}</small></li>'
            body = f'<ul class="dls">{items or "<li><small>進行中の成果物なし</small></li>"}</ul>'
            if kind == "partner" and m.get("business") in last_rep:
                age_ = (today - last_rep[m["business"]]).days
                body += f'<small class="rp">報告 {last_rep[m["business"]]:%m/%d}（{age_}日前）</small>{meter(age_)}'
                if age_ > REPORT_DAYS:
                    h_opts.append(opt(mid, "report", "報告のお願い", f"報告が{age_}日届いていない", "【ご報告のお願い】", greet + [
                        f"{biz_short.get(m['business'], '')}の報告について、最後にいただいたのは{last_rep[m['business']]:%m/%d}です。",
                        "お手数ですが、直近の報告をお送りください。すでにお送りいただいている場合は、その旨だけ教えてください。"] + sign, to_addr))
                    ring = "warn"
            note = "成果物と納期で見る（時間は管理しない）"
        else:
            hrs = shifts.get(mid, {})
            counts = [hrs.get(d, 0) for d in days14]
            week, cap = sum(counts[-7:]), float(m.get("contract_hours") or 0)
            ring = "warn" if (cap and week > cap) else "on" if counts[-1] else "off"
            body = (f'{day_bars(counts, labels14, "var(--s1)", unit="時間")}<small class="cap">直近14日の勤務時間</small>'
                    f'<span class="num">今週 {week:g}時間{(" ／ 契約の上限 " + f"{cap:g}" + "時間") if cap else ""}</span>{cap_meter(week, cap) if cap else ""}')
            note = "シフトの実績で見る（給与・社会保険は専用のサービスへ）"
            if cap and week > cap:
                h_opts.append(opt(mid, "cap", "勤務時間の相談", f"今週{week:g}時間で契約の上限{cap:g}時間を超えている", "【今週の勤務時間について】", greet + [
                    f"今週の勤務時間が{week:g}時間となり、契約の上限（週{cap:g}時間）を超えています。",
                    "来週以降のシフトを調整したいので、ご都合を教えてください。体調や学業・家庭のご事情で変更したい点があれば、あわせて教えてください。"] + sign, to_addr))
            else:
                h_opts.append(opt(mid, "shift", "来週のシフトの確認", "いつでも（週に1回）", "【来週のシフトの確認】", greet + [
                    "来週のシフトについて、入れる日と時間を教えてください。", "希望の曜日・時間帯、お休みしたい日があれば、あわせてお知らせください。"] + sign, to_addr))
        member_cards += (f'<div class="card mem {ring}"><div class="av">{person_svg(m["name"])}<i class="dot"></i></div><b>{e(m["name"])}</b>'
                         f'<span class="kind">{e(KIND_LABEL[kind])}</span><small>{e(m.get("role", ""))}・{e(biz_short.get(m.get("business", ""), ""))}</small>'
                         f'<small class="how">{e(note)}</small>{body}{contact_menu(h_opts)}</div>')

    # ---- 事業カード
    cards, changes, late = [], [], []
    for b in businesses:
        bid = b["_id"]
        member = members.get(b.get("member", ""), {})
        rdays = sorted(day(r.get("date")) for r in reports if r.get("business") == bid and day(r.get("date")))
        last_report = rdays[-1] if rdays else None
        age = (today - last_report).days if last_report else None
        names = [x.strip() for x in b.get("metrics", "").split("|") if x.strip()]
        latest_metric = None
        tiles, table_cols, main_chart = "", {}, ""
        for i, name in enumerate(names):
            s = series_of(bid, name)
            table_cols[name] = s
            if not s:
                tiles += f'<div class="tl"><span>{e(name)}</span><b class="miss">未取得</b><small>metrics.csv に記録なし</small></div>'
                continue
            ds = sorted(s)
            d1, v1 = ds[-1], s[ds[-1]]
            latest_metric = max(latest_metric or d1, d1)
            prev = s[ds[-2]] if len(ds) > 1 else None
            delta = ""
            if prev is not None:
                diff = v1 - prev
                pct = (diff / prev * 100) if prev else None
                arrow = "▲" if diff > 0 else "▼" if diff < 0 else "▶"
                cls = "up" if diff > 0 else "down" if diff < 0 else "flat"
                delta = f'<span class="dl {cls}">{arrow} {diff:+g}（前週{fmt(prev)}）</span>'
                changes.append((pct if pct is not None else (999 if diff > 0 else 0), f'{b["name"].split("（")[0]} ・ {name}', diff, pct, d1))
            note = f"{d1:%m/%d}の週" + ("（以降の数字が届いていません）" if d1 < axis[-1] else "")
            if i == 0:
                main_chart = f'<div class="main"><div class="mh"><span>{e(name)}</span><b>{fmt(v1)}</b>{delta}</div>{line_chart(name, s, axis)}<small>{e(note)}</small></div>'
            else:
                tiles += f'<div class="tl"><span>{e(name)}</span><b>{fmt(v1)}</b>{delta}{spark(s, axis)}<small>{e(note)}</small></div>'
        if last_report is None:
            pill, pcls = "報告なし", "off"
        elif age > REPORT_DAYS:
            pill, pcls = f"報告が{age}日止まっています", "bad"
            late.append(b["name"].split("（")[0])
        else:
            pill, pcls = "週次どおり", "ok"
        updated = max([x for x in (last_report, latest_metric) if x], default=None)
        ring = {"ok": "on", "bad": "warn"}.get(pcls, "off")
        recent = [r for r in logs if r.get("business") == bid]
        recent_txt = f'{recent[-1]["date"][5:]} {clip(recent[-1]["what"], 50)}' if recent else "AIの作業記録なし"
        rows_tbl = "".join(f'<tr><td>{d:%m/%d}</td>' + "".join(f'<td>{fmt(table_cols[n][d]) if d in table_cols[n] else "—"}</td>' for n in names) + "</tr>" for d in reversed(axis))
        table = f'<details class="tv"><summary>表で見る（12週）</summary><table><tr><th>週の終わり</th>{"".join(f"<th>{e(n)}</th>" for n in names)}</tr>{rows_tbl}</table></details>'
        upd_cls = "bad" if (updated is None or (today - updated).days > REPORT_DAYS) else "ok"
        upd_txt = f"最終更新 {updated:%m/%d}（{(today - updated).days}日前）" if updated else "更新の記録なし"
        rep_txt = f"報告 {last_report:%m/%d}（{age}日前・目安{REPORT_DAYS}日）" if last_report else "報告 なし"
        mark = "✕ " if pcls == "bad" else "● " if pcls == "ok" else ""
        team = "".join(f'<span class="tm">{e(m["name"])}<small>{e(KIND_LABEL[m["kind"]])}</small></span>' for m in humans if m.get("business") == bid)
        cards.append((updated or dt.date.min,
                      f'<div class="cc"><div class="cc-h"><div class="who {ring}"><div class="av">{person_svg(member.get("name", bid))}<i class="dot"></i></div></div>'
                      f'<div class="cc-t"><b>{e(b["name"])}</b><span class="pill {pcls}">{mark}{e(pill)}</span><small class="upd {upd_cls}">{upd_txt}</small>'
                      f'<div class="cc-m">担当 <b>{e(member.get("name", "未設定"))}</b>（{e(member.get("role", ""))}）</div></div></div>'
                      f'<div class="team"><small>メンバー</small>{team or "<small>なし</small>"}</div>'
                      f'{main_chart}<div class="tls">{tiles}</div>'
                      f'<div class="rep"><div><small>報告の届いた週（直近8週）</small>{report_dots(rdays, axis)}</div>'
                      f'<div class="rm"><small>{rep_txt}</small>{meter(age)}</div></div>'
                      f'<small class="lastact">AIの直近の作業: {e(recent_txt)}</small>{table}</div>'))
    cards.sort(key=lambda x: x[0], reverse=True)   # 更新が新しい順だけ（固定しない）

    # ---- 今週の変化（前週比・左右に伸びる棒。増=青／減=赤／中央は灰）
    changes.sort(key=lambda c: c[0], reverse=True)
    dv = ""
    for key, label, diff, p, d1 in changes:
        shown = "新規" if p is None else f"{p:+.0f}%".replace("-", "−")
        w = min(abs(p) if p is not None else 100, 100) / 2
        side = "pos" if diff > 0 else "neg" if diff < 0 else "zero"
        bar = f'<b class="{side}" style="width:{w:.1f}%"></b>' if side != "zero" else ""
        stale = f' <small>※{d1:%m/%d}の週まで</small>' if d1 < axis[-1] else ""
        dv += f'<div class="dv"><span class="dlb">{e(label)}{stale}</span><div class="dbar"><i></i>{bar}</div><span class="dvv">{e(shown)}</span></div>'

    # ---- 自分にボールがある行だけ（相手の宿題は載せない）
    mine = [t for t in tasks if t.get("owner") == "自分" and t.get("status") in ("todo", "doing")]
    mine.sort(key=lambda t: t.get("due") or "9999")
    todo = "".join(
        f'<li class="{"r0" if (day(t.get("due")) and day(t.get("due")) < today) else "r1"}"><b>'
        f'{("期限" + t["due"][5:] + ("・" + str((today - day(t["due"])).days) + "日超過" if day(t["due"]) < today else "")) if day(t.get("due")) else "期限なし"}</b>'
        f'<span>{e(t.get("title", ""))}</span><small>{e(t.get("business", ""))}</small></li>' for t in mine) or "<li class='none'>自分が動く件はありません</li>"
    overdue = sum(1 for t in mine if day(t.get("due")) and day(t.get("due")) < today)
    ck_json = json.dumps(ck, ensure_ascii=False).replace("</", "<\\/")
    return PAGE.format(
        css=CSS, title=e(company.get("name", "")), today=today, js=f"window.CK={ck_json};" + JS,
        today_n=all_counts[-1], bars=day_bars(all_counts, labels14, "var(--s1)", bw=9, gap=3, h=30), total14=sum(all_counts),
        late=len(late), late_names=e("・".join(late) or "なし"), mine=len(mine), overdue=overdue, waiting=waiting, members=member_cards,
        agents=agent_cards, dv=dv or '<div class="empty">前週と比べられる数字がありません</div>', cards="".join(h for _, h in cards), todo=todo)


PAGE = """<!doctype html>
<html lang="ja"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="robots" content="noindex,nofollow"><meta name="color-scheme" content="light dark">
<title>{title} 事業ダッシュボード</title><style>{css}</style></head><body><main>
<header><h1>{title}　事業ダッシュボード<small>{today}時点の写し（読み取り専用）／ 元データから生成</small></h1></header>
<section class="kpis">
<div class="kpi"><span>今日のAIの動き</span><b>{today_n}<em>件</em></b><div>{bars}</div><small>直近14日の合計 {total14}件</small></div>
<div class="kpi"><span>報告が遅れている事業</span><b class="warnv">{late}<em>事業</em></b><small>{late_names}</small></div>
<div class="kpi"><span>自分が動く件</span><b>{mine}<em>件</em></b><small>うち期限超過 {overdue}件・検収待ち {waiting}件</small></div>
</section>
<h2>今週の変化<em>前週からの増減（青＝増・赤＝減）。止まった数字は注記つき</em></h2>
<section class="panel dvs">{dv}</section>
<h2>AIエージェントの動き<em>今日動けば稼働中。直近4件を省略せず表示</em></h2><div class="grid">{agents}</div>
<h2>メンバー（人）<em>契約形態ごとに、見る数字を変える。業務委託・外注は納期と検収、アルバイト・パートはシフトの実績。点数や順位は作りません</em></h2><div class="grid">{members}</div>
<h2>各事業の状況<em>更新が新しい順（固定なし）。顔は担当メンバー（縁の色は報告の鮮度。人の評価ではありません）</em></h2>
<div class="cc-grid">{cards}</div>
<h2>いま対応が要るもの（参考）<em>ボールが自分にある行だけ</em></h2><ul class="al">{todo}</ul>
<footer>このページは生成物です。直すときは元データ（members・reports・metrics・tasks・actionlog）を直して、build_dashboard.py を実行し直します。限定情報を含むときは、本人だけが見られる場所に置いてください。</footer>
</main><div id="tip" role="status"></div><script>{js}</script></body></html>
"""

CSS = """
:root{color-scheme:light;--surface:#fcfcfb;--plane:#f4f3ef;--ink:#0b0b0b;--ink2:#52514e;--muted:#898781;--grid:#e1e0d9;--axis:#c3c2b7;--ring:rgba(11,11,11,.10);
--s1:#2a78d6;--s2:#eb6834;--s3:#1baf7a;--s4:#eda100;--pos:#2a78d6;--neg:#e34948;--good:#0ca30c;--bad:#d03b3b;--goodtext:#006300;--badtext:#b42a2a;--shadow:0 1px 2px rgba(11,11,11,.06)}
@media (prefers-color-scheme:dark){:root{color-scheme:dark;--surface:#1a1a19;--plane:#0d0d0d;--ink:#fff;--ink2:#c3c2b7;--grid:#2c2c2a;--axis:#383835;--ring:rgba(255,255,255,.10);
--s1:#3987e5;--s2:#d95926;--s3:#199e70;--s4:#c98500;--pos:#3987e5;--neg:#e66767;--goodtext:#0ca30c;--badtext:#e66767;--shadow:none}}
*{box-sizing:border-box}body{margin:0;background:var(--plane);color:var(--ink);font:14px/1.55 system-ui,-apple-system,"Segoe UI","Hiragino Sans","Yu Gothic",sans-serif}
main{max-width:1120px;margin:0 auto;padding:18px 16px 44px}h1{font-size:21px;margin:6px 0 2px}h1 small{display:block;color:var(--muted);font-size:12px;font-weight:400}
h2{font-size:13px;margin:26px 0 8px;color:var(--ink2);letter-spacing:.12em}h2 em{font-style:normal;font-weight:400;letter-spacing:0;margin-left:8px;color:var(--muted)}
small{color:var(--muted);font-size:11.5px}.card,.cc,.panel,.kpi{background:var(--surface);border:1px solid var(--ring);border-radius:14px;box-shadow:var(--shadow)}
.kpis{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:10px}.kpi{padding:12px 14px;display:grid;gap:2px;align-content:start}
.kpi>span{font-size:12px;color:var(--ink2)}.kpi b{font-size:34px;line-height:1.15;font-weight:600}.kpi b em{font-style:normal;font-size:13px;font-weight:400;color:var(--ink2);margin-left:4px}.kpi .warnv{color:var(--badtext)}
.panel{padding:12px 16px}.dvs{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:2px 28px}
.dv{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(90px,1fr) 52px;gap:8px;align-items:center;padding:3px 0;font-size:12.5px}.dlb{color:var(--ink2);line-height:1.3}
.dbar{position:relative;height:10px}.dbar i{position:absolute;left:50%;top:-2px;bottom:-2px;width:1px;background:var(--axis)}
.dbar b{position:absolute;top:0;height:10px}.dbar b.pos{left:50%;background:var(--pos);border-radius:0 4px 4px 0}.dbar b.neg{right:50%;background:var(--neg);border-radius:4px 0 0 4px}
.dvv{text-align:right;font-weight:600}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(215px,1fr));gap:10px;align-items:start}
.card{padding:12px 10px;display:grid;justify-items:center;gap:3px;text-align:center;align-content:start}
.av{position:relative;width:52px;height:52px}.av svg{width:100%;height:100%;border-radius:50%;display:block;border:3px solid var(--axis)}
.dot{position:absolute;right:0;bottom:2px;width:12px;height:12px;border-radius:50%;background:var(--muted);border:2px solid var(--surface)}
.on .av svg{border-color:var(--good)}.on .dot{background:var(--good)}.idle .av svg{border-color:var(--s4)}.idle .dot{background:var(--s4)}.warn .av svg{border-color:var(--bad)}.warn .dot{background:var(--bad)}
.st{font-size:11px;padding:1px 9px;border-radius:99px;background:var(--plane);color:var(--ink2)}.num{font-size:12px}.cap{margin-top:-2px}
.aw{list-style:none;margin:6px 0 0;padding:0;text-align:left;width:100%}.aw li{font-size:11.5px;color:var(--ink2);padding:3px 0;border-top:1px solid var(--grid)}.aw time{color:var(--muted);margin-right:6px}
.kind{font-size:11px;padding:1px 9px;border-radius:99px;border:1px solid var(--axis);color:var(--ink2)}.how{margin-top:1px}
.mem .dls{list-style:none;margin:6px 0 0;padding:0;text-align:left;width:100%}.dls li{padding:5px 0;border-top:1px solid var(--grid);display:grid;gap:1px}.dls .tt{font-size:12.5px}
.dls small b.wait{color:var(--s1);font-weight:600}.dls small b.over{color:var(--badtext);font-weight:600}.mem .meter{width:100%;margin-top:2px}.rp{width:100%;text-align:left;margin-top:4px}
.team{display:flex;flex-wrap:wrap;gap:4px 8px;align-items:center}.tm{font-size:12px;padding:1px 8px;border-radius:99px;background:var(--plane);color:var(--ink2)}.tm small{margin-left:5px}
details.cmd{margin-top:6px;width:100%;text-align:left}details.cmd>summary{cursor:pointer;font-size:12px;color:var(--s1)}
.px{background:var(--plane);border-radius:8px;padding:7px 9px;margin-top:6px;display:grid;gap:2px}.px b{font-size:12px;line-height:1.4}
.btns{display:flex;gap:6px;flex-wrap:wrap;margin-top:4px}.btns button{font:inherit;font-size:11.5px;padding:4px 10px;border-radius:8px;border:1px solid var(--s1);background:var(--s1);color:#fff;cursor:pointer}
.btns button.ghost{background:transparent;color:var(--ink2);border-color:var(--axis)}
svg.bars{margin-top:4px;overflow:visible}svg.spark{display:block;margin-top:2px;width:108px;height:30px}
.cc-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(470px,1fr));gap:12px}.cc{padding:14px 16px;display:grid;gap:10px;align-content:start}
.cc-h{display:flex;gap:12px;align-items:center}.cc-t{display:grid;gap:1px;min-width:0}.cc-t>b{font-size:16px}.cc-m{font-size:12px;color:var(--ink2)}.upd.ok{color:var(--goodtext)}.upd.bad{color:var(--badtext)}
.pill{display:inline-block;width:fit-content;font-size:11.5px;padding:1px 10px;border-radius:99px;background:var(--plane);font-weight:600;color:var(--ink2)}.pill.ok{color:var(--goodtext)}.pill.bad{color:var(--badtext)}
.main .mh{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}.main .mh span{font-size:12px;color:var(--ink2)}.main .mh b{font-size:30px;font-weight:600;line-height:1.1}
.dl{font-size:12px;font-weight:600}.dl.up{color:var(--goodtext)}.dl.down{color:var(--badtext)}.dl.flat{color:var(--muted)}
svg.lc{width:100%;height:auto;display:block;touch-action:pan-y}svg.lc .ax{font-size:10px;fill:var(--muted)}svg.lc .endv{font-size:11px;font-weight:600;fill:var(--ink)}
.tls{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px}.tl{background:var(--plane);border-radius:10px;padding:8px 10px;display:grid;gap:1px;align-content:start}
.tl>span{font-size:11.5px;color:var(--ink2)}.tl>b{font-size:22px;font-weight:600;line-height:1.2}.tl .miss{color:var(--badtext);font-size:16px}
.rep{display:flex;gap:18px;flex-wrap:wrap;align-items:center}.rep>div{display:grid;gap:3px}.rm{flex:1;min-width:190px}
.meter{position:relative;height:8px;border-radius:4px;background:var(--plane)}.meter i{position:absolute;left:0;top:0;bottom:0;border-radius:4px}.meter i.ok{background:var(--good)}.meter i.bad{background:var(--bad)}
.meter b{position:absolute;left:50%;top:-3px;bottom:-3px;width:2px;background:var(--ink2);opacity:.55}
.lastact{color:var(--muted)}details.tv summary{cursor:pointer;font-size:12px;color:var(--ink2)}
details.tv table{border-collapse:collapse;width:100%;margin-top:6px;font-size:12px}details.tv th,details.tv td{padding:3px 8px;border-bottom:1px solid var(--grid);text-align:right}details.tv th:first-child,details.tv td:first-child{text-align:left}details.tv th{color:var(--muted);font-weight:500}
ul{list-style:none;margin:0;padding:0}.al li{background:var(--surface);border:1px solid var(--ring);border-left:4px solid var(--muted);border-radius:10px;padding:8px 12px;margin:7px 0;display:grid;gap:1px}
.al li.r0{border-left-color:var(--bad)}.al li.r1{border-left-color:var(--s4)}.al li b{font-size:12px;color:var(--ink2)}.al li.none{color:var(--muted);font-size:12px}
.empty{color:var(--muted);font-size:12px;padding:10px 0}footer{margin-top:28px;color:var(--muted);font-size:11.5px}
#tip{position:fixed;z-index:9;pointer-events:none;background:var(--surface);color:var(--ink);border:1px solid var(--ring);border-radius:8px;padding:6px 10px;font-size:12px;box-shadow:0 4px 14px rgba(0,0,0,.18);opacity:0;transition:opacity .08s}
#tip b{font-size:15px;display:block}#tip span{color:var(--ink2)}#tip.on{opacity:1}
@media(max-width:700px){.kpis{gap:6px}.kpi{padding:8px 9px}.kpi b{font-size:26px}.kpi small{font-size:10.5px}.dvs{grid-template-columns:1fr}.cc-grid{grid-template-columns:1fr}.grid{grid-template-columns:repeat(2,1fr)}.card{padding:8px 6px}.cc{padding:12px}}
@media(prefers-reduced-motion:reduce){#tip{transition:none}}
"""

JS = """
(function(){var tip=document.getElementById('tip');
document.querySelectorAll('svg.lc').forEach(function(svg){
  var pts=JSON.parse(svg.dataset.pts),W=+svg.dataset.w,xh=svg.querySelector('.xh'),hd=svg.querySelector('.hd'),name=svg.dataset.name;
  function show(ev){var r=svg.getBoundingClientRect(),x=(ev.clientX-r.left)/r.width*W,best=0;
    pts.forEach(function(p,i){if(Math.abs(p[0]-x)<Math.abs(pts[best][0]-x))best=i;});
    var p=pts[best],prev=pts[best-1];
    xh.setAttribute('x1',p[0]);xh.setAttribute('x2',p[0]);xh.setAttribute('visibility','visible');
    hd.setAttribute('cx',p[0]);hd.setAttribute('cy',p[1]);hd.setAttribute('visibility','visible');
    tip.textContent='';var b=document.createElement('b');b.textContent=p[3].toLocaleString();var s=document.createElement('span');
    s.textContent=name+'・'+p[2]+'の週'+(prev?'（前週 '+(p[3]-prev[3]>=0?'+':'')+(p[3]-prev[3])+'）':'');tip.appendChild(b);tip.appendChild(s);
    tip.style.left=Math.min(ev.clientX+14,window.innerWidth-190)+'px';tip.style.top=(ev.clientY+14)+'px';tip.className='on';}
  function hide(){xh.setAttribute('visibility','hidden');hd.setAttribute('visibility','hidden');tip.className='';}
  svg.addEventListener('pointermove',show);svg.addEventListener('pointerdown',show);svg.addEventListener('pointerleave',hide);});
function say(m,ev){tip.textContent=m;tip.style.left=Math.min(ev.clientX+14,window.innerWidth-260)+'px';tip.style.top=(ev.clientY+14)+'px';tip.className='on';clearTimeout(window._t);window._t=setTimeout(function(){tip.className='';},3200);}
document.addEventListener('click',function(ev){var b=ev.target.closest('button[data-ck]');if(!b)return;var o=window.CK[b.dataset.id];if(!o)return;
  if(b.dataset.ck==='mail'){window.open('https://mail.google.com/mail/?view=cm&fs=1&to='+encodeURIComponent(o.to)+'&su='+encodeURIComponent(o.subject)+'&body='+encodeURIComponent(o.body),'_blank','noopener');say('メールの作成画面を開きました。送信は、本文を読んでから自分で行います',ev);return;}
  var done=function(){say('コピーしました',ev);};
  if(navigator.clipboard){navigator.clipboard.writeText(o.body).then(done,done);}else{var a=document.createElement('textarea');a.value=o.body;document.body.appendChild(a);a.select();document.execCommand('copy');a.remove();done();}});})();
"""


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("folder", nargs="?", default=str(HERE / "sample"))
    ap.add_argument("--today", help="基準日 YYYY-MM-DD（省略時は company.md の as_of）")
    ap.add_argument("--out", help="出力先（省略時は sample の隣、または folder/dashboard.html）")
    args = ap.parse_args()
    folder = Path(args.folder)
    out = Path(args.out) if args.out else (folder.parent / "dashboard_sample.html" if folder.name == "sample" else folder / "dashboard.html")
    out.write_text(build(folder, day(args.today) if args.today else None), encoding="utf-8")
    print(f"生成: {out}")
