/**
* Home and Profile Routes
*
* Handles:
* - GET / - Home page (repo manager)
* - GET /profile - User profile page
*/
import { html, json, notFound, unauthorized, badRequest, readJson } from '../response.js'
import { renderRepoManager } from '../repo-manager-ui.js'
import { renderUserProfile } from '../user-profile-ui.js'
import { requireAuth, optionalAuth, loadUser } from '../middleware/require-auth.js'
import { spawn } from 'bare-subprocess'
import path from 'bare-path'
// Cache activity data (recompute every 10 minutes)
let _activityCache = null
let _activityCacheTime = 0
const ACTIVITY_CACHE_TTL = 600000 // 10 min
async function getActivityData (reposDir, repos) {
const now = Date.now()
if (_activityCache && (now - _activityCacheTime) < ACTIVITY_CACHE_TTL) {
return _activityCache
}
const daySeconds = 24 * 60 * 60
const days = 365
const nowSec = Math.floor(now / 1000)
const result = {}
await Promise.all(repos.map(repo => {
return new Promise(resolve => {
const repoPath = path.join(reposDir, repo.name + '.git')
const child = spawn('git', ['-C', repoPath, 'log', '--format=%at', '--since=365 days ago'])
const chunks = []
child.stdout.on('data', d => chunks.push(d))
child.on('exit', () => {
const timestamps = Buffer.concat(chunks).toString().trim().split('\n').filter(Boolean).map(Number)
const buckets = new Array(days).fill(0)
for (const ts of timestamps) {
const daysAgo = Math.floor((nowSec - ts) / daySeconds)
if (daysAgo >= 0 && daysAgo < days) {
buckets[days - 1 - daysAgo]++
}
}
result[repo.name] = buckets
resolve()
})
child.on('error', () => resolve())
// Timeout after 5s per repo
setTimeout(() => resolve(), 5000)
})
}))
_activityCache = result
_activityCacheTime = now
return result
}
/**
* Register home routes
*/
export function registerHomeRoutes(router) {
// Home page
router.get('/', serveHome)
router.get('', serveHome)
// Profile page
router.get('/profile', serveProfile)
router.get('/profile/', serveProfile)
}
/**
* Serve home page (repo manager)
*/
async function serveHome(req, res, server) {
const { namespaceManager, namespaceRegistry } = server.namespaceData
const { repoRegistry, repos, scanner } = server.repoData
const { pathPrefix, defaultNamespace } = server.config
// Load groups for inline display — merge local + shared (same as /groups page)
let groups = []
try {
const local = await namespaceManager.list({ type: 'group' })
if (namespaceRegistry) {
const shared = await namespaceRegistry.list({ type: 'group' })
const byName = new Map()
for (const g of local) byName.set(g.name, { ...g, source: 'local' })
for (const g of shared) {
if (g.name === 'local') continue
const existing = byName.get(g.name)
if (existing) {
const mergedRepos = [...new Set([...g.repos, ...existing.repos])]
byName.set(g.name, { ...g, repos: mergedRepos, source: 'shared' })
} else {
byName.set(g.name, { ...g, source: 'shared' })
}
}
groups = [...byName.values()]
} else {
groups = local.map(g => ({ ...g, source: 'local' }))
}
} catch (err) {
console.error('[Home] Failed to load groups:', err.message)
}
// Compute activity graphs (cached, non-blocking)
const activity = await getActivityData(repoRegistry.gitReposPath, repos).catch(() => ({}))
// Filter private repos for non-admin users
const isAdmin = req.user && req.user.globalPermissions && req.user.globalPermissions.admin
const visibleRepos = repos.filter(r => {
if (r.visibility === 'private' && !isAdmin) return false
return true
})
// Attach activity data to repos
const reposWithActivity = visibleRepos.map(r => ({
...r,
activity: activity[r.name] || null
}))
const content = renderRepoManager(reposWithActivity, scanner.stats, defaultNamespace, pathPrefix, groups)
html(res, content)
}
/**
* Serve user profile page
*/
async function serveProfile(req, res, server) {
const content = renderUserProfile(server.config.pathPrefix)
html(res, content)
}
export { serveHome, serveProfile }