···616616 deleted text -- timestamp when the domain was released/unclaimed; null means actively claimed
617617 );
618618619619+ create table if not exists repo_sites (
620620+ id integer primary key autoincrement,
621621+ repo_at text not null unique,
622622+ branch text not null,
623623+ dir text not null default '/',
624624+ is_index integer not null default 0,
625625+ created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
626626+ updated 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+630630+ create table if not exists site_deploys (
631631+ id integer primary key autoincrement,
632632+ repo_at text not null,
633633+ branch text not null,
634634+ dir text not null default '/',
635635+ commit_sha text not null default '',
636636+ status text not null check (status in ('success', 'failure')),
637637+ trigger text not null check (trigger in ('config_change', 'push')),
638638+ error text not null default '',
639639+ created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
640640+ foreign key (repo_at) references repos(at_uri) on delete cascade
641641+ );
642642+619643 create table if not exists migrations (
620644 id integer primary key autoincrement,
621645 name text unique
···635659 create index if not exists idx_references_to_at on reference_links(to_at);
636660 create index if not exists idx_webhooks_repo_at on webhooks(repo_at);
637661 create index if not exists idx_webhook_deliveries_webhook_id on webhook_deliveries(webhook_id);
662662+ create index if not exists idx_site_deploys_repo_at on site_deploys(repo_at);
638663 `)
639664 if err != nil {
640665 return nil, err
···1258128312591284 -- rename new table
12601285 alter table profile_stats_new rename to profile_stats;
12611261- `)
12621262- return err
12631263- })
12641264-12651265- orm.RunMigration(conn, logger, "add-repo-sites-table", func(tx *sql.Tx) error {
12661266- _, err := tx.Exec(`
12671267- create table if not exists repo_sites (
12681268- id integer primary key autoincrement,
12691269- repo_at text not null unique,
12701270- branch text not null,
12711271- dir text not null default '/',
12721272- is_index integer not null default 0,
12731273- created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
12741274- updated text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
12751275- foreign key (repo_at) references repos(at_uri) on delete cascade
12761276- );
12771286 `)
12781287 return err
12791288 })
+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+}