Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
['name' => 'APIv2#getDefault', 'url' => '/api/v2/activity', 'verb' => 'GET'],
['name' => 'APIv2#listFilters', 'url' => '/api/v2/activity/filters', 'verb' => 'GET'],
['name' => 'APIv2#getDownloadCount', 'url' => '/api/v2/activity/downloads/count', 'verb' => 'GET'],
// Before the catch-all below, and a sub-path of it so `{filter}` cannot
// swallow it either way
['name' => 'APIv2#getHistogram', 'url' => '/api/v2/activity/{filter}/histogram', 'verb' => 'GET'],
['name' => 'APIv2#getFilter', 'url' => '/api/v2/activity/{filter}', 'verb' => 'GET'],
],
'routes' => [
Expand Down
62 changes: 62 additions & 0 deletions docs/endpoint-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,65 @@ In case the endpoint returns more fields, they should be ignored and are depreca
]
}
```

# Daily activity counts

`GET /ocs/v2.php/apps/activity/api/v2/activity/{filter}/histogram`

Number of activities per calendar day, for the stream's heatmap.

Name | Type | Description
---|---|---
`days` | int (Optional) | Length of the window, ending today. Clamped to 1–366 (Default: `364`)
`search` | string (Optional) | Only count activities whose file path contains this substring
`actor` | string (Optional) | Only count activities authored by this account
`object_type` / `object_id` | (Optional) | As on the stream endpoint, and only together

The window deliberately takes no `from`/`to`. The histogram is the control a
reader picks a date range *with*, so it has to keep reporting the days outside
the current selection. Every other restriction is honoured, which is what keeps
the counts equal to the number of activities the stream itself would list — the
two share one query builder internally for exactly that reason.

Days are bucketed in the account's own timezone (the `core`/`timezone` user
setting), not in UTC, so an activity at 01:00 local time belongs to that local
day. A real timezone rather than a fixed offset is used, so a window spanning a
DST change still groups correctly.

## Response

```json
{
"from": "2025-08-03",
"to": "2026-08-01",
"counts": { "2025-08-04": 12, "2025-08-06": 3 },
"max": 12,
"total": 15,
"partial_before": null
}
```

Name | Description
---|---
`from` / `to` | Inclusive window boundaries as local dates
`counts` | Activities per day, keyed by date. **Days with no activity are omitted**, so the payload stays proportional to real activity rather than to the window length; a client fills the gaps with zero
`max` | Busiest day in the window, for scaling a colour ramp. `0` when the window is empty
`total` | Sum over the window
`partial_before` | Normally `null`. Set to a date when the query hit its row guard, meaning counts before that date are incomplete — a client should mark those days rather than draw understated values

## HTTP Status

Status Code | Description
---|---
`200 OK` | Counts
`400 Bad Request` | The search term is too short or too long, or the account name is too long
`403 Forbidden` | The user is not logged in
`404 Not Found` | The filter is unknown

## Cost

The query selects one column, restricted to a single `affecteduser` and a
timestamp range, which is exactly the `activity_user_time` index. Bucketing into
days happens in PHP rather than in SQL: grouping by day needs integer division or
a modulo, neither of which `IQueryBuilder` can express, and a raw expression
would have to be written four different ways for the databases this app supports.

This file was deleted.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions js/ActivityTab-Cxq6JF8r.chunk.mjs

This file was deleted.

3 changes: 3 additions & 0 deletions js/ActivityTab-Dpi1imUR.chunk.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(function(){"use strict";try{if(typeof document<"u"){var t=document.createElement("style");t.appendChild(document.createTextNode(".download-summary[data-v-81e4514f]{display:flex;align-items:flex-start;padding:8px 0;margin-bottom:calc(var(--default-grid-baseline) * 2);color:var(--color-text-maxcontrast)}.download-summary__icon[data-v-81e4514f]{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:20px;height:20px;margin-top:2px;opacity:.5}.download-summary__text[data-v-81e4514f]{padding:0 5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activity[data-v-50818749]{display:flex;flex-direction:column;overflow:hidden;height:100%}.activity__actions[data-v-50818749]{display:flex;flex-direction:column;width:100%}.activity__list[data-v-50818749]{flex-grow:1;overflow:scroll}.activity__empty-content[data-v-50818749]{height:100%}[data-v-50818749] .empty-content__icon span{background-size:64px;width:64px;height:64px}")),document.head.appendChild(t)}}catch(e){console.error("vite-plugin-css-injected-by-js",e)}})();
import{l as R}from"./activity-sidebar.mjs";import{b as A,m as I,h as G,o as D}from"./NcCheckboxRadioSwitch-BVTMQSAg-OXnfnqVj.chunk.mjs";import{d as b,x as V,B as E,y as T,a as i,c as o,j as B,X as p,J as h,m as n,e as S,f as u,t as H,E as g,l as M,b as r,g as y,F as v,K as k,n as q}from"./translation-DoG5ZELJ-B36BKsSA.chunk.mjs";import{a as j}from"./index-BrNGPgve.chunk.mjs";import{j as F,f as O,A as W}from"./ActivityComponent.vue_vue_type_script_setup_true_lang-BtJFcDdb.chunk.mjs";import{l as f}from"./logger-CXg3FpL2.chunk.mjs";import{g as U,a as z,b as J}from"./api-fwrRGLr6.chunk.mjs";import"./preload-helper-DxYC2qmj.chunk.mjs";var w;(function(t){t[t.User=0]="User",t[t.Group=1]="Group",t[t.Link=3]="Link",t[t.Email=4]="Email",t[t.Remote=6]="Remote",t[t.Team=7]="Team",t[t.Guest=8]="Guest",t[t.RemoteGroup=9]="RemoteGroup",t[t.Room=10]="Room",t[t.Deck=12]="Deck",t[t.FederatedGroup=14]="FederatedGroup",t[t.ScienceMesh=15]="ScienceMesh"})(w||(w={}));const K=b({__name:"ActivitySidebarPlugin",props:{plugin:{},node:{}},emits:["reloadActivities"],setup(t,{emit:e}){const a=t,s=e,d=B();return V(()=>a.plugin.mount(d.value,{node:a.node,context:E()?.proxy??void 0,reload:()=>s("reloadActivities")})),T(()=>a.plugin.unmount()),(_,c)=>(i(),o("div",{ref_key:"attachTarget",ref:d},null,512))}}),X='<svg xmlns="http://www.w3.org/2000/svg" id="mdi-download-circle" viewBox="0 0 24 24"><path d="M12 2C17.5 2 22 6.5 22 12C22 17.5 17.5 22 12 22C6.5 22 2 17.5 2 12C2 6.5 6.5 2 12 2M8 17H16V15H8V17M16 10H13.5V6H10.5V10H8L12 14L16 10Z" /></svg>',Z=b({name:"DownloadSummary",components:{NcIconSvgWrapper:A},props:{fileId:{type:Number,required:!0}},data(){return{totalCount:0,monthlyCount:0,downloadSVG:X}},computed:{summaryText(){return this.monthlyCount>0&&this.monthlyCount<this.totalCount?p("activity","Downloaded %n time (%s in the last 30 days)","Downloaded %n times (%s in the last 30 days)",this.totalCount,[String(this.monthlyCount)]):p("activity","Downloaded %n time","Downloaded %n times",this.totalCount)}},watch:{fileId:{immediate:!0,handler(){this.fetchCounts()}}},methods:{async fetchCounts(){if(this.fileId){this.totalCount=0,this.monthlyCount=0;try{const t=await I.get(j("apps/activity/api/v2/activity/downloads/count"),{params:{format:"json",object_type:"files",object_id:this.fileId}});this.totalCount=t.data.ocs.data.total,this.monthlyCount=t.data.ocs.data.last30d}catch(t){f.error("Failed to fetch download counts",{error:t})}}},t:h,n:p}}),$={key:0,class:"download-summary"},Q={class:"download-summary__icon"},Y={class:"download-summary__text"};function tt(t,e,a,s,d,_){const c=n("NcIconSvgWrapper");return t.totalCount>0?(i(),o("div",$,[S("span",Q,[u(c,{svg:t.downloadSVG,size:20},null,8,["svg"])]),S("span",Y,H(t.summaryText),1)])):g("",!0)}const it=G(Z,[["render",tt],["__scopeId","data-v-81e4514f"]]),et=b({name:"ActivityTab",components:{ActivityComponent:O,DownloadSummary:it,NcEmptyContent:F,NcIconSvgWrapper:A,NcLoadingIcon:D,ActivitySidebarPlugin:K},props:{node:{type:Object,required:!0},folder:{type:Object,required:!1,default:void 0},view:{type:Object,required:!1,default:void 0}},expose:["update"],data(){return{error:"",loading:!0,activities:[],lightningBoltSVG:R,sidebarPlugins:[]}},computed:{hasPublicLink(){return Object.values(this.node?.attributes?.["share-types"]??{}).flat().includes(w.Link)}},watch:{node:{immediate:!0,async handler(){await this.update()}}},async mounted(){this.node&&await this.update()},methods:{async update(){this.sidebarPlugins=[];const t=J();t.length>0&&M(()=>{this.sidebarPlugins=t}),this.resetState(),await this.getActivities()},async getActivities(){try{this.loading=!0;const t=await this.processActivities(await this.loadRealActivities()),e=await z({node:this.node});this.activities=[...t,...e].sort((a,s)=>s.timestamp-a.timestamp)}catch(t){this.error=h("activity","Unable to load the activity list"),f.error("Error loading the activity list",{error:t})}finally{this.loading=!1}},resetState(){this.loading=!0,this.error="",this.activities=[]},async loadRealActivities(){try{const{data:t}=await I.get(j("apps/activity/api/v2/activity/filter"),{params:{format:"json",object_type:"files",object_id:this.node.fileid}});return t.ocs.data}catch(t){if(t.response!==void 0&&t.response.status===304)return[];throw t}},processActivities(t){t=t.map(a=>new W(a)),f.debug(`Processed ${t.length} activity(ies)`,{activities:t,node:this.node});const e=U();return t.filter(a=>!e||e.every(s=>s(a)))},t:h}}),at={key:0,class:"activity__actions"},ot={key:4,class:"activity__list"};function st(t,e,a,s,d,_){const c=n("NcIconSvgWrapper"),m=n("NcEmptyContent"),P=n("ActivitySidebarPlugin"),x=n("DownloadSummary"),L=n("NcLoadingIcon"),N=n("ActivityComponent");return i(),o("div",{class:q([{"icon-loading":t.loading},"activity"])},[t.error||!t.node?(i(),r(m,{key:0,name:t.error},{icon:y(()=>[u(c,{svg:t.lightningBoltSVG},null,8,["svg"])]),_:1},8,["name"])):(i(),o(v,{key:1},[t.sidebarPlugins.length>0?(i(),o("div",at,[(i(!0),o(v,null,k(t.sidebarPlugins,(l,C)=>(i(),r(P,{key:C,plugin:l,node:t.node,onReloadActivities:e[0]||(e[0]=nt=>t.getActivities())},null,8,["plugin","node"]))),128))])):g("",!0),t.hasPublicLink&&t.node.fileid?(i(),r(x,{key:1,fileId:t.node.fileid},null,8,["fileId"])):g("",!0),t.loading?(i(),r(m,{key:2,class:"activity__empty-content",name:t.t("activity","Loading activities")},{icon:y(()=>[u(L)]),_:1},8,["name"])):t.activities.length===0?(i(),r(m,{key:3,class:"activity__empty-content",name:t.t("activity","No activity yet")},{icon:y(()=>[u(c,{svg:t.lightningBoltSVG},null,8,["svg"])]),_:1},8,["name"])):(i(),o("ul",ot,[(i(!0),o(v,null,k(t.activities,l=>(i(),r(N,{key:l.id,activity:l,showPreviews:!1,onReload:e[1]||(e[1]=C=>t.getActivities())},null,8,["activity"]))),128))]))],64))],2)}const vt=G(et,[["render",st],["__scopeId","data-v-50818749"]]);export{vt as default};
//# sourceMappingURL=ActivityTab-Dpi1imUR.chunk.mjs.map

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Loading
Loading