diff --git a/agents/stock/workspace/scripts/behive_web.py b/agents/stock/workspace/scripts/behive_web.py
index b16c7be..5083e28 100644
--- a/agents/stock/workspace/scripts/behive_web.py
+++ b/agents/stock/workspace/scripts/behive_web.py
@@ -3152,7 +3152,31 @@ def _aggregate_cash_flows_by_unit(daily_cf: dict[str, dict], aggregated_dates: l
return out
-def _load_net_worth_series(owner: str, days: int = NET_WORTH_CHART_DAYS, unit: str = 'day', with_prev_baseline: bool = False):
+def _available_chart_periods() -> dict:
+ """스냅샷에 데이터가 있는 연도·연월 목록 (연도별/월별 드롭다운 채움용). 최신이 앞.
+ 반환: {'years': ['2026','2025',...], 'months': {'2026': ['06','05',...], ...}}"""
+ empty = {'years': [], 'months': {}}
+ if not SNAPSHOT_FILE.exists():
+ return empty
+ try:
+ snap = json.loads(SNAPSHOT_FILE.read_text())
+ except Exception:
+ return empty
+ years: set = set()
+ months: dict = {}
+ for date_key in snap.keys():
+ if len(date_key) < 7 or date_key[4] != '-':
+ continue
+ y, m = date_key[:4], date_key[5:7]
+ years.add(y)
+ months.setdefault(y, set()).add(m)
+ return {
+ 'years': sorted(years, reverse=True),
+ 'months': {y: sorted(ms, reverse=True) for y, ms in months.items()},
+ }
+
+
+def _load_net_worth_series(owner: str, days: int = NET_WORTH_CHART_DAYS, unit: str = 'day', with_prev_baseline: bool = False, period: str = ''):
"""portfolio_daily_snapshot에서 owner별 순자산 시계열을 추출.
각 날짜의 net_worth = sum(holdings[*].eval_value) + deposit. 누락 필드는 0으로 처리.
unit으로 일/주/월/연 단위 집계 (각 구간 마지막 영업일 점). 기간(days) 필터는 단위 집계 후 적용.
@@ -3188,7 +3212,10 @@ def _load_net_worth_series(owner: str, days: int = NET_WORTH_CHART_DAYS, unit: s
# filtered 에 baseline 점이 prepend 되었으면 그게 series[0] = PnL baseline 역할 → prev_baseline 별도 안 넘김
baseline_prepended = False
- if days == -2: # 이번달 — KST 기준 이번 달 1일 이후만
+ if period: # 절대 선택 — 'YYYY'(연도별·월별집계) 또는 'YYYY-MM'(월별·일별). caller가 unit을 맞춰 호출.
+ range_start = (period + '-01-01') if len(period) == 4 else (period + '-01')
+ filtered = [(d, v) for d, v in aggregated if d.startswith(period)]
+ elif days == -2: # 이번달 — KST 기준 이번 달 1일 이후만
prefix = datetime.now(KST).strftime('%Y-%m')
range_start = f'{prefix}-01'
filtered = [(d, v) for d, v in aggregated if d.startswith(prefix)]
@@ -4954,6 +4981,7 @@ table.market-adr td.adr-breakdown { font-size: 11px; color: #8b8f9a; }
/* 차트 컨트롤 — 자산정보 탭 상단의 공통 기간·단위 select 묶음. */
.chart-controls { display: flex; gap: 10px; margin: 10px 14px 4px; flex-wrap: wrap; }
.chart-controls .cc-field { display: flex; align-items: center; gap: 6px; }
+.chart-controls .cc-ym[hidden] { display: none; } /* 연도별/월별 아닐 때 연·월 드롭다운 숨김 ([hidden] UA 스타일이 flex에 가려지지 않도록 명시) */
.chart-controls .cc-label { color: #8b8f9a; font-size: 11px; }
.chart-controls .nw-select {
appearance: none; -webkit-appearance: none;
@@ -5111,17 +5139,42 @@ def _owner_full_title(owner: str) -> str:
return OWNER_LABELS.get(owner, f'{owner} 자산')
-def _render_chart_controls(selected_days: int, selected_unit: str, selected_mode: str = NET_WORTH_CHART_MODE) -> str:
- """차트용 공통 컨트롤 — 기간 / 단위 / 모드(순자산·손익누적·기간손익) select 3개.
- 자산정보 탭 상단에 1세트만 노출, 본인·가희 차트 둘 다 동시 컨트롤.
+def _render_chart_controls(selected_days: int, selected_unit: str, selected_mode: str = NET_WORTH_CHART_MODE, chart_ym: str = '', avail: dict | None = None) -> str:
+ """차트용 공통 컨트롤 — 기간 / 단위 / 모드(순자산·손익누적·기간손익) select.
+ 기간에 '연도별'(연도 선택→월별집계)·'월별'(연도+월 선택→일별) 추가. period(chart_ym) 활성 시 단위는 강제·비활성.
의미없는 단위(이번주+월별 등)는 disabled."""
+ avail = avail or {'years': [], 'months': {}}
+ period = (chart_ym or '').strip()
+ is_year = len(period) == 4
+ is_month = len(period) == 7
+ period_active = is_year or is_month
+ years = avail.get('years') or []
+ months_map = avail.get('months') or {}
+
allowed_units = NET_WORTH_ALLOWED_UNITS_BY_DAYS.get(
selected_days, tuple(u for _, u in NET_WORTH_UNIT_PRESETS)
)
range_opts = ''.join(
- f''
+ f''
for lbl, d in NET_WORTH_CHART_PRESETS
)
+ range_opts += f''
+ range_opts += f''
+
+ cur_year = period[:4] if period_active else (years[0] if years else '')
+ cur_month = period[5:7] if is_month else ''
+ year_opts = ''.join(
+ f''
+ for y in years
+ )
+ month_opts = ''.join(
+ f''
+ for m in months_map.get(cur_year, [])
+ )
+ _yhide = '' if period_active else ' hidden'
+ _mhide = '' if is_month else ' hidden'
+ months_json = html.escape(json.dumps(months_map, ensure_ascii=False), quote=True)
+
unit_opts = ''.join(
f''
for lbl, m in NET_WORTH_MODE_PRESETS
)
+ _unit_dis = ' disabled' if period_active else ''
return (
- '
'
+ f'
'
f''
- # 단위·모드 한 cc-field 묶음 — 좁은 화면 줄바꿈 방지 + 시각적 인접 배치.
+ # 연도별/월별 선택 시 노출되는 연·월 드롭다운 (데이터 있는 기간만).
+ f''
+ f''
+ # 단위·모드 한 cc-field 묶음. period 활성 시 단위 강제(disabled).
f''
'
'
)
@@ -5685,7 +5744,7 @@ def _render_adr_trend_card(days: int = ADR_TREND_DAYS) -> str:
)
-def _render_summary_panel(ordered_owners: list[str], owner_data: dict, balances: dict | None = None, journal_by_label: dict | None = None, chart_days: int = NET_WORTH_CHART_DAYS, chart_unit: str = NET_WORTH_CHART_UNIT, chart_mode: str = NET_WORTH_CHART_MODE, adr_days: int = ADR_TREND_DAYS) -> str:
+def _render_summary_panel(ordered_owners: list[str], owner_data: dict, balances: dict | None = None, journal_by_label: dict | None = None, chart_days: int = NET_WORTH_CHART_DAYS, chart_unit: str = NET_WORTH_CHART_UNIT, chart_mode: str = NET_WORTH_CHART_MODE, adr_days: int = ADR_TREND_DAYS, chart_ym: str = '') -> str:
"""자산정보 탭 — sub-tab(자산보기/차트보기/시장정보)로 분리.
- 자산보기(기본): owner별 compact KPI 카드 스택 (합산 / 각계좌별도 토글)
- 차트보기: 기간·단위·모드 select + owner별 차트 (순자산 / 손익누적 토글)
@@ -5697,9 +5756,13 @@ def _render_summary_panel(ordered_owners: list[str], owner_data: dict, balances:
balances = balances or {}
journal_by_label = journal_by_label or {}
+ # 연도별/월별 절대 선택 — chart_ym('YYYY'=연도별·월별집계 / 'YYYY-MM'=월별·일별) 있으면 단위 강제 + period 필터.
+ period = (chart_ym or '').strip()
+ eff_unit = ('day' if len(period) > 4 else 'month') if period else chart_unit
+
consolidated_parts: list[str] = []
by_account_parts: list[str] = []
- chart_parts: list[str] = [_render_chart_controls(chart_days, chart_unit, chart_mode)]
+ chart_parts: list[str] = [_render_chart_controls(chart_days, eff_unit, chart_mode, chart_ym=period, avail=_available_chart_periods())]
for owner in ordered_owners:
d = owner_data.get(owner)
if not d:
@@ -5718,11 +5781,11 @@ def _render_summary_panel(ordered_owners: list[str], owner_data: dict, balances:
has_prev_snap=has_prev_snap,
label_disp=label_disp,
))
- chart_series, prev_baseline = _load_net_worth_series(owner, days=chart_days, unit=chart_unit, with_prev_baseline=True)
+ chart_series, prev_baseline = _load_net_worth_series(owner, days=chart_days, unit=eff_unit, with_prev_baseline=True, period=period)
if chart_series:
# 스냅샷에 누적된 일자별 입출금 → unit 집계에 맞춰 sum.
daily_cf = _load_cash_flow_by_date(owner)
- cf_by_date = _aggregate_cash_flows_by_unit(daily_cf, [d_ for d_, _ in chart_series], chart_unit) if daily_cf else {}
+ cf_by_date = _aggregate_cash_flows_by_unit(daily_cf, [d_ for d_, _ in chart_series], eff_unit) if daily_cf else {}
# 오늘 live 포인트용 — snapshot에 아직 안 들어간 오늘 입출금. owner_data에 이미 합산되어 있음.
today_cf = None
if d.get('cash_in', 0) or d.get('cash_out', 0):
@@ -5748,7 +5811,7 @@ def _render_summary_panel(ordered_owners: list[str], owner_data: dict, balances:
pnl_series,
current_net=current_pnl,
selected_days=chart_days,
- selected_unit=chart_unit,
+ selected_unit=eff_unit,
cash_flow_by_date=None, # pnl·period 모두 cf 영향 이미 제거 — 마커 숨김
today_cash_flow=None,
mode=chart_mode,
@@ -5761,7 +5824,7 @@ def _render_summary_panel(ordered_owners: list[str], owner_data: dict, balances:
chart_series,
current_net=d.get('total_net'),
selected_days=chart_days,
- selected_unit=chart_unit,
+ selected_unit=eff_unit,
cash_flow_by_date=cf_by_date,
today_cash_flow=today_cf,
mode='net',
@@ -6190,7 +6253,7 @@ def _render_interests_panel(cards: list[dict]) -> tuple[str, int]:
TAB_KEY_TO_OWNER = {'self': '본인', 'gahee': '가희'}
-def _build_panels_payload(owner: str | None = None, chart_days: int = NET_WORTH_CHART_DAYS, chart_unit: str = NET_WORTH_CHART_UNIT, chart_mode: str = NET_WORTH_CHART_MODE, adr_days: int = ADR_TREND_DAYS) -> dict:
+def _build_panels_payload(owner: str | None = None, chart_days: int = NET_WORTH_CHART_DAYS, chart_unit: str = NET_WORTH_CHART_UNIT, chart_mode: str = NET_WORTH_CHART_MODE, adr_days: int = ADR_TREND_DAYS, chart_ym: str = '') -> dict:
"""fetch_all_data + 탭별 패널 HTML 생성. /api/panels와 캐시의 단위.
owner 지정 시: 해당 owner 계좌만 호출하고 응답 tabs는 owner 탭 하나만 (summary·wl 제외).
클라이언트가 partial swap으로 사용 — 보지 않는 owner 호출 폭주 방지.
@@ -6220,7 +6283,7 @@ def _build_panels_payload(owner: str | None = None, chart_days: int = NET_WORTH_
# 전체 — 모든 탭 채움. 모두 동일 fetched_at.
wl_body, wl_count = _render_watchlist_panel(wl_cards)
in_body, in_count = _render_interests_panel(in_cards)
- summary_body = _render_summary_panel(ordered_owners, owner_data, balances, journal_by_label, chart_days=chart_days, chart_unit=chart_unit, chart_mode=chart_mode, adr_days=adr_days)
+ summary_body = _render_summary_panel(ordered_owners, owner_data, balances, journal_by_label, chart_days=chart_days, chart_unit=chart_unit, chart_mode=chart_mode, adr_days=adr_days, chart_ym=chart_ym)
tabs.append({'id': 'tab-summary', 'label': '자산정보', 'html': summary_body, 'fetched_at': fetched_hms})
for o in ordered_owners:
d = owner_data[o]
@@ -6487,10 +6550,21 @@ def render_html() -> str:
'if(v!==7&&v!==30&&v!==90&&v!==180&&v!==365&&v!==-1)return null;'
'return v;'
'}'
+ # chart_ym — 연도별('YYYY')/월별('YYYY-MM') 절대 선택. 빈값이면 프리셋(chartDays) 사용.
+ 'function chartYM(){'
+ 'var v=localStorage.getItem("behive.chartYM")||"";'
+ 'var p=v.split("-");'
+ 'if(p.length===1&&/^\\d{4}$/.test(p[0]))return v;'
+ 'if(p.length===2&&/^\\d{4}$/.test(p[0])&&/^\\d{2}$/.test(p[1]))return v;'
+ 'return "";'
+ '}'
+ 'function setYM(v){try{if(v)localStorage.setItem("behive.chartYM",v);else localStorage.removeItem("behive.chartYM");}catch(_){}}'
'window.__behive_chart_days=chartDays;'
'window.__behive_chart_unit=chartUnit;'
'window.__behive_chart_mode=chartMode;'
'window.__behive_adr_days=adrDays;'
+ 'window.__behive_chart_ym=chartYM;'
+ 'window.__behive_set_ym=setYM;'
'function load(owner,fresh){'
# 5분 이상 stale 면 전체로 승격하되, 화면은 클리어하지 않고 기존 내용 유지 → fetch 완료 시 교체(swap).
'if(lastLoadAt&&(Date.now()-lastLoadAt)>STALE_MS){owner=null;}'
@@ -6501,6 +6575,7 @@ def render_html() -> str:
'var cu=chartUnit();if(cu!==null)qs.push("chart_unit="+cu);'
'var cm=chartMode();if(cm!==null)qs.push("chart_mode="+cm);'
'var ad=adrDays();if(ad!==null)qs.push("adr_days="+ad);'
+ 'var ym=chartYM();if(ym)qs.push("chart_ym="+encodeURIComponent(ym));'
'var url="/api/panels"+(qs.length?("?"+qs.join("&")):"");'
'var pr=fetch(url,{cache:"no-store",credentials:"same-origin"})'
'.then(function(r){if(!r.ok)throw new Error("HTTP "+r.status);return r.json();})'
@@ -6849,16 +6924,34 @@ def render_html() -> str:
'document.addEventListener("change",function(e){'
'var sel=e.target;'
'if(!sel||sel.tagName!=="SELECT"||!sel.classList||!sel.classList.contains("nw-select"))return;'
+ 'function ymFirstMonth(y){var cc=document.querySelector(".chart-controls");try{var mp=JSON.parse(cc.getAttribute("data-ym-months")||"{}");var a=mp[y]||[];return a.length?a[0]:"";}catch(_){return "";}}'
'if(sel.hasAttribute("data-nw-range")){'
- 'try{localStorage.setItem("behive.chartDaysV2",sel.value);}catch(_){}'
- # 기간 변경 시 현재 단위가 비허용이면 첫 허용으로 자동 보정 (서버 sanity와 일치).
- 'var allowedMap={"-3":["day","week"],"-2":["day","week","month"],"-4":["day","week","month","year"],"-1":["day","week","month","year"]};'
- 'var allowed=allowedMap[sel.value]||["day","week","month","year"];'
- 'var unitSel=document.querySelector(\'select.nw-select[data-nw-unit]\');'
- 'var curU=unitSel?unitSel.value:"day";'
- 'if(allowed.indexOf(curU)<0){'
- 'try{localStorage.setItem("behive.chartUnit",allowed[0]);}catch(_){}'
+ 'var rv=sel.value;'
+ 'if(rv==="year"||rv==="month"){'
+ # 연도별/월별 진입 — 현재 연도 드롭다운 값(기본=최신연도)으로 chartYM 세팅.
+ 'var yEl=document.querySelector(\'select.nw-select[data-nw-year]\');'
+ 'var y=yEl&&yEl.value?yEl.value:"";'
+ 'if(!y)return;' # 데이터 없음
+ 'if(rv==="year"){window.__behive_set_ym(y);}'
+ 'else{var mm=ymFirstMonth(y);window.__behive_set_ym(mm?(y+"-"+mm):y);}'
+ '}else{'
+ 'window.__behive_set_ym("");' # 프리셋 → 절대선택 해제
+ 'try{localStorage.setItem("behive.chartDaysV2",rv);}catch(_){}'
+ # 기간 변경 시 현재 단위가 비허용이면 첫 허용으로 자동 보정 (서버 sanity와 일치).
+ 'var allowedMap={"-3":["day","week"],"-2":["day","week","month"],"-4":["day","week","month","year"],"-1":["day","week","month","year"]};'
+ 'var allowed=allowedMap[rv]||["day","week","month","year"];'
+ 'var unitSel=document.querySelector(\'select.nw-select[data-nw-unit]\');'
+ 'var curU=unitSel?unitSel.value:"day";'
+ 'if(allowed.indexOf(curU)<0){try{localStorage.setItem("behive.chartUnit",allowed[0]);}catch(_){}}'
'}'
+ '}else if(sel.hasAttribute("data-nw-year")){'
+ 'var rEl=document.querySelector(\'select.nw-select[data-nw-range]\');'
+ 'var mode=rEl?rEl.value:"year";var yy=sel.value;'
+ 'if(mode==="month"){var m2=ymFirstMonth(yy);window.__behive_set_ym(m2?(yy+"-"+m2):yy);}'
+ 'else{window.__behive_set_ym(yy);}'
+ '}else if(sel.hasAttribute("data-nw-month")){'
+ 'var ry=document.querySelector(\'select.nw-select[data-nw-year]\');'
+ 'window.__behive_set_ym((ry?ry.value:"")+"-"+sel.value);'
'}else if(sel.hasAttribute("data-nw-unit")){'
'try{localStorage.setItem("behive.chartUnit",sel.value);}catch(_){}'
'}else if(sel.hasAttribute("data-nw-mode")){'
@@ -9329,11 +9422,11 @@ def _save_owner_slices(owner_data: dict, balances: dict, journal_by_label: dict
}
-def _patch_all_cache_for_owner(owner: str, chart_days: int = NET_WORTH_CHART_DAYS, chart_unit: str = NET_WORTH_CHART_UNIT, chart_mode: str = NET_WORTH_CHART_MODE, adr_days: int = ADR_TREND_DAYS) -> None:
+def _patch_all_cache_for_owner(owner: str, chart_days: int = NET_WORTH_CHART_DAYS, chart_unit: str = NET_WORTH_CHART_UNIT, chart_mode: str = NET_WORTH_CHART_MODE, adr_days: int = ADR_TREND_DAYS, chart_ym: str = '') -> None:
"""partial fetch 직후 'all' 캐시의 summary 패널과 해당 owner 패널만 in-place로 갈아끼운다.
다른 owner와 watchlist/interests 슬라이스는 cached 'all'에 있던 그대로 — TTL 만료 시 full fetch로 동기화.
핵심 목표: 자산정보 탭의 본인 KPI가 관리자 탭과 같은 시점이 되도록."""
- all_key = f'all:d{chart_days}:u{chart_unit}:m{chart_mode}:a{adr_days}'
+ all_key = f'all:d{chart_days}:u{chart_unit}:m{chart_mode}:a{adr_days}:y{chart_ym}'
all_lock = _panels_lock_for(all_key)
with all_lock:
slot = _panels_cache.get(all_key)
@@ -9354,7 +9447,7 @@ def _patch_all_cache_for_owner(owner: str, chart_days: int = NET_WORTH_CHART_DAY
for e in _owner_data_cache.values():
merged_balances.update(e.get('balances') or {})
merged_journal.update(e.get('journal_by_label') or {})
- new_summary = _render_summary_panel(ordered, merged, merged_balances, merged_journal, chart_days=chart_days, chart_unit=chart_unit, chart_mode=chart_mode, adr_days=adr_days)
+ new_summary = _render_summary_panel(ordered, merged, merged_balances, merged_journal, chart_days=chart_days, chart_unit=chart_unit, chart_mode=chart_mode, adr_days=adr_days, chart_ym=chart_ym)
entry = _owner_data_cache.get(owner)
if not entry:
return
@@ -9378,7 +9471,7 @@ def _patch_all_cache_for_owner(owner: str, chart_days: int = NET_WORTH_CHART_DAY
_panels_cache[all_key] = {'json': new_body, 'expires_at': slot['expires_at']}
-def _get_cached_panels_json(owner: str | None = None, fresh: bool = False, chart_days: int = NET_WORTH_CHART_DAYS, chart_unit: str = NET_WORTH_CHART_UNIT, chart_mode: str = NET_WORTH_CHART_MODE, adr_days: int = ADR_TREND_DAYS) -> bytes:
+def _get_cached_panels_json(owner: str | None = None, fresh: bool = False, chart_days: int = NET_WORTH_CHART_DAYS, chart_unit: str = NET_WORTH_CHART_UNIT, chart_mode: str = NET_WORTH_CHART_MODE, adr_days: int = ADR_TREND_DAYS, chart_ym: str = '') -> bytes:
"""fresh=True 시 캐시 우회 — 명시적 새로고침(?fresh=1)에서 호출.
응답을 새로 만든 뒤에는 캐시에 덮어쓰므로 그 후 자동 갱신은 새 캐시를 본다.
@@ -9388,7 +9481,7 @@ def _get_cached_panels_json(owner: str | None = None, fresh: bool = False, chart
갈아끼운다(_patch_all_cache_for_owner). 자산정보 = 본인+가희 KPI 카드 스택이라 owner_data만
있으면 재렌더 가능 — 다시 fetch할 필요 없음. 패치 실패 시 안전망으로 'all' 캐시 비워
다음 자산정보 진입에서 full fetch."""
- key = f'{owner or "all"}:d{chart_days}:u{chart_unit}:m{chart_mode}:a{adr_days}'
+ key = f'{owner or "all"}:d{chart_days}:u{chart_unit}:m{chart_mode}:a{adr_days}:y{chart_ym}'
if not fresh:
slot = _panels_cache.get(key)
if slot and slot['expires_at'] > time.time():
@@ -9399,15 +9492,15 @@ def _get_cached_panels_json(owner: str | None = None, fresh: bool = False, chart
slot = _panels_cache.get(key)
if slot and slot['expires_at'] > time.time():
return slot['json']
- payload = _build_panels_payload(owner=owner, chart_days=chart_days, chart_unit=chart_unit, chart_mode=chart_mode, adr_days=adr_days)
+ payload = _build_panels_payload(owner=owner, chart_days=chart_days, chart_unit=chart_unit, chart_mode=chart_mode, adr_days=adr_days, chart_ym=chart_ym)
body = json.dumps(payload, ensure_ascii=False).encode('utf-8')
_panels_cache[key] = {'json': body, 'expires_at': time.time() + PANELS_CACHE_TTL}
if owner is not None:
try:
- _patch_all_cache_for_owner(owner, chart_days=chart_days, chart_unit=chart_unit, chart_mode=chart_mode, adr_days=adr_days)
+ _patch_all_cache_for_owner(owner, chart_days=chart_days, chart_unit=chart_unit, chart_mode=chart_mode, adr_days=adr_days, chart_ym=chart_ym)
except Exception:
traceback.print_exc()
- _panels_cache.pop(f'all:d{chart_days}:u{chart_unit}:m{chart_mode}:a{adr_days}', None)
+ _panels_cache.pop(f'all:d{chart_days}:u{chart_unit}:m{chart_mode}:a{adr_days}:y{chart_ym}', None)
return body
@@ -10082,8 +10175,17 @@ class Handler(BaseHTTPRequestHandler):
adr_days = ADR_TREND_DAYS
if adr_days not in {d for _, d in ADR_TREND_PRESETS}:
adr_days = ADR_TREND_DAYS
+ # chart_ym — 'YYYY'(연도별) 또는 'YYYY-MM'(월별). 형식 위반은 무시(빈값=프리셋).
+ chart_ym = (qs.get('chart_ym') or [''])[0]
+ _ymp = (chart_ym or '').split('-')
+ _ym_ok = (
+ (len(_ymp) == 1 and len(_ymp[0]) == 4 and _ymp[0].isdigit())
+ or (len(_ymp) == 2 and len(_ymp[0]) == 4 and _ymp[0].isdigit() and len(_ymp[1]) == 2 and _ymp[1].isdigit())
+ )
+ if not _ym_ok:
+ chart_ym = ''
try:
- body = _get_cached_panels_json(owner=owner, fresh=fresh, chart_days=chart_days, chart_unit=chart_unit, chart_mode=chart_mode, adr_days=adr_days)
+ body = _get_cached_panels_json(owner=owner, fresh=fresh, chart_days=chart_days, chart_unit=chart_unit, chart_mode=chart_mode, adr_days=adr_days, chart_ym=chart_ym)
except Exception:
traceback.print_exc()
self.send_error(500, 'panels failed')