summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoraura <nw@moneybot.cc>2026-02-23 09:21:27 +0100
committeraura <nw@moneybot.cc>2026-02-23 09:21:27 +0100
commit6d554927ec34072599161b7e8eb3cce40eda5c17 (patch)
tree230818917fb1997bf8f411318756fdb83be558cf
parent2cd9a203b8e8c37951aae9e338e2e0fa503bd667 (diff)
add activity to git page
-rw-r--r--web/cgit-extra.html152
-rw-r--r--web/git.css73
2 files changed, 221 insertions, 4 deletions
diff --git a/web/cgit-extra.html b/web/cgit-extra.html
index 2237e26..ed62938 100644
--- a/web/cgit-extra.html
+++ b/web/cgit-extra.html
@@ -30,7 +30,7 @@ function branchFromForm() {
async function getFile( repo, branch, file ) {
try {
const res = await fetch( `https://${location.host}/${repo}/plain/${file}` );
- if( !res.ok )
+ if( !res.ok || !res.headers.get( "Content-Type" ).toLowerCase().startsWith( "text/plain" ) )
return null;
return await res.text();
} catch( e ) {
@@ -68,6 +68,7 @@ function isIndex( repo ) {
let path = location.pathname;
return path.replaceAll( "/", "" ) === repo;
}
+
function parseLink( text, start ) {
let isImg = 0;
@@ -194,7 +195,7 @@ function replaceCodeTicks( text ) {
}
function runHighlight() {
- if( hljs === undefined || !hljs )
+ if( typeof( hljs ) === 'undefined' || !hljs )
return setTimeout( runHighlight, 200 );
hljs.highlightAll();
@@ -249,7 +250,154 @@ function isCodeView( repo ) {
return location.pathname.includes( `/${repo}/tree` );
}
+function isMainPage() {
+ return location.pathname === "/";
+}
+
+async function getActivityData( days ) {
+ const response = await fetch( "https://networkheaven.net/git-access.log" );
+ const data = await response.text();
+ const ret = new Array( days ).fill( 0 );
+ let highest = 0;
+
+ const now = new Date();
+ let start = data.length - 1;
+ let line = '';
+ for( let i = data.length - 1; i >= 0; --i ) {
+ if( i != 0 && data[i] != '\n' )
+ continue;
+
+ line = data.substring( i, start );
+ start = i;
+ if( !line.length )
+ continue;
+
+ const date = new Date( parseInt( line ) * 1000 );
+ const delta = Math.floor( ( now - date ) / 1000 / 60 / 60 / 24 );
+ if( delta > days )
+ break;
+
+ ret[delta] += 1;
+ if( ret[delta] > highest )
+ highest = ret[delta];
+ }
+
+ return { values: ret, highest };
+}
+
+function getActivityClass( delta, highest ) {
+ if( !highest )
+ return "";
+
+ const ratio = delta / highest;
+ if( ratio < 0.0001 )
+ return "";
+ if( ratio < 0.25 )
+ return "activity-low";
+ else if( ratio < 0.5 )
+ return "activity-medium";
+ else if( ratio < 0.75 )
+ return "activity-high";
+ else
+ return "activity-very-high";
+}
+
+let spinnerFrame = 0;
+const spinnerFrames = [
+ '/',
+ '-',
+ '\\',
+ '|',
+];
+
+function doSpinner() {
+ if( spinnerFrame >= spinnerFrames.length )
+ spinnerFrame = 0;
+ const frame = spinnerFrames[spinnerFrame];
+ const spinners = document.querySelectorAll( ".spinner" );
+ if( !spinners.length )
+ return;
+ for( const el of spinners )
+ el.innerText = frame;
+ spinnerFrame++;
+ setTimeout( doSpinner, 150 );
+}
+
+function createActivityContainer() {
+ const wrapper = document.createElement( "div" );
+ wrapper.classList.add( "activity-outer" );
+
+ const title = document.createElement( "h2" );
+ title.classList.add( "activity-title" );
+ title.textContent = "Activity";
+ wrapper.appendChild( title );
+
+ const hr = document.createElement( "hr" );
+ hr.classList.add( "activity-hr" );
+ wrapper.appendChild( hr );
+
+ const inner = document.createElement( "div" );
+ inner.classList.add( "activity-inner" );
+ wrapper.appendChild( inner );
+
+ const table = document.createElement( "table" );
+ table.classList.add( "activity-table" );
+ inner.appendChild( table );
+
+ const spinner = document.createElement( "div" );
+ spinner.classList.add( "spinner" );
+ spinner.innerText = "-";
+ inner.appendChild( spinner );
+ setTimeout( doSpinner, 150 );
+
+ const content = document.querySelector( ".content" );
+ content.appendChild( wrapper );
+}
+
+async function createActivityGraph() {
+ const date = new Date();
+ let weekday = date.getDay();
+ // christ is king
+ if( weekday == 0 ) weekday = 6;
+ else weekday -= 1;
+
+ const activity = await getActivityData( 365 + weekday );
+ const table = document.querySelector( ".activity-table" );
+
+ let row = document.createElement( "tr" );
+ table.appendChild( row );
+ let rowc = 0;
+ let colc = 0;
+ for( let i = 0; i < 365 + weekday; ++i ) {
+ const day = 364 + weekday - i;
+ const commits = activity.values[day];
+ const aclass = getActivityClass( commits, activity.highest );
+ const cell = document.createElement( "td" );
+ cell.classList.add( "activity-cell-" + day );
+ if( aclass ) cell.classList.add( aclass );
+
+ const popup = document.createElement( "div" );
+ popup.classList.add( "popup" );
+ if( i > 20 ) popup.classList.add( "right" );
+ const dateStr = new Date( (new Date()) - day * 24 * 60 * 60 * 1000 ).toLocaleDateString();
+ popup.innerText = `${dateStr}\n${commits} commits`;
+
+ cell.appendChild( popup );
+ row.appendChild( cell );
+ }
+
+ const wrapper = document.querySelector( ".activity-outer" );
+ const spinner = wrapper.querySelector( ".spinner" );
+ if( spinner )
+ spinner.remove();
+}
+
const main = async () => {
+ if( isMainPage() ) {
+ createActivityContainer();
+ return setTimeout( createActivityGraph );
+ }
+
const mainEl = document.querySelector( ".main" );
if( !mainEl )
return;
diff --git a/web/git.css b/web/git.css
index 84ef9ad..a8b3435 100644
--- a/web/git.css
+++ b/web/git.css
@@ -84,6 +84,75 @@ a:hover {
color: #fff;
}
+.activity-wrapper {
+ display: flex;
+}
+
+.activity-inner {
+ display: flex;
+}
+
+.activity-table tr {
+ height: 11px;
+}
+
+.activity-table td {
+ height: 9px;
+ width: 9px;
+ border: 1px solid #888;
+ background-color: #7f7f7f;
+}
+
+.activity-title {
+ margin-left: 5px;
+ margin-bottom: 1px;
+ text-decoration: none;
+ background: var( --gradient );
+ background-clip: border-box;
+ -webkit-text-fill-color: transparent;
+ -webkit-background-clip: text;
+ color: #c6a6ff;
+ font-weight: normal !important;
+ font-size: 17px;
+ font-family: JPN16;
+ width: fit-content;
+}
+
+.activity-table td:hover {
+ border-left: 3px solid #888;
+ border-right: 3px solid #888;
+}
+td .popup {
+ display: none;
+ position: absolute;
+ border-top: 1px solid #fff;
+ border-left: 1px solid #ccc;
+ border-right: 1px solid #666;
+ border-bottom: 1px solid #000;
+ background: #888;
+ padding: 3px;
+ padding-right: 5px;
+ z-index: 1;
+}
+
+td .popup.right {
+ translate: -100%;
+}
+
+td:hover .popup {
+ display: inline-block;
+}
+
+hr {
+ margin: 0;
+ color: #fff;
+}
+
+.activity-low { background-color: #f0f !important; }
+.activity-medium { background-color: #c488ff !important; }
+.activity-high { background-color: #c6a6ff !important; }
+.activity-very-high { background-color: #0ff !important; }
+
div#cgit {
padding: 0em;
margin: 0em;
@@ -725,7 +794,7 @@ div#cgit ul.pager .current {
}
div#cgit span.age-mins {
- color: #0f0;
+ color: #0ff;
}
div#cgit span.age-hours {
@@ -733,7 +802,7 @@ div#cgit span.age-hours {
}
div#cgit span.age-days {
- color: #040;
+ color: #0c0;
}
div#cgit span.age-weeks {