index.twig 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. {% extends 'layout.twig' %}
  2. {% block title %}Search — IRDB{% endblock %}
  3. {% block content %}
  4. {% import 'partials/sort.twig' as sort %}
  5. <div class="mx-auto max-w-5xl">
  6. <div class="flex items-baseline justify-between gap-4">
  7. <h1 class="text-2xl font-semibold tracking-tight">Search</h1>
  8. {% if has_query %}
  9. <span class="text-sm text-slate-500 dark:text-slate-400">
  10. Query: <span class="font-mono text-slate-700 dark:text-slate-200">{{ query }}</span>
  11. </span>
  12. {% endif %}
  13. </div>
  14. <form method="get" action="/app/search" class="mt-4">
  15. <label for="search-q" class="sr-only">Search IPs</label>
  16. <div class="flex gap-2">
  17. <input type="search" id="search-q" name="q" value="{{ query }}"
  18. placeholder="Enter an IP, prefix, or CIDR (e.g. 203.0.113 or 10.0.0.0/8)"
  19. class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm font-mono dark:border-slate-700 dark:bg-slate-900"
  20. autofocus>
  21. <button type="submit" class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500">Search</button>
  22. </div>
  23. </form>
  24. {% if not has_query %}
  25. <p class="mt-6 text-sm text-slate-500 dark:text-slate-400">
  26. Enter an IP address or prefix to search across the IPs list, manual blocks, and allowlist.
  27. </p>
  28. {% else %}
  29. {# ------------------------------- IPs --------------------------------- #}
  30. <section class="mt-8">
  31. <div class="flex items-center justify-between">
  32. <h2 class="text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
  33. IPs <span class="ml-2 rounded bg-slate-100 px-1.5 py-0.5 font-mono text-xs text-slate-700 dark:bg-slate-800 dark:text-slate-300">{{ ips ? ips.total : 0 }}</span>
  34. </h2>
  35. <a href="/app/ips?q={{ query|url_encode }}" class="text-sm text-indigo-600 hover:underline dark:text-indigo-400">
  36. Open in IPs &rsaquo;
  37. </a>
  38. </div>
  39. {% if errors.ips is defined %}
  40. <div class="mt-2 rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-300">
  41. Failed to load IPs: {{ errors.ips }}
  42. </div>
  43. {% elseif ips and ips.items|length > 0 %}
  44. <div class="mt-3 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
  45. <table class="w-full text-sm" data-sortable-table="search-ips">
  46. <thead class="border-b border-slate-200 bg-slate-50 text-left text-xs uppercase tracking-wider text-slate-500 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-400">
  47. <tr>
  48. {{ sort.th('IP', 'ip') }}
  49. {{ sort.th('Top category', 'top_category') }}
  50. {{ sort.th('Max score', 'max_score', 'number', 'right') }}
  51. {{ sort.th('Last report', 'last_report', 'date') }}
  52. {{ sort.th('Status', 'status') }}
  53. </tr>
  54. </thead>
  55. <tbody class="divide-y divide-slate-100 dark:divide-slate-800">
  56. {% for item in ips.items %}
  57. <tr>
  58. <td class="px-4 py-2" data-sort-value="{{ item.ip }}"><a href="/app/ips/{{ item.ip|url_encode }}" class="font-mono text-indigo-600 hover:underline dark:text-indigo-400">{{ item.ip }}</a></td>
  59. <td class="px-4 py-2 font-mono text-slate-600 dark:text-slate-300" data-sort-value="{{ item.topCategory|default('') }}">{{ item.topCategory|default('—') }}</td>
  60. <td class="px-4 py-2 text-right font-mono" data-sort-value="{{ item.maxScore }}">{{ item.maxScore|number_format(2) }}</td>
  61. <td class="px-4 py-2 text-slate-500 dark:text-slate-400" data-sort-value="{{ item.lastReportAt|default('') }}">{% if item.lastReportAt %}<time class="irdb-dt" datetime="{{ item.lastReportAt }}">{{ item.lastReportAt }}</time>{% else %}—{% endif %}</td>
  62. <td class="px-4 py-2 font-mono text-xs uppercase" data-sort-value="{{ item.status }}">{{ item.status }}</td>
  63. </tr>
  64. {% endfor %}
  65. </tbody>
  66. </table>
  67. </div>
  68. {% if ips.total > ips.items|length %}
  69. <p class="mt-2 text-xs text-slate-500 dark:text-slate-400">
  70. Showing {{ ips.items|length }} of {{ ips.total }} —
  71. <a href="/app/ips?q={{ query|url_encode }}" class="text-indigo-600 hover:underline dark:text-indigo-400">view all</a>.
  72. </p>
  73. {% endif %}
  74. {% else %}
  75. <p class="mt-3 text-sm text-slate-500 dark:text-slate-400">No IPs match this query.</p>
  76. {% endif %}
  77. </section>
  78. {# --------------------------- Manual blocks --------------------------- #}
  79. <section class="mt-8">
  80. <div class="flex items-center justify-between">
  81. <h2 class="text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
  82. Manual blocks <span class="ml-2 rounded bg-slate-100 px-1.5 py-0.5 font-mono text-xs text-slate-700 dark:bg-slate-800 dark:text-slate-300">{{ manual_blocks|length }}</span>
  83. </h2>
  84. <a href="/app/manual-blocks" class="text-sm text-indigo-600 hover:underline dark:text-indigo-400">
  85. Open Manual blocks &rsaquo;
  86. </a>
  87. </div>
  88. {% if errors.manual_blocks is defined %}
  89. <div class="mt-2 rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-300">
  90. Failed to load manual blocks: {{ errors.manual_blocks }}
  91. </div>
  92. {% elseif manual_blocks|length > 0 %}
  93. <div class="mt-3 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
  94. <table class="w-full text-sm" data-sortable-table="search-manual-blocks">
  95. <thead class="border-b border-slate-200 bg-slate-50 text-left text-xs uppercase tracking-wider text-slate-500 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-400">
  96. <tr>
  97. {{ sort.th('Kind', 'kind') }}
  98. {{ sort.th('Target', 'target') }}
  99. {{ sort.th('Reason', 'reason') }}
  100. {{ sort.th('Expires', 'expires', 'date') }}
  101. {{ sort.th('Created', 'created', 'date') }}
  102. </tr>
  103. </thead>
  104. <tbody class="divide-y divide-slate-100 dark:divide-slate-800">
  105. {% for item in manual_blocks %}
  106. <tr>
  107. <td class="px-4 py-2 font-mono text-xs uppercase" data-sort-value="{{ item.kind }}">{{ item.kind }}</td>
  108. <td class="px-4 py-2 font-mono" data-sort-value="{{ item.kind == 'ip' ? item.ip : item.cidr }}">{{ item.kind == 'ip' ? item.ip : item.cidr }}</td>
  109. <td class="px-4 py-2 text-slate-600 dark:text-slate-300" data-sort-value="{{ item.reason|default('') }}">{{ item.reason|default('—') }}</td>
  110. <td class="px-4 py-2 text-slate-500 dark:text-slate-400" data-sort-value="{{ item.expires_at|default('') }}">{% if item.expires_at %}<time class="irdb-dt" datetime="{{ item.expires_at }}">{{ item.expires_at }}</time>{% else %}—{% endif %}</td>
  111. <td class="px-4 py-2 text-slate-500 dark:text-slate-400" data-sort-value="{{ item.created_at|default('') }}">{% if item.created_at %}<time class="irdb-dt" datetime="{{ item.created_at }}">{{ item.created_at }}</time>{% endif %}</td>
  112. </tr>
  113. {% endfor %}
  114. </tbody>
  115. </table>
  116. </div>
  117. {% else %}
  118. <p class="mt-3 text-sm text-slate-500 dark:text-slate-400">No manual blocks match this query.</p>
  119. {% endif %}
  120. </section>
  121. {# ------------------------------ Allowlist --------------------------- #}
  122. <section class="mt-8">
  123. <div class="flex items-center justify-between">
  124. <h2 class="text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
  125. Allowlist <span class="ml-2 rounded bg-slate-100 px-1.5 py-0.5 font-mono text-xs text-slate-700 dark:bg-slate-800 dark:text-slate-300">{{ allowlist|length }}</span>
  126. </h2>
  127. <a href="/app/allowlist" class="text-sm text-indigo-600 hover:underline dark:text-indigo-400">
  128. Open Allowlist &rsaquo;
  129. </a>
  130. </div>
  131. {% if errors.allowlist is defined %}
  132. <div class="mt-2 rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-300">
  133. Failed to load allowlist: {{ errors.allowlist }}
  134. </div>
  135. {% elseif allowlist|length > 0 %}
  136. <div class="mt-3 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
  137. <table class="w-full text-sm" data-sortable-table="search-allowlist">
  138. <thead class="border-b border-slate-200 bg-slate-50 text-left text-xs uppercase tracking-wider text-slate-500 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-400">
  139. <tr>
  140. {{ sort.th('Kind', 'kind') }}
  141. {{ sort.th('Target', 'target') }}
  142. {{ sort.th('Reason', 'reason') }}
  143. {{ sort.th('Created', 'created', 'date') }}
  144. </tr>
  145. </thead>
  146. <tbody class="divide-y divide-slate-100 dark:divide-slate-800">
  147. {% for item in allowlist %}
  148. <tr>
  149. <td class="px-4 py-2 font-mono text-xs uppercase" data-sort-value="{{ item.kind }}">{{ item.kind }}</td>
  150. <td class="px-4 py-2 font-mono" data-sort-value="{{ item.kind == 'ip' ? item.ip : item.cidr }}">{{ item.kind == 'ip' ? item.ip : item.cidr }}</td>
  151. <td class="px-4 py-2 text-slate-600 dark:text-slate-300" data-sort-value="{{ item.reason|default('') }}">{{ item.reason|default('—') }}</td>
  152. <td class="px-4 py-2 text-slate-500 dark:text-slate-400" data-sort-value="{{ item.created_at|default('') }}">{% if item.created_at %}<time class="irdb-dt" datetime="{{ item.created_at }}">{{ item.created_at }}</time>{% endif %}</td>
  153. </tr>
  154. {% endfor %}
  155. </tbody>
  156. </table>
  157. </div>
  158. {% else %}
  159. <p class="mt-3 text-sm text-slate-500 dark:text-slate-400">No allowlist entries match this query.</p>
  160. {% endif %}
  161. </section>
  162. {% endif %}
  163. </div>
  164. {% endblock %}