index.twig 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  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. {% import 'partials/sort.twig' as sort %}
  24. <div class="mx-auto max-w-6xl">
  25. <div class="flex items-center justify-between">
  26. <h1 class="text-2xl font-semibold tracking-tight">IPs</h1>
  27. {% if list %}
  28. <span class="text-sm text-slate-500 dark:text-slate-400">{{ list.total }} total</span>
  29. {% endif %}
  30. </div>
  31. {% if error %}
  32. <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>
  33. {% endif %}
  34. <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">
  35. <div class="col-span-2">
  36. <label for="f-q" class="block text-xs font-medium text-slate-600 dark:text-slate-400">IP / prefix</label>
  37. <input type="search" id="f-q" name="q" value="{{ filters.q|default('') }}" placeholder="203.0.113."
  38. 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">
  39. </div>
  40. <div>
  41. <label for="f-cat" class="block text-xs font-medium text-slate-600 dark:text-slate-400">Category</label>
  42. <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">
  43. <option value="">— any —</option>
  44. {% for c in categories %}
  45. <option value="{{ c }}" {% if filters.category == c %}selected{% endif %}>{{ c }}</option>
  46. {% endfor %}
  47. </select>
  48. </div>
  49. <div>
  50. <label for="f-min" class="block text-xs font-medium text-slate-600 dark:text-slate-400">Min score</label>
  51. <input type="number" id="f-min" name="min_score" step="0.01" min="0" value="{{ filters.min_score|default('') }}"
  52. 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">
  53. </div>
  54. <div>
  55. <label for="f-max" class="block text-xs font-medium text-slate-600 dark:text-slate-400">Max score</label>
  56. <input type="number" id="f-max" name="max_score" step="0.01" min="0" value="{{ filters.max_score|default('') }}"
  57. 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">
  58. </div>
  59. <div>
  60. <label for="f-country" class="block text-xs font-medium text-slate-600 dark:text-slate-400">Country</label>
  61. {% if countries|default([])|length > 0 %}
  62. <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">
  63. <option value="">— any —</option>
  64. {% for c in countries %}
  65. <option value="{{ c.code }}" {% if filters.country|upper == c.code|upper %}selected{% endif %}>{{ c.code }} ({{ c.count }})</option>
  66. {% endfor %}
  67. </select>
  68. {% else %}
  69. <input type="text" id="f-country" name="country" maxlength="2" value="{{ filters.country|default('') }}"
  70. 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">
  71. {% endif %}
  72. </div>
  73. <div>
  74. <label for="f-asn" class="block text-xs font-medium text-slate-600 dark:text-slate-400">ASN</label>
  75. <input type="number" id="f-asn" name="asn" min="1" value="{{ filters.asn|default('') }}"
  76. 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">
  77. </div>
  78. <div class="col-span-2 md:col-span-7 flex flex-wrap items-end justify-between gap-3">
  79. <div class="flex items-center gap-2">
  80. <label for="f-status" class="text-xs font-medium text-slate-600 dark:text-slate-400">Status</label>
  81. <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">
  82. <option value="">any</option>
  83. {% for s in statuses %}
  84. <option value="{{ s }}" {% if filters.status == s %}selected{% endif %}>{{ s }}</option>
  85. {% endfor %}
  86. </select>
  87. </div>
  88. <div class="flex gap-2">
  89. <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>
  90. <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>
  91. </div>
  92. </div>
  93. </form>
  94. {% if list %}
  95. <div class="mt-6 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
  96. <table class="w-full text-sm" data-sortable-table="ips-index">
  97. <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">
  98. <tr>
  99. {{ sort.th('IP', 'ip') }}
  100. {{ sort.th('Country', 'country') }}
  101. {{ sort.th('ASN', 'asn', 'number') }}
  102. {{ sort.th('Top category', 'top_category') }}
  103. {{ sort.th('Max score', 'max_score', 'number', 'right') }}
  104. {{ sort.th('Last report', 'last_report', 'date') }}
  105. {{ sort.th('Status', 'status') }}
  106. </tr>
  107. </thead>
  108. <tbody class="divide-y divide-slate-100 dark:divide-slate-800">
  109. {% for item in list.items %}
  110. <tr>
  111. <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>
  112. <td class="px-4 py-2" data-sort-value="{{ item.enrichment.country_code|default('') }}">{{ h.flag(item.enrichment.country_code|default('')) }}</td>
  113. <td class="px-4 py-2 font-mono text-slate-500" data-sort-value="{{ item.enrichment.asn|default('') }}">{{ item.enrichment.asn|default('—') }}</td>
  114. <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>
  115. <td class="px-4 py-2 text-right font-mono" data-sort-value="{{ item.maxScore }}">{{ item.maxScore|number_format(2) }}</td>
  116. <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>
  117. <td class="px-4 py-2" data-sort-value="{{ item.status }}">{{ h.status_pill(item.status) }}</td>
  118. </tr>
  119. {% else %}
  120. <tr><td colspan="7" class="px-4 py-6 text-center text-slate-400">No results.</td></tr>
  121. {% endfor %}
  122. </tbody>
  123. </table>
  124. </div>
  125. {% if list.total > list.pageSize %}
  126. {% set total_pages = list.totalPages() %}
  127. <nav class="mt-4 flex items-center justify-between text-sm">
  128. <span class="text-slate-500 dark:text-slate-400">Page {{ page }} of {{ total_pages }}</span>
  129. <div class="flex gap-2">
  130. {% set prev_qs = filters|merge({'page': page - 1}) %}
  131. {% set next_qs = filters|merge({'page': page + 1}) %}
  132. {% if page > 1 %}
  133. <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>
  134. {% endif %}
  135. {% if page < total_pages %}
  136. <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>
  137. {% endif %}
  138. </div>
  139. </nav>
  140. {% endif %}
  141. {% endif %}
  142. </div>
  143. {% endblock %}