feat: git push status indicator + multi-select bulk push
Release APK / build (push) Has been cancelled

- DB migration v2: git_pushed_at column on incidents
- markIncidentPushed(id): stamps the push timestamp
- loadGitConfig(): shared helper in lib/git.ts, removes duplication
- Home screen: colored dot per incident (red=never pushed,
  orange=dirty/modified after push, green=up to date);
  dot visible only when git is configured
- Home screen: long-press enters selection mode, tap toggles;
  action bar with Select unpushed / Select all shortcuts +
  bulk push with sequential progress counter
- [id].tsx: marks incident pushed after successful push,
  updates local state without reload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 18:11:23 +02:00
parent 1437e99cbb
commit 947ae5917c
5 changed files with 392 additions and 139 deletions
+18
View File
@@ -45,6 +45,13 @@ async function migrate(db: SQLite.SQLiteDatabase): Promise<void> {
INSERT INTO schema_version (version) VALUES (1);
`);
}
if (current < 2) {
await db.execAsync(`
ALTER TABLE incidents ADD COLUMN git_pushed_at TEXT;
INSERT INTO schema_version (version) VALUES (2);
`);
}
}
// --- row mapper ---
@@ -60,6 +67,7 @@ interface IncidentRow {
created_at: string;
updated_at: string;
tags: string;
git_pushed_at: string | null;
}
function rowToIncident(row: IncidentRow): Incident {
@@ -74,6 +82,7 @@ function rowToIncident(row: IncidentRow): Incident {
createdAt: row.created_at,
updatedAt: row.updated_at,
tags: JSON.parse(row.tags) as string[],
gitPushedAt: row.git_pushed_at ?? null,
};
}
@@ -107,6 +116,7 @@ export async function createIncident(draft: IncidentDraft): Promise<Incident> {
id,
createdAt: now,
updatedAt: now,
gitPushedAt: null,
};
}
@@ -163,6 +173,14 @@ export async function deleteIncident(id: string): Promise<void> {
await db.runAsync("DELETE FROM incidents WHERE id = ?", [id]);
}
export async function markIncidentPushed(id: string): Promise<void> {
const db = await getDb();
await db.runAsync(
"UPDATE incidents SET git_pushed_at = ? WHERE id = ?",
[new Date().toISOString(), id]
);
}
export async function getMaxTitleNumber(prefix: string): Promise<number> {
const db = await getDb();
const rows = await db.getAllAsync<{ title: string }>(
+39
View File
@@ -1,3 +1,4 @@
import * as SecureStore from "expo-secure-store";
import { incidentToFilename, renderMarkdown } from "./markdown";
import type { Incident } from "@/types/incident";
@@ -19,6 +20,44 @@ export interface PushResult {
const TIMEOUT_MS = 15_000;
export async function loadGitConfig(): Promise<GitConfig | null> {
const [provider, gu, gt, go, gr, ghu, ghown, ghrepo, glu, glt, glo, glr] =
await Promise.all([
SecureStore.getItemAsync("git_provider"),
SecureStore.getItemAsync("gitea_url"),
SecureStore.getItemAsync("gitea_token"),
SecureStore.getItemAsync("gitea_owner"),
SecureStore.getItemAsync("gitea_repo"),
SecureStore.getItemAsync("github_token"),
SecureStore.getItemAsync("github_owner"),
SecureStore.getItemAsync("github_repo"),
SecureStore.getItemAsync("gitlab_url"),
SecureStore.getItemAsync("gitlab_token"),
SecureStore.getItemAsync("gitlab_owner"),
SecureStore.getItemAsync("gitlab_repo"),
]);
const p = (provider ?? "gitea") as GitProvider;
let url: string | undefined;
let token: string | null;
let owner: string | null;
let repo: string | null;
if (p === "gitea") {
url = gu ?? undefined;
token = gt; owner = go; repo = gr;
} else if (p === "github") {
token = ghu; owner = ghown; repo = ghrepo;
} else {
url = glu ?? undefined;
token = glt; owner = glo; repo = glr;
}
const needsUrl = p === "gitea" || p === "gitlab";
if (!token || !owner || !repo || (needsUrl && !url)) return null;
return { provider: p, url, token, owner, repo };
}
function fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);