Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- node:sqlite refuse `undefined` et les booléens : passer `null` / `0|1`.
- Synchro (`server/src/sync/`) : toute nouveauté passe par `pushInbox` avec une `external_key` unique ; la première synchro d'une source est « baselinée » (marquée lue) via la table `settings`.
- `pushInbox` remplit aussi `fresh` (entrées nouvelles et non lues) : c'est ce qui part sur ntfy en fin de `syncProject` (`sync/notify.js`). La CI ne génère une entrée qu'aux changements d'état (vert → rouge, rouge → vert).
- Forges (`server/src/sync/forges/`) : un adaptateur par forge (github, gitlab, gitea — Forgejo = gitea) avec la même interface (`fetchRepo`, `fetchCi`, `latestRelease`, `createIssue`, `listOwnerRepos`). Toujours passer par `forgeFor(db, project)` qui résout le token (comptes `config:forges` puis variables d'env). `whoami()` (compte du token) et `fetchMentions` (GitHub) remplissent `github_items.mine` (author, assignee, review, mention) pour les filtres « Me concernant » et la page « Mon GitHub » (`/api/my-github`) ; `fetchPrState` (GitHub) remplit `github_items.pr_state` pour ces PR, et les transitions sur les PR de l'utilisateur deviennent des entrées `pr_status`. Un bug venu du forum fermé par la synchro crée une entrée `forum_reply` (`items.forum_replied_at` une fois répondu). Sujets suivis : table `forum_watches` (`fetchTopic`, entrées `forum_followed`). CI GitHub : seuls les runs `push` / `merge_group` comptent, GitLab ignore les pipelines `schedule`. Checklist de release : `milestones.checklist`, modèle dans `config:release_checklist`. Noms historiques : la table `github_items` et la colonne `projects.github` contiennent les données de toutes les forges.
- Forges (`server/src/sync/forges/`) : un adaptateur par forge (github, gitlab, gitea — Forgejo = gitea) avec la même interface (`fetchRepo`, `fetchCi`, `latestRelease`, `createIssue`, `listOwnerRepos`). Toujours passer par `forgeFor(db, project)` qui résout le token (comptes `config:forges` puis variables d'env). `whoami()` (compte du token) et `fetchMentions` (GitHub) remplissent `github_items.mine` (author, assignee, review, mention) pour les filtres « Me concernant » et la page « Mon GitHub » (`/api/my-github`) ; `fetchPrState` (GitHub) remplit `github_items.pr_state` pour ces PR, et les transitions sur les PR de l'utilisateur deviennent des entrées `pr_status`. Un bug venu du forum fermé par la synchro crée une entrée `forum_reply` (`items.forum_replied_at` une fois répondu). Sujets suivis : table `forum_watches` (`fetchTopic`, entrées `forum_followed`). CI GitHub : seuls les runs `push` / `merge_group` comptent, GitLab ignore les pipelines `schedule`. Portée par projet : `projects.github_scope` (auto | all | mine) → `projects.mine_only` (recalculé par `refreshMineOnly` et à la synchro) ; toute requête qui compte ou liste les `github_items` passe par `VISIBLE_ITEM` (`server/src/visibility.js`). Checklist de release : `milestones.checklist`, modèle dans `config:release_checklist`. Noms historiques : la table `github_items` et la colonne `projects.github` contiennent les données de toutes les forges.
- Source de version `repo` = releases du dépôt du projet (toute forge) ; `github` = autre dépôt GitHub.
- Tâches planifiées (`server/src/jobs.js`, tick 60 s) : résumé hebdo ntfy (`insights.js`) et sauvegardes `VACUUM INTO` (`backup.js`, `BACKUP_DIR`). Le flux `/api/calendar.ics?token=` échappe à `APP_PASSWORD` (clé dans `config:calendar`). Statistiques : `fetchActivity` des forges, agrégées par `stats.js`.
- Pièces jointes (`server/src/uploads.js`) : fichiers dans `UPLOADS_DIR` (défaut `DATA_PATH/uploads`), référencés uniquement depuis le Markdown (`/api/uploads/<fichier>`), sans table ; les orphelins sont supprimés au moment de la sauvegarde. Côté front, `MarkdownInput` remplace `Input.TextArea` pour tout champ Markdown. Vue « Aujourd'hui » : colonne `items.focus_date` (`server/src/today.js`).
Expand Down
4 changes: 2 additions & 2 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ Self-hosted (Docker), single-user, all data in one SQLite file.
| **Projects grouped by category** | Freelance, Personal, Organisation (e.g. Gladys), Other — editable categories with colours. Status (idea, active, maintenance, paused, archived), client, tags, links, pinning. |
| **GitHub, GitLab, Gitea / Forgejo** | Each project points to a repository on github.com, gitlab.com or a self-hosted instance (GitLab, Gitea, Forgejo). One token per instance in **Settings → Forges and tokens**. |
| **Bulk repository import** | Lists the repositories of a user, organisation or group (filter, forks, archived) and creates the projects in one go, with category, status and release tracking. Repositories already tracked are skipped. |
| **Repository tracking** | Open issues and PRs / MRs, stars, last activity. "For me" filters (opened by you, assigned, review requested, mentioned — based on the token's account), by label, author or title, remembered per project: handy on a very active repository like Gladys. **New** ones land in the inbox; a "Track" button adds them to your bugs & tasks. |
| **My GitHub** | One page gathering, across all your projects, the reviews requested from you, your PRs, what is assigned to you, your mentions and your issues. Each PR shows its state: approved, changes requested, conflict with the main branch, CI. On your PRs, an approval, a change request, a conflict or a red CI lands in the inbox and on ntfy. |
| **Repository tracking** | Open issues and PRs / MRs, stars, last activity. "For me" filters (opened by you, assigned, review requested, mentioned — based on the token's account), by label, author or title, remembered per project: handy on a very active repository like Gladys. Per-project "Issues and PRs followed" setting: Auto (default) follows everything on your repositories and only what concerns you on other people's (counters, dashboard, stale list, notifications). **New** ones land in the inbox; a "Track" button adds them to your bugs & tasks. |
| **My GitHub** | One page gathering, across all your projects, the reviews requested from you, your PRs, what is assigned to you, your mentions and your issues. Each PR shows its state: approved, changes requested, conflict with the main branch, CI. One click turns it into a task with an explicit title ("Resolve the conflict: #3028…"). On your PRs, an approval, a change request, a conflict or a red CI lands in the inbox and on ntfy. |
| **Create an issue from a bug** | A bug reported on a forum becomes an issue on the repository (title, description, forum link, target version, label). Linked both ways: when the issue is closed, the bug is marked as done. |
| **CI status** | GitHub Actions (latest run of each workflow), GitLab pipelines, Gitea/Forgejo commit statuses (Actions, Woodpecker, Drone) on the default branch: green/red badge (only runs triggered by the code count: scheduled jobs and manually started workflows are ignored), dashboard alert, notification when CI breaks and when it is green again. |
| **Personal GitHub notifications** | Review requests, mentions, assignments, replies on your issues/PRs (e.g. to GladysAssistant/Gladys), attached to the matching tracked project. Requires a classic token with the `notifications` scope. |
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ Auto-hébergé (Docker), mono-utilisateur, données dans un seul fichier SQLite.
| **Projets classés par catégories** | Freelance, Perso, Organisation (Gladys…), Autre — catégories modifiables avec couleur. Statut (idée, actif, maintenance, pause, archivé), client, tags, liens, épinglage. |
| **GitHub, GitLab, Gitea / Forgejo** | Chaque projet pointe vers un dépôt sur github.com, gitlab.com ou une instance auto-hébergée (GitLab, Gitea, Forgejo). Un token par instance dans **Paramètres → Forges et tokens**. |
| **Import de dépôts en masse** | Liste les dépôts d'un utilisateur, d'une organisation ou d'un groupe (filtre, forks, archivés) et crée les projets d'un coup avec catégorie, statut et suivi des releases. Les dépôts déjà suivis sont ignorés. |
| **Suivi des dépôts** | Issues et PR / MR ouvertes, étoiles, dernière activité. Filtres « Me concernant » (ouvert par toi, assigné, review demandée, mentionné — d'après le compte du token), par label, auteur ou titre, mémorisés par projet : pratique sur un dépôt très actif comme Gladys. Les **nouvelles** arrivent dans la boîte de réception ; un bouton « Suivre » les ajoute aux bugs & tâches. |
| **Mon GitHub** | Une page qui réunit, sur tous tes projets, les reviews qu'on te demande, tes PR, ce qui t'est assigné, tes mentions et tes issues. Chaque PR affiche son état : approuvée, changements demandés, conflit avec la branche principale, CI. Sur tes PR, une approbation, une demande de changements, un conflit ou une CI rouge arrive dans la boîte de réception et sur ntfy. |
| **Suivi des dépôts** | Issues et PR / MR ouvertes, étoiles, dernière activité. Filtres « Me concernant » (ouvert par toi, assigné, review demandée, mentionné — d'après le compte du token), par label, auteur ou titre, mémorisés par projet : pratique sur un dépôt très actif comme Gladys. Réglage par projet « Issues et PR suivies » : en mode Auto (par défaut), tout est suivi sur tes dépôts, et seulement ce qui te concerne sur ceux des autres (compteurs, tableau de bord, « En attente », notifications). Les **nouvelles** arrivent dans la boîte de réception ; un bouton « Suivre » les ajoute aux bugs & tâches. |
| **Mon GitHub** | Une page qui réunit, sur tous tes projets, les reviews qu'on te demande, tes PR, ce qui t'est assigné, tes mentions et tes issues. Chaque PR affiche son état : approuvée, changements demandés, conflit avec la branche principale, CI. Un clic en fait une tâche au titre explicite (« Résoudre le conflit : #3028… »). Sur tes PR, une approbation, une demande de changements, un conflit ou une CI rouge arrive dans la boîte de réception et sur ntfy. |
| **Créer une issue depuis un bug** | Un bug remonté par le forum devient une issue sur le dépôt (titre, description, lien du forum, version cible, label). Lien dans les deux sens : quand l'issue est fermée, le bug passe à « Terminé ». |
| **État de la CI** | GitHub Actions (dernier run de chaque workflow), pipelines GitLab, statuts de commit Gitea/Forgejo (Actions, Woodpecker, Drone) sur la branche principale : pastille verte/rouge (seuls les runs déclenchés par le code comptent : les tâches planifiées et les workflows lancés à la main sont ignorés), alerte au tableau de bord, notification quand la CI casse puis repasse au vert. |
| **Notifications GitHub perso** | Reviews demandées, mentions, assignations, réponses sur tes issues/PR (ex. vers GladysAssistant/Gladys), rattachées au projet suivi correspondant. Nécessite un token classique avec le scope `notifications`. |
Expand Down
8 changes: 7 additions & 1 deletion server/src/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,12 @@ const MIGRATIONS = [
-- Release checklist of a version: [{ label, done }].
ALTER TABLE milestones ADD COLUMN checklist TEXT;
`,
`
-- Which issues / PRs of the repository are followed: auto | all | mine.
ALTER TABLE projects ADD COLUMN github_scope TEXT NOT NULL DEFAULT 'auto';
-- Effective value (1 = only those that concern the user), kept up to date.
ALTER TABLE projects ADD COLUMN mine_only INTEGER NOT NULL DEFAULT 0;
`,
]

export function openDb(file = join(DATA_PATH, 'projectory.db')) {
Expand Down Expand Up @@ -223,7 +229,7 @@ export function decode(row) {
try { out[col] = JSON.parse(out[col]) } catch { /* keep raw */ }
}
}
for (const col of ['pinned', 'draft', 'notify']) if (col in out) out[col] = !!out[col]
for (const col of ['pinned', 'draft', 'notify', 'mine_only']) if (col in out) out[col] = !!out[col]
return out
}

Expand Down
3 changes: 2 additions & 1 deletion server/src/insights.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { VISIBLE_ITEM } from './visibility.js'
import { getConfig } from './db.js'
import { tr } from './i18n.js'
import { compareVersions } from './sync/http.js'
Expand All @@ -17,7 +18,7 @@ export function findStale(db, now = new Date()) {
const base = `SELECT g.id, g.project_id, g.type, g.number, g.title, g.url, g.author, g.comments, g.created_at, g.updated_at,
p.name AS project_name, p.forge
FROM github_items g JOIN projects p ON p.id = g.project_id
WHERE p.status != 'archived'`
WHERE p.status != 'archived' AND ${VISIBLE_ITEM}`
const issues = db.prepare(`${base} AND g.type = 'issue' AND g.comments = 0 AND g.created_at <= ?`).all(daysAgo(issue_days, now))
.map((r) => ({ ...r, reason: 'no_reply', age_days: ageDays(r.created_at, now) }))
const prs = db.prepare(`${base} AND g.type = 'pr' AND g.draft = 0 AND g.updated_at <= ?`).all(daysAgo(pr_days, now))
Expand Down
2 changes: 1 addition & 1 deletion server/src/routes/integrations.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export default async function integrationRoutes(app, { db }) {
project_id: g.project_id,
kind: req.body?.kind || (g.type === 'pr' ? 'task' : 'bug'),
priority: req.body?.priority || 'normal',
title: g.title,
title: String(req.body?.title || '').trim() || g.title,
source: 'github',
issue_number: g.type === 'issue' ? g.number : null,
issue_url: g.url,
Expand Down
7 changes: 4 additions & 3 deletions server/src/routes/overview.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { forgeContext } from '../sync/forges/index.js'
import { tr } from '../i18n.js'
import { findStale } from '../insights.js'
import { todayView } from '../today.js'
import { VISIBLE_ITEM } from '../visibility.js'

const REPO_SOURCES = "'github', 'gitlab', 'gitea'"
const INBOX_SQL = `SELECT x.*, p.name AS project_name FROM inbox x LEFT JOIN projects p ON p.id = x.project_id`
Expand Down Expand Up @@ -59,8 +60,8 @@ export default async function overviewRoutes(app, { db }) {
stats: {
projects: projects.length,
active: projects.filter((p) => p.status === 'active').length,
open_issues: count("SELECT count(*) n FROM github_items WHERE type = 'issue'"),
open_prs: count("SELECT count(*) n FROM github_items WHERE type = 'pr'"),
open_issues: count(`SELECT count(*) n FROM github_items g JOIN projects p ON p.id = g.project_id WHERE g.type = 'issue' AND p.status != 'archived' AND ${VISIBLE_ITEM}`),
open_prs: count(`SELECT count(*) n FROM github_items g JOIN projects p ON p.id = g.project_id WHERE g.type = 'pr' AND p.status != 'archived' AND ${VISIBLE_ITEM}`),
ci_failing: projects.filter((p) => p.ci === 'failure').length,
stale: findStale(db).length,
open_bugs: count("SELECT count(*) n FROM items WHERE kind = 'bug' AND status IN ('todo', 'in_progress')"),
Expand All @@ -80,7 +81,7 @@ export default async function overviewRoutes(app, { db }) {
stale: findStale(db).slice(0, 8),
recent_prs: db.prepare(
`SELECT g.*, p.name AS project_name FROM github_items g JOIN projects p ON p.id = g.project_id
WHERE g.type = 'pr' ORDER BY g.updated_at DESC LIMIT 8`
WHERE g.type = 'pr' AND ${VISIBLE_ITEM} ORDER BY g.updated_at DESC LIMIT 8`
).all().map(decode),
sync: syncState,
}
Expand Down
16 changes: 11 additions & 5 deletions server/src/routes/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@ import { compareVersions } from '../sync/http.js'
import { normalizeRepo } from '../sync/forges/index.js'
import { syncProject } from '../sync/index.js'
import { tr } from '../i18n.js'
import { VISIBLE_ITEM, refreshMineOnly } from '../visibility.js'

const FIELDS = [
'name', 'description', 'category_id', 'status', 'tags', 'client', 'repo', 'homepage', 'links',
'deployed_version', 'version_sources', 'forum_sources', 'pinned', 'notify', 'forge', 'forge_url',
'deployed_version', 'version_sources', 'forum_sources', 'pinned', 'notify', 'forge', 'forge_url', 'github_scope',
]

// Item of Bugs & tasks already created from this issue / PR, if any.
const TRACKED = '(SELECT i.id FROM items i WHERE i.project_id = g.project_id AND i.issue_url = g.url LIMIT 1) AS item_id'

const LIST_SQL = `
SELECT p.*, c.name AS category_name, c.color AS category_color,
(SELECT count(*) FROM items i WHERE i.project_id = p.id AND i.status IN ('todo', 'in_progress')) AS open_items,
(SELECT count(*) FROM items i WHERE i.project_id = p.id AND i.status IN ('todo', 'in_progress') AND i.kind = 'bug') AS open_bugs,
(SELECT count(*) FROM github_items g WHERE g.project_id = p.id AND g.type = 'issue') AS open_issues,
(SELECT count(*) FROM github_items g WHERE g.project_id = p.id AND g.type = 'pr') AS open_prs,
(SELECT count(*) FROM github_items g WHERE g.project_id = p.id AND g.type = 'issue' AND ${VISIBLE_ITEM}) AS open_issues,
(SELECT count(*) FROM github_items g WHERE g.project_id = p.id AND g.type = 'pr' AND ${VISIBLE_ITEM}) AS open_prs,
(SELECT count(*) FROM inbox x WHERE x.project_id = p.id AND x.read_at IS NULL) AS unread
FROM projects p LEFT JOIN categories c ON c.id = p.category_id
`
Expand Down Expand Up @@ -64,12 +68,14 @@ export default async function projectRoutes(app, { db }) {
app.post('/projects', async (req, reply) => {
if (!req.body?.name?.trim()) return reply.code(400).send({ error: tr('Le nom est obligatoire') })
const id = insertRow(db, 'projects', clean(req.body), FIELDS)
refreshMineOnly(db, id)
return reply.code(201).send(listProjects(db, { where: ['p.id = ?'], params: [id] })[0])
})

app.put('/projects/:id', async (req, reply) => {
const id = Number(req.params.id)
if (!updateRow(db, 'projects', id, clean(req.body || {}), FIELDS)) return reply.code(404).send({ error: tr('Projet introuvable') })
refreshMineOnly(db, id)
return listProjects(db, { where: ['p.id = ?'], params: [id] })[0]
})

Expand All @@ -86,7 +92,7 @@ export default async function projectRoutes(app, { db }) {
// Open issues / PRs that concern the user, across every project.
app.get('/my-github', async () =>
db.prepare(
`SELECT g.*, p.name AS project_name, p.forge FROM github_items g JOIN projects p ON p.id = g.project_id
`SELECT g.*, p.name AS project_name, p.forge, ${TRACKED} FROM github_items g JOIN projects p ON p.id = g.project_id
WHERE g.mine != '[]' AND p.status != 'archived' ORDER BY g.updated_at DESC`
).all().map(decode))

Expand All @@ -97,7 +103,7 @@ export default async function projectRoutes(app, { db }) {
if (project_id) { where.push('g.project_id = ?'); params.push(Number(project_id)) }
if (type) { where.push('g.type = ?'); params.push(type) }
return db.prepare(
`SELECT g.*, p.name AS project_name FROM github_items g JOIN projects p ON p.id = g.project_id
`SELECT g.*, p.name AS project_name, ${TRACKED} FROM github_items g JOIN projects p ON p.id = g.project_id
WHERE ${where.join(' AND ')} ORDER BY g.updated_at DESC`
).all(...params).map(decode)
})
Expand Down
Loading
Loading