arkaos 3.58.0 → 3.59.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/VERSION CHANGED
@@ -1 +1 @@
1
- 3.58.0
1
+ 3.59.0
@@ -37,6 +37,25 @@ const { data, status, error, refresh } = await fetchApi<DeptDetail>(
37
37
  `/api/departments/${deptId.value}`,
38
38
  )
39
39
 
40
+ // PR97a v3.59.0 — 30d activity sparkline for this department.
41
+ interface SparklineDay {
42
+ date: string
43
+ calls: number
44
+ cost_usd: number | null
45
+ }
46
+ const { data: sparklineData } = fetchApi<{
47
+ days: SparklineDay[]
48
+ period_days: number
49
+ department: string
50
+ }>(() => deptId.value ? `/api/departments/${deptId.value}/activity-sparkline?days=30` : '')
51
+ const sparkline = computed<SparklineDay[]>(() => sparklineData.value?.days ?? [])
52
+ const sparklineMaxCalls = computed(() =>
53
+ Math.max(1, sparkline.value.reduce((acc, d) => Math.max(acc, d.calls), 0)),
54
+ )
55
+ const sparklineTotalCalls = computed(() =>
56
+ sparkline.value.reduce((acc, d) => acc + d.calls, 0),
57
+ )
58
+
40
59
  // PR90b v3.32.0 — Compare with another department.
41
60
  const { data: deptListData } = fetchApi<{ departments: Array<{ department: string }> }>(
42
61
  '/api/departments',
@@ -177,6 +196,39 @@ const tierColor = (tier: number | undefined) => {
177
196
  :on-retry="() => refresh()"
178
197
  >
179
198
  <div v-if="detail" class="space-y-5 max-w-5xl">
199
+ <!-- PR97a v3.59.0 — 30d sparkline -->
200
+ <section
201
+ v-if="sparklineTotalCalls > 0"
202
+ class="rounded-xl border border-default bg-elevated/10 p-5"
203
+ >
204
+ <div class="flex items-center justify-between text-xs mb-2">
205
+ <span class="font-semibold text-muted uppercase tracking-wide">
206
+ Daily calls (30d)
207
+ </span>
208
+ <span class="font-mono text-muted">
209
+ {{ sparklineTotalCalls }} total · max {{ sparklineMaxCalls }}/day
210
+ </span>
211
+ </div>
212
+ <svg
213
+ :viewBox="`0 0 ${sparkline.length * 8} 48`"
214
+ class="w-full h-12"
215
+ preserveAspectRatio="none"
216
+ >
217
+ <rect
218
+ v-for="(day, idx) in sparkline"
219
+ :key="day.date"
220
+ :x="idx * 8 + 1"
221
+ :y="48 - (day.calls / sparklineMaxCalls) * 46"
222
+ width="6"
223
+ :height="(day.calls / sparklineMaxCalls) * 46"
224
+ class="fill-primary"
225
+ :class="day.calls === 0 ? 'opacity-20' : 'opacity-90'"
226
+ >
227
+ <title>{{ day.date }} · {{ day.calls }} calls</title>
228
+ </rect>
229
+ </svg>
230
+ </section>
231
+
180
232
  <!-- Stats row -->
181
233
  <section class="grid grid-cols-2 md:grid-cols-4 gap-3">
182
234
  <div class="rounded-xl border border-default p-4 bg-elevated/20">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.58.0",
3
+ "version": "3.59.0",
4
4
  "description": "The Operating System for AI Agent Teams",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arkaos-core"
3
- version = "3.58.0"
3
+ version = "3.59.0"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -1941,6 +1941,80 @@ def department_merge(src: str, dst: str):
1941
1941
  }
1942
1942
 
1943
1943
 
1944
+ @app.get("/api/departments/{dept_id}/activity-sparkline")
1945
+ def department_activity_sparkline(dept_id: str, days: int = 30):
1946
+ """PR97a v3.59.0 — daily calls/cost series for a department.
1947
+
1948
+ Returns ``{days: [{date, calls, cost_usd}], period_days, department}``
1949
+ pre-seeded with zeros so the SVG renders without gaps. Counts every
1950
+ telemetry row with ``subagent:<dept>`` or ``subagent:<dept>:<agent>``
1951
+ category. Capped at 90 days.
1952
+ """
1953
+ dept_id = dept_id.strip().lower()
1954
+ try:
1955
+ days_int = int(days) if days is not None else 30
1956
+ except (TypeError, ValueError):
1957
+ days_int = 30
1958
+ capped_days = max(1, min(days_int, 90))
1959
+
1960
+ # Bail early if department has no agents — keep parity with detail endpoint.
1961
+ if not any(a.get("department") == dept_id for a in _load_agents()):
1962
+ return {"error": "Department not found or has no agents"}
1963
+
1964
+ try:
1965
+ from core.runtime.llm_cost_telemetry import read_entries
1966
+ except Exception:
1967
+ return {"days": []}
1968
+
1969
+ from datetime import datetime, timedelta, timezone
1970
+ today = datetime.now(timezone.utc).date()
1971
+ buckets: dict[str, dict] = {}
1972
+ for offset in range(capped_days):
1973
+ d = today - timedelta(days=capped_days - 1 - offset)
1974
+ buckets[d.isoformat()] = {
1975
+ "date": d.isoformat(),
1976
+ "calls": 0,
1977
+ "cost_usd": 0.0,
1978
+ "cost_known": False,
1979
+ }
1980
+ cutoff = today - timedelta(days=capped_days - 1)
1981
+ prefix_dept = f"subagent:{dept_id}"
1982
+ for entry in read_entries():
1983
+ raw_ts = entry.get("ts") or ""
1984
+ if not isinstance(raw_ts, str):
1985
+ continue
1986
+ try:
1987
+ ts = datetime.fromisoformat(raw_ts.replace("Z", "+00:00"))
1988
+ except ValueError:
1989
+ continue
1990
+ if ts.date() < cutoff:
1991
+ continue
1992
+ cat = str(entry.get("category") or "")
1993
+ # subagent:<dept> OR subagent:<dept>:<agent>
1994
+ if cat != prefix_dept and not cat.startswith(prefix_dept + ":"):
1995
+ continue
1996
+ key = ts.date().isoformat()
1997
+ if key not in buckets:
1998
+ continue
1999
+ buckets[key]["calls"] += 1
2000
+ cost = entry.get("estimated_cost_usd")
2001
+ if isinstance(cost, (int, float)):
2002
+ buckets[key]["cost_usd"] += float(cost)
2003
+ buckets[key]["cost_known"] = True
2004
+
2005
+ out: list[dict] = []
2006
+ for k in sorted(buckets.keys()):
2007
+ bucket = buckets[k]
2008
+ out.append({
2009
+ "date": bucket["date"],
2010
+ "calls": bucket["calls"],
2011
+ "cost_usd": (
2012
+ round(bucket["cost_usd"], 6) if bucket["cost_known"] else None
2013
+ ),
2014
+ })
2015
+ return {"days": out, "period_days": capped_days, "department": dept_id}
2016
+
2017
+
1944
2018
  @app.get("/api/departments/{dept_id}")
1945
2019
  def department_detail(dept_id: str):
1946
2020
  """Full department detail: agents, workflows, 30d cost."""