OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / routes / home.js

137 lines · javascript ref:
/**
 * 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 }