···603603 deleted text -- timestamp when the domain was released/unclaimed; null means actively claimed
604604 );
605605606606+ create table if not exists repo_sites (
607607+ id integer primary key autoincrement,
608608+ repo_at text not null unique,
609609+ branch text not null,
610610+ dir text not null default '/',
611611+ is_index integer not null default 0,
612612+ created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
613613+ updated text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
614614+ foreign key (repo_at) references repos(at_uri) on delete cascade
615615+ );
616616+617617+ create table if not exists site_deploys (
618618+ id integer primary key autoincrement,
619619+ repo_at text not null,
620620+ branch text not null,
621621+ dir text not null default '/',
622622+ commit_sha text not null default '',
623623+ status text not null check (status in ('success', 'failure')),
624624+ trigger text not null check (trigger in ('config_change', 'push')),
625625+ error text not null default '',
626626+ created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
627627+ foreign key (repo_at) references repos(at_uri) on delete cascade
628628+ );
629629+606630 create table if not exists migrations (
607631 id integer primary key autoincrement,
608632 name text unique
···622646 create index if not exists idx_references_to_at on reference_links(to_at);
623647 create index if not exists idx_webhooks_repo_at on webhooks(repo_at);
624648 create index if not exists idx_webhook_deliveries_webhook_id on webhook_deliveries(webhook_id);
649649+ create index if not exists idx_site_deploys_repo_at on site_deploys(repo_at);
625650 `)
626651 if err != nil {
627652 return nil, err
···1245127012461271 -- rename new table
12471272 alter table profile_stats_new rename to profile_stats;
12481248- `)
12491249- return err
12501250- })
12511251-12521252- orm.RunMigration(conn, logger, "add-repo-sites-table", func(tx *sql.Tx) error {
12531253- _, err := tx.Exec(`
12541254- create table if not exists repo_sites (
12551255- id integer primary key autoincrement,
12561256- repo_at text not null unique,
12571257- branch text not null,
12581258- dir text not null default '/',
12591259- is_index integer not null default 0,
12601260- created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
12611261- updated text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
12621262- foreign key (repo_at) references repos(at_uri) on delete cascade
12631263- );
12641273 `)
12651274 return err
12661275 })
+102
appview/db/site_deploys.go
···11+package db
22+33+import (
44+ "fmt"
55+ "time"
66+77+ "tangled.org/core/appview/models"
88+)
99+1010+// AddSiteDeploy records a site deploy attempt.
1111+func AddSiteDeploy(e Execer, deploy *models.SiteDeploy) error {
1212+ result, err := e.Exec(`
1313+ insert into site_deploys (
1414+ repo_at,
1515+ branch,
1616+ dir,
1717+ commit_sha,
1818+ status,
1919+ trigger,
2020+ error
2121+ ) values (?, ?, ?, ?, ?, ?, ?)
2222+ `,
2323+ deploy.RepoAt,
2424+ deploy.Branch,
2525+ deploy.Dir,
2626+ deploy.CommitSHA,
2727+ string(deploy.Status),
2828+ string(deploy.Trigger),
2929+ deploy.Error,
3030+ )
3131+ if err != nil {
3232+ return fmt.Errorf("failed to insert site deploy: %w", err)
3333+ }
3434+3535+ id, err := result.LastInsertId()
3636+ if err != nil {
3737+ return fmt.Errorf("failed to get site deploy id: %w", err)
3838+ }
3939+4040+ deploy.Id = id
4141+ return nil
4242+}
4343+4444+// GetSiteDeploys returns recent deploy records for a repository, newest first.
4545+func GetSiteDeploys(e Execer, repoAt string, limit int) ([]models.SiteDeploy, error) {
4646+ if limit <= 0 {
4747+ limit = 20
4848+ }
4949+5050+ rows, err := e.Query(`
5151+ select
5252+ id,
5353+ repo_at,
5454+ branch,
5555+ dir,
5656+ commit_sha,
5757+ status,
5858+ trigger,
5959+ error,
6060+ created_at
6161+ from site_deploys
6262+ where repo_at = ?
6363+ order by created_at desc
6464+ limit ?
6565+ `, repoAt, limit)
6666+ if err != nil {
6767+ return nil, fmt.Errorf("failed to query site deploys: %w", err)
6868+ }
6969+ defer rows.Close()
7070+7171+ var deploys []models.SiteDeploy
7272+ for rows.Next() {
7373+ var d models.SiteDeploy
7474+ var createdAt string
7575+7676+ if err := rows.Scan(
7777+ &d.Id,
7878+ &d.RepoAt,
7979+ &d.Branch,
8080+ &d.Dir,
8181+ &d.CommitSHA,
8282+ &d.Status,
8383+ &d.Trigger,
8484+ &d.Error,
8585+ &createdAt,
8686+ ); err != nil {
8787+ return nil, fmt.Errorf("failed to scan site deploy: %w", err)
8888+ }
8989+9090+ if t, err := time.Parse(time.RFC3339, createdAt); err == nil {
9191+ d.CreatedAt = t
9292+ }
9393+9494+ deploys = append(deploys, d)
9595+ }
9696+9797+ if err := rows.Err(); err != nil {
9898+ return nil, fmt.Errorf("failed to iterate site deploys: %w", err)
9999+ }
100100+101101+ return deploys, nil
102102+}
+72
appview/db/sites.go
···196196 _, err := e.Exec(`delete from repo_sites where repo_at = ?`, repoAt)
197197 return err
198198}
199199+200200+// GetRepoSiteConfigsForDid returns all site configurations for repos owned by a DID.
201201+// RepoName is populated on each returned RepoSite.
202202+func GetRepoSiteConfigsForDid(e Execer, did string) ([]*models.RepoSite, error) {
203203+ rows, err := e.Query(`
204204+ select rs.id, rs.repo_at, r.name, rs.branch, rs.dir, rs.is_index, rs.created, rs.updated
205205+ from repo_sites rs
206206+ join repos r on r.at_uri = rs.repo_at
207207+ where r.did = ?
208208+ `, did)
209209+ if err != nil {
210210+ return nil, err
211211+ }
212212+ defer rows.Close()
213213+214214+ var sites []*models.RepoSite
215215+ for rows.Next() {
216216+ var s models.RepoSite
217217+ var isIndex int
218218+ var createdStr, updatedStr string
219219+ if err := rows.Scan(&s.ID, &s.RepoAt, &s.RepoName, &s.Branch, &s.Dir, &isIndex, &createdStr, &updatedStr); err != nil {
220220+ return nil, err
221221+ }
222222+ s.IsIndex = isIndex != 0
223223+ s.Created, err = time.Parse(time.RFC3339, createdStr)
224224+ if err != nil {
225225+ return nil, fmt.Errorf("parsing created timestamp: %w", err)
226226+ }
227227+ s.Updated, err = time.Parse(time.RFC3339, updatedStr)
228228+ if err != nil {
229229+ return nil, fmt.Errorf("parsing updated timestamp: %w", err)
230230+ }
231231+ sites = append(sites, &s)
232232+ }
233233+ return sites, rows.Err()
234234+}
235235+236236+// DeleteRepoSiteConfigsForDid removes all site configurations for repos owned by a DID.
237237+func DeleteRepoSiteConfigsForDid(e Execer, did string) error {
238238+ _, err := e.Exec(`
239239+ delete from repo_sites
240240+ where repo_at in (
241241+ select at_uri from repos where did = ?
242242+ )
243243+ `, did)
244244+ return err
245245+}
246246+247247+// GetIndexRepoAtForDid returns the repo_at of the repo that currently holds
248248+// is_index=1 for the given DID, excluding excludeRepoAt (the current repo).
249249+// Returns "", nil if no other repo is the index site.
250250+func GetIndexRepoAtForDid(e Execer, did, excludeRepoAt string) (string, error) {
251251+ row := e.QueryRow(`
252252+ select rs.repo_at
253253+ from repo_sites rs
254254+ join repos r on r.at_uri = rs.repo_at
255255+ where r.did = ?
256256+ and rs.is_index = 1
257257+ and rs.repo_at != ?
258258+ limit 1
259259+ `, did, excludeRepoAt)
260260+261261+ var repoAt string
262262+ err := row.Scan(&repoAt)
263263+ if errors.Is(err, sql.ErrNoRows) {
264264+ return "", nil
265265+ }
266266+ if err != nil {
267267+ return "", err
268268+ }
269269+ return repoAt, nil
270270+}