Make the home page's AS name search work (closes #151)
check / check (push) Canceled after 0s

The AS Name Search card on the home page said the search was coming
soon. It now asks the new GET /api/v1/search/as?q=<text>, which
searches the AS list built into asinfo, and lists the matches as links
to their AS pages. An empty q answers 400. asinfo.Search now ignores
case and returns its matches in AS number order. asinfo lowers each
handle and description once, when the list loads.

A match can be an AS that the database has no record of. Its AS page
and /api/v1/as/{asn} then take its handle and description from asinfo
and still list its prefixes. They answer 404 only for an AS with no
record, no asinfo entry and no prefix. Empty prefix lists in that JSON
answer are [] rather than null.

Model: opus-5-5
This commit is contained in:
2026-10-26 20:58:05 +00:00
parent 71ee2aeda7
commit 72034907a7
15 changed files with 730 additions and 77 deletions
+29 -7
View File
@@ -1748,6 +1748,11 @@ func (d *Database) GetASDetails(asn int) (*ASN, []LiveRoute, error) {
// GetASDetailsContext returns the AS and the prefixes it originates, the IPv4
// ones first, each once with the time of its newest route. It makes two index
// searches per prefix, however many peers carry the prefix.
//
// The routes of an AS can be stored before its record in the asns table. When
// the AS has no record, its handle and description come from the AS info
// lookup, and it has no seen times. An AS with no record, no lookup entry and
// no prefix is ErrNoRoute.
func (d *Database) GetASDetailsContext(
ctx context.Context, asn int,
) (*ASN, []LiveRoute, error) {
@@ -1763,17 +1768,28 @@ func (d *Database) GetASDetailsContext(
asn,
).Scan(&asnInfo.ASN, &handle, &description, &asnInfo.FirstSeen,
&asnInfo.LastSeen)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil, fmt.Errorf("%w: AS%d", ErrNoRoute, asn)
// known is whether the asns table or the AS info lookup holds the AS.
known := true
switch {
case errors.Is(err, sql.ErrNoRows):
asnInfo.ASN = asn
info, ok := asinfo.Get(asn)
if ok {
asnInfo.Handle = info.Handle
asnInfo.Description = info.Description
}
known = ok
case err != nil:
return nil, nil, fmt.Errorf("failed to query AS: %w", err)
default:
asnInfo.Handle = handle.String
asnInfo.Description = description.String
}
asnInfo.Handle = handle.String
asnInfo.Description = description.String
allPrefixes, err := d.originPrefixes(ctx, asn, ipVersionV4)
if err != nil {
return &asnInfo, nil, err
@@ -1784,7 +1800,13 @@ func (d *Database) GetASDetailsContext(
return &asnInfo, allPrefixes, err
}
return &asnInfo, append(allPrefixes, prefixesV6...), nil
allPrefixes = append(allPrefixes, prefixesV6...)
if !known && len(allPrefixes) == 0 {
return nil, nil, fmt.Errorf("%w: AS%d", ErrNoRoute, asn)
}
return &asnInfo, allPrefixes, nil
}
// ASPeer represents a peering relationship with another AS including handle,