Projekte: neue Gitea-Repos beim Oeffnen der Projekte-Seite abholen; Review-Kleinigkeiten

- Projekte-Seite holt neue/archivierte Repos selbst ab (vorher bis zu 10 Minuten unsichtbar)
- Import-Drossel: nach einem Fehler sofort neuer Versuch
- Zuordnen-Assistent nennt den Status einer vorhandenen Preview
- Ticket "Neues Projekt": ungueltige Subdomain wird abgewiesen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-04 13:33:40 +00:00
1 parent f4c8b235b9
commit c262268015
5 files changed
+27 -8

No files matched your search

+4
View File
@@ -135,6 +135,10 @@ export default function TicketProjects({ ticket }) {
const create = (e) => { const create = (e) => {
e.preventDefault() e.preventDefault()
if (!slugify(createForm.subdomain)) {
setError('Bitte eine gültige Subdomain eingeben (Buchstaben, Ziffern, Bindestrich).')
return
}
run(async () => { run(async () => {
const subdomain = slugify(createForm.subdomain) const subdomain = slugify(createForm.subdomain)
await createProjectFromTemplate({ await createProjectFromTemplate({
+7 -5
View File
@@ -1,5 +1,7 @@
// Ein Aufruf hoechstens alle `maxAgeMs`: ein laufender Aufruf wird geteilt, danach liefert die // Ein Aufruf hoechstens alle `maxAgeMs`: ein laufender Aufruf wird geteilt, danach liefert die
// Funktion bis zum Ablauf null (uebersprungen). `now` nur fuer Tests. // Funktion bis zum Ablauf null (uebersprungen). Nach einem Fehler darf sofort neu versucht
// werden (sonst verschwand der Hinweis und der Abgleich fiel eine Minute lang still aus).
// `now` nur fuer Tests.
export function throttleRuns(run, maxAgeMs) { export function throttleRuns(run, maxAgeMs) {
let pending = null let pending = null
let doneAt = 0 let doneAt = 0
@@ -8,10 +10,10 @@ export function throttleRuns(run, maxAgeMs) {
if (doneAt && now - doneAt < maxAgeMs) return Promise.resolve(null) if (doneAt && now - doneAt < maxAgeMs) return Promise.resolve(null)
pending = Promise.resolve() pending = Promise.resolve()
.then(run) .then(run)
.finally(() => { .then(
doneAt = Date.now() (result) => { doneAt = Date.now(); pending = null; return result },
pending = null (err) => { doneAt = 0; pending = null; throw err }
}) )
return pending return pending
} }
} }
+3 -1
View File
@@ -357,7 +357,9 @@ export default function AssignProjectPage() {
</ul> </ul>
{isWebsiteWithCustomer && hasPreviewAlready && ( {isWebsiteWithCustomer && hasPreviewAlready && (
<p className="muted" style={{ marginTop: 0 }}> <p className="muted" style={{ marginTop: 0 }}>
Preview läuft bereits unter https://{project.subdomain}.project.webklar.com und bleibt unverändert. {['deployed', 'ready'].includes(String(project.status || '').toLowerCase())
? <>Preview läuft bereits unter https://{project.subdomain}.project.webklar.com und bleibt unverändert.</>
: <>Preview ist unter https://{project.subdomain}.project.webklar.com eingerichtet (Status: {project.status || 'unbekannt'}). Neu bauen geht im Projekt über „Preview".</>}
</p> </p>
)} )}
{isWebsiteWithCustomer && !hasPreviewAlready && ( {isWebsiteWithCustomer && !hasPreviewAlready && (
+8
View File
@@ -24,6 +24,7 @@ import {
updateWebsiteProject, updateWebsiteProject,
archiveWebsiteProject, archiveWebsiteProject,
unarchiveWebsiteProject, unarchiveWebsiteProject,
importNewReposThrottled,
} from '../lib/projectAdminApi' } from '../lib/projectAdminApi'
import { assignSummary, ticketIdForCustomer } from '../lib/projectAssign' import { assignSummary, ticketIdForCustomer } from '../lib/projectAssign'
import { projectCounts, projectsInView } from '../lib/projectList' import { projectCounts, projectsInView } from '../lib/projectList'
@@ -116,8 +117,15 @@ export default function ProjectsPage() {
const [editError, setEditError] = useState('') const [editError, setEditError] = useState('')
const [archiveBusy, setArchiveBusy] = useState(false) const [archiveBusy, setArchiveBusy] = useState(false)
// Erst die bekannte Liste zeigen, dann neue/archivierte Gitea-Repos abholen und neu laden -
// sonst fehlte ein frisch angelegtes Repo hier bis zum naechsten 10-Minuten-Lauf.
useEffect(() => { useEffect(() => {
let cancelled = false
fetchAllProjects() fetchAllProjects()
importNewReposThrottled()
.then((r) => { if (r && !cancelled) fetchAllProjects() })
.catch(() => { /* Import-Fehler zeigt die Ticketkarte; die Liste bleibt wie sie ist */ })
return () => { cancelled = true }
}, [fetchAllProjects]) }, [fetchAllProjects])
const customerMap = useMemo(() => { const customerMap = useMemo(() => {
+5 -2
View File
@@ -24,8 +24,11 @@ test('Repo-Import hoechstens einmal pro Minute, laufender Abgleich wird geteilt'
assert.equal(runs, 2) assert.equal(runs, 2)
}) })
test('Fehler wird weitergereicht, der naechste Versuch wartet trotzdem die Minute ab', async () => { test('Fehler wird weitergereicht, danach darf sofort neu versucht werden', async () => {
const throttled = throttleRuns(async () => { throw new Error('weg') }, 60000) let runs = 0
const throttled = throttleRuns(async () => { runs += 1; if (runs === 1) throw new Error('weg'); return { ok: true } }, 60000)
await assert.rejects(throttled(), /weg/) await assert.rejects(throttled(), /weg/)
assert.deepEqual(await throttled(), { ok: true })
assert.equal(runs, 2)
assert.equal(await throttled(), null) assert.equal(await throttled(), null)
}) })