9.6 KiB
This file documents the Gitea issue-tracker integration subsystem. Claude Code auto-loads it when a file under devplacepy/services/gitea/ is read or edited.
Issue tracker (services/gitea/)
The /issues feature is a full Gitea integration with no local issue store - there is no local issue store; the listing and detail views read issues straight from Gitea (default repo retoor/devplacepy on retoor.molodetz.nl) with live status. All connection settings live in site_settings and are admin-editable on the IssueTrackerService config at /admin/services.
Package services/gitea/
config.py- the admin-editableCONFIG_FIELDS(Gitea base URL/owner/repo/token, AI enhance toggle/model/key) plusgitea_config()-> frozenGiteaConfig(withapi_base/repo_base/is_configured) andis_configured(). The token is one shared bot account; per-user attribution is done by storing names in DevPlace, not by per-user Gitea accounts.client.py- asyncGiteaClientover the Gitea REST API (Authorization: token):create_issue,list_issues(state,page,limit)returning(issues, total)fromX-Total-Countand filtering out pull requests,get_issue,list_comments,create_comment,set_state. RaisesGiteaError(message, status)on any non-2xx or transport error.fake.py- in-memoryFakeGiteaClientmirroring the same async interface (plusadd_external_commentto simulate a developer reply); the test backend/double.runtime.py-get_client()/set_client()swap (tests inject the fake).get_client()builds a freshGiteaClientfrom current config unless overridden.enhance.py-enhance_ticket(title, description, config)rewrites a raw report into a consistent markdown ticket via the internal AI gateway (INTERNAL_GATEWAY_URL, key defaults to the gateway internal key): a level-2-heading markdown body (Summary/Steps to Reproduce/Expected Behaviour/Actual Behaviour/Environment) and a tightened title. It is fail-soft to a deterministic template: any error or unparseable response falls back to a deterministic template built from the original text (EnhancedTicket.enhanced=False). This is the AI attachment point - the gateway, like every other AI consumer.store.py- the only DB state, a thin local mapping:issue_tickets(gitea_number -> author_uid + original text + cachedlast_status/last_comment_countfor update detection) andissue_comment_authors(gitea_comment_id -> author_uid, for display attribution and to tell DevPlace-authored comments from developer replies). Helpers:record_ticket,get_ticket,author_uid_for_issue,author_map,record_comment_author,comment_author_map,local_comment_ids,tracked_tickets,update_ticket_cache. Both tables are inSOFT_DELETE_TABLES;init_dbensures their full column set + indexes (so queries are safe on a fresh DB before any insert).service.py-IssueTrackerService(BaseService)(default_enabled=False, holdsCONFIG_FIELDS), polls tracked tickets and notifies the reporter on developer replies / status changes. Each tick it polls every tracked ticket: when the Gitea comment count grew, it loads comments and notifies the reporter if any of the new tail comments are not inlocal_comment_ids(a developer reply); when the state changed it notifies the reporter (closed/reopened). It then updates the cache. This is the single source of update notifications to the author (issue.sync.reply/issue.sync.status).
Filing (async job)
Filing is an async job (services/jobs/issue_create_service.py IssueCreateService, kind issue_create): process loads the user, enhances the report, appends a Reported by **user** via DevPlace footer, creates the Gitea issue, records the local mapping, notifies the reporter (Your issue report was filed as #N), and audits issue.create. cleanup is a no-op (the issue is permanent; only the job row is swept). POST /issues/create enqueues and returns {uid, status_url}; frontend app.issueReporter (static/js/IssueReporter.js) submits the create form, polls /issues/jobs/{uid} via JobPoller, and redirects to /issues/{number}.
Routes (routers/issues/ package, prefix /issues)
GET /issues- Gitea list,?state=open|closed|all&page=, admin-stylebuild_pagination+_pagination.htmlwith astate=prefix; renders an empty-state notice when unconfigured/unreachable.POST /issues/create- enqueue (see Filing above).GET /issues/jobs/{uid}-IssueJobOut.GET /issues/{number}-issue_detail.html: issue body + comments rendered as markdown viadata-render, decorated with DevPlace authors or adevbadge.POST /issues/{number}/comment- synchronous: posts the comment to Gitea attributed to the user, records the author mapping, notifies all admins, auditsissue.comment.POST /issues/{number}/status- admin-only open/closed via Gitea PATCH, auditsissue.status; non-admins get adeniedaudit + 403.
Comments and status changes are deliberately synchronous (one fast Gitea call, immediate redirect); only the AI-heavy create is a job. Status-change notifications to the author are NOT emitted by the route - the poller detects the diff and notifies, so the admin (or external developer) closing an issue both flow through one path.
Content rendering and mentions (frontend reuse, no new modules)
The issue body and every comment render through the platform content pipeline like posts/comments - each is a <div class="...rendered-content" data-render>{{ body|comment.body }}</div> (issue_detail.html), so the globally-instantiated ContentEnhancer (app.content) runs contentRenderer.applyTo (marked -> DOMPurify -> highlight.js -> media/autolink) over Gitea-sourced markdown client-side from element.textContent. Gitea content is NEVER marked | safe into raw HTML; Jinja autoescapes it and the DOMPurify step inside ContentRenderer is the XSS control, identical to user posts. The issue authoring textareas (#issue-description in the create modal on issues.html, and the comment textarea on issue_detail.html) carry the data-mention attribute, so the same ContentEnhancer.initMentionInputs() -> MentionInput flow that wires post @-mention autocomplete (debounced GET /profile/search?q= dropdown) attaches to them with no new code. There is no local issue-edit form (issues are AI-created then commented), so "edit" reuses nothing further. mention.css is loaded globally in base.html.
Attachments (mirror: local store + Gitea assets, routers/issues/attachments.py)
Issues and issue comments accept file attachments, with full add/delete CRUD allowed only while the issue is open. Storage is a mirror - the canonical copy is the platform attachment system (attachments table, keyed target_type="issue"/target_uid=str(number) and target_type="issue_comment"/target_uid=str(comment_id)), and each file is also pushed to the Gitea native asset API so it appears on the tracker. The local row carries a gitea_asset_id back-reference (added to the init_db attachments ensure-block + born-live in store_attachment) so a later delete removes both copies; attachments.mirror_attachment_to_gitea(uid) / remove_gitea_asset(row) branch on target_type to call GiteaClient.create_issue_asset/create_comment_asset / delete_issue_asset/delete_comment_asset (all best-effort - the local copy and the user action survive any Gitea failure). Upload reuses the canonical orphan-then-link path: dp-upload stores to /uploads/upload, the form carries attachment_uids, and the handler link_attachments(uids, target_type, target_uid) then mirrors.
Role/state enforcement: require_user blocks guests; add validates each uid is the caller's own unlinked orphan (admins any); delete is owner-or-admin on attachments.user_uid; every mutation re-checks the live Gitea state == "open" (closed -> 409) and audits denied branches. Attach-at-creation: IssueForm.attachment_uids rides into the issue_create job, which links + mirrors once the issue number exists; IssueCommentForm.attachment_uids links + mirrors inline after the comment is created (synchronous comment path).
Routes: GET/POST /issues/{number}/attachments, DELETE /issues/{number}/attachments/{uid}, and the /issues/{number}/comments/{cid}/attachments[...] comment variants. Frontend app.issueAttachments (static/js/IssueAttachments.js) submits the add form (form[data-issue-attach] -> POST attachment_uids -> reload) and handles per-attachment delete ([data-attachment-delete] -> confirm -> DELETE). The shared render partial is _issue_attachments.html (gallery + per-item delete gated by can_modify).
Note: for Gitea to accept every file type the instance app.ini [attachment] ALLOWED_TYPES must be */* (infra, out of app scope); the DevPlace allow-list is the allowed_file_types setting, and the local copy is kept even when Gitea rejects a type. Audit events issue.attachment.add/issue.attachment.delete (category content). Devii tools list_issue_attachments, add_issue_attachment, delete_issue_attachment (+ add_comment_attachment/delete_comment_attachment); the two deletes are in CONFIRM_REQUIRED.
Schemas, forms, and tools
Schemas: IssueItemOut/IssueCommentOut/IssueDetailOut/IssueAttachmentsOut/IssuesOut/IssueJobOut (schemas.py). Forms: IssueForm/IssueCommentForm/IssueAttachmentForm/IssueStatusForm (models/community.py). Devii tools: list_issues, create_issue, view_issue, comment_issue, set_issue_status (admin), plus the attachment tools above. Docs: the issues API group in docs_api.py. Footer link in base.html under .site-footer.