index.twig 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. {% extends 'layout.twig' %}
  2. {% block title %}IPs — IRDB{% endblock %}
  3. {% macro flag(country) %}
  4. {%- set emoji = flag_emoji(country) -%}
  5. {%- if emoji -%}
  6. <span class="text-base leading-none">{{- emoji -}}</span>
  7. {%- else -%}
  8. <span class="rounded bg-slate-100 px-1.5 py-0.5 font-mono text-[0.65rem] text-slate-500 dark:bg-slate-800 dark:text-slate-400">??</span>
  9. {%- endif -%}
  10. {% endmacro %}
  11. {% macro status_pill(status) %}
  12. {%- set classes = {
  13. 'allowlisted': 'bg-emerald-100 text-emerald-900 dark:bg-emerald-900 dark:text-emerald-100',
  14. 'manually_blocked': 'bg-amber-100 text-amber-900 dark:bg-amber-900 dark:text-amber-100',
  15. 'scored': 'bg-red-100 text-red-900 dark:bg-red-900 dark:text-red-100',
  16. 'clean': 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300',
  17. 'manual': 'bg-amber-100 text-amber-900 dark:bg-amber-900 dark:text-amber-100',
  18. } -%}
  19. <span class="rounded px-2 py-0.5 font-mono text-[0.65rem] uppercase {{ classes[status]|default('bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300') }}">{{ status }}</span>
  20. {% endmacro %}
  21. {% block content %}
  22. {% import _self as h %}
  23. <div class="mx-auto max-w-6xl">
  24. <div class="flex items-center justify-between">
  25. <h1 class="text-2xl font-semibold tracking-tight">IPs</h1>
  26. {% if list %}
  27. <span class="text-sm text-slate-500 dark:text-slate-400">{{ list.total }} total</span>
  28. {% endif %}
  29. </div>
  30. {% if error %}
  31. <div class="mt-4 rounded-md border border-red-300 bg-red-50 px-4 py-2 text-sm text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-300">{{ error }}</div>
  32. {% endif %}
  33. <form method="get" action="/app/ips" class="mt-4 grid grid-cols-2 gap-3 rounded-2xl border border-slate-200 bg-white p-4 text-sm shadow-sm dark:border-slate-800 dark:bg-slate-900 md:grid-cols-7">
  34. <div class="col-span-2">
  35. <label for="f-q" class="block text-xs font-medium text-slate-600 dark:text-slate-400">IP / prefix</label>
  36. <input type="search" id="f-q" name="q" value="{{ filters.q|default('') }}" placeholder="203.0.113."
  37. class="mt-1 w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 text-sm dark:border-slate-700 dark:bg-slate-950">
  38. </div>
  39. <div>
  40. <label for="f-cat" class="block text-xs font-medium text-slate-600 dark:text-slate-400">Category</label>
  41. <select id="f-cat" name="category" class="mt-1 w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 text-sm dark:border-slate-700 dark:bg-slate-950">
  42. <option value="">— any —</option>
  43. {% for c in categories %}
  44. <option value="{{ c }}" {% if filters.category == c %}selected{% endif %}>{{ c }}</option>
  45. {% endfor %}
  46. </select>
  47. </div>
  48. <div>
  49. <label for="f-min" class="block text-xs font-medium text-slate-600 dark:text-slate-400">Min score</label>
  50. <input type="number" id="f-min" name="min_score" step="0.01" min="0" value="{{ filters.min_score|default('') }}"
  51. class="mt-1 w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 text-sm dark:border-slate-700 dark:bg-slate-950">
  52. </div>
  53. <div>
  54. <label for="f-max" class="block text-xs font-medium text-slate-600 dark:text-slate-400">Max score</label>
  55. <input type="number" id="f-max" name="max_score" step="0.01" min="0" value="{{ filters.max_score|default('') }}"
  56. class="mt-1 w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 text-sm dark:border-slate-700 dark:bg-slate-950">
  57. </div>
  58. <div>
  59. <label for="f-country" class="block text-xs font-medium text-slate-600 dark:text-slate-400">Country</label>
  60. {% if countries|default([])|length > 0 %}
  61. <select id="f-country" name="country" class="mt-1 w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 font-mono text-sm uppercase dark:border-slate-700 dark:bg-slate-950">
  62. <option value="">— any —</option>
  63. {% for c in countries %}
  64. <option value="{{ c.code }}" {% if filters.country|upper == c.code|upper %}selected{% endif %}>{{ c.code }} ({{ c.count }})</option>
  65. {% endfor %}
  66. </select>
  67. {% else %}
  68. <input type="text" id="f-country" name="country" maxlength="2" value="{{ filters.country|default('') }}"
  69. class="mt-1 w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 font-mono text-sm uppercase dark:border-slate-700 dark:bg-slate-950">
  70. {% endif %}
  71. </div>
  72. <div>
  73. <label for="f-asn" class="block text-xs font-medium text-slate-600 dark:text-slate-400">ASN</label>
  74. <input type="number" id="f-asn" name="asn" min="1" value="{{ filters.asn|default('') }}"
  75. class="mt-1 w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 text-sm dark:border-slate-700 dark:bg-slate-950">
  76. </div>
  77. <div class="col-span-2 md:col-span-7 flex flex-wrap items-end justify-between gap-3">
  78. <div class="flex items-center gap-2">
  79. <label for="f-status" class="text-xs font-medium text-slate-600 dark:text-slate-400">Status</label>
  80. <select id="f-status" name="status" class="rounded-md border border-slate-300 bg-white px-2 py-1.5 text-sm dark:border-slate-700 dark:bg-slate-950">
  81. <option value="">any</option>
  82. {% for s in statuses %}
  83. <option value="{{ s }}" {% if filters.status == s %}selected{% endif %}>{{ s }}</option>
  84. {% endfor %}
  85. </select>
  86. </div>
  87. <div class="flex gap-2">
  88. <a href="/app/ips" class="rounded-md border border-slate-300 px-3 py-1.5 text-sm hover:bg-slate-50 dark:border-slate-700 dark:hover:bg-slate-800">Reset</a>
  89. <button type="submit" class="rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-500">Filter</button>
  90. </div>
  91. </div>
  92. </form>
  93. {% if list %}
  94. <div class="mt-6 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
  95. <table class="w-full text-sm">
  96. <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">
  97. <tr>
  98. <th class="px-4 py-2 font-medium">IP</th>
  99. <th class="px-4 py-2 font-medium">Country</th>
  100. <th class="px-4 py-2 font-medium">ASN</th>
  101. <th class="px-4 py-2 font-medium">Top category</th>
  102. <th class="px-4 py-2 text-right font-medium">Max score</th>
  103. <th class="px-4 py-2 font-medium">Last report</th>
  104. <th class="px-4 py-2 font-medium">Status</th>
  105. </tr>
  106. </thead>
  107. <tbody class="divide-y divide-slate-100 dark:divide-slate-800">
  108. {% for item in list.items %}
  109. <tr>
  110. <td class="px-4 py-2"><a href="/app/ips/{{ item.ip|url_encode }}" class="font-mono text-indigo-600 hover:underline dark:text-indigo-400">{{ item.ip }}</a></td>
  111. <td class="px-4 py-2">{{ h.flag(item.enrichment.country_code|default('')) }}</td>
  112. <td class="px-4 py-2 font-mono text-slate-500">{{ item.enrichment.asn|default('—') }}</td>
  113. <td class="px-4 py-2 font-mono text-slate-600 dark:text-slate-300">{{ item.topCategory|default('—') }}</td>
  114. <td class="px-4 py-2 text-right font-mono">{{ item.maxScore|number_format(2) }}</td>
  115. <td class="px-4 py-2 text-slate-500 dark:text-slate-400">{% if item.lastReportAt %}<time class="irdb-dt" datetime="{{ item.lastReportAt }}">{{ item.lastReportAt }}</time>{% else %}—{% endif %}</td>
  116. <td class="px-4 py-2">{{ h.status_pill(item.status) }}</td>
  117. </tr>
  118. {% else %}
  119. <tr><td colspan="7" class="px-4 py-6 text-center text-slate-400">No results.</td></tr>
  120. {% endfor %}
  121. </tbody>
  122. </table>
  123. </div>
  124. {% if list.total > list.pageSize %}
  125. {% set total_pages = list.totalPages() %}
  126. <nav class="mt-4 flex items-center justify-between text-sm">
  127. <span class="text-slate-500 dark:text-slate-400">Page {{ page }} of {{ total_pages }}</span>
  128. <div class="flex gap-2">
  129. {% set prev_qs = filters|merge({'page': page - 1}) %}
  130. {% set next_qs = filters|merge({'page': page + 1}) %}
  131. {% if page > 1 %}
  132. <a href="/app/ips?{{ prev_qs|url_encode }}" class="rounded-md border border-slate-300 px-3 py-1.5 hover:bg-slate-50 dark:border-slate-700 dark:hover:bg-slate-800">‹ Prev</a>
  133. {% endif %}
  134. {% if page < total_pages %}
  135. <a href="/app/ips?{{ next_qs|url_encode }}" class="rounded-md border border-slate-300 px-3 py-1.5 hover:bg-slate-50 dark:border-slate-700 dark:hover:bg-slate-800">Next ›</a>
  136. {% endif %}
  137. </div>
  138. </nav>
  139. {% endif %}
  140. {% endif %}
  141. </div>
  142. {% endblock %}