eping 3.85__tar.gz → 3.91__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eping
3
- Version: 3.85
3
+ Version: 3.91
4
4
  Summary: Continuous ICMP reachability monitor built on fping, with a CSV logfile analyser (epinga)
5
5
  Author-email: Ewald Jeitler <ewald@jeitler.cc>
6
6
  License: GPL-3.0-or-later
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Dynamic: license-file
19
19
 
20
- # eping.py 3.85
20
+ # eping.py 3.91
21
21
 
22
22
  Continuous ICMP reachability monitor built on `fping`. Scans a host list in a loop,
23
23
  reports each host UP/DOWN/NO-DNS, counts state changes. CLI (curses) or web GUI.
@@ -244,7 +244,7 @@ Single self-contained page, no external resources.
244
244
  - Row 1: view select (9 views), sort order select, SET REFERENCE, ZERO CHANGES,
245
245
  CLEAR ALL, address mode select, GET NAMES, ADV OPTIONS, RESET LOG, FILE OPERATIONS
246
246
  select, EXIT, font size.
247
- - Row 2: match filter (SET/CLEAR), ADV FILTER, host field (ADD/DELETE), comment
247
+ - Row 2: match filter (SET/CLEAR), ADV FILTER, host field (ADD/DELETE), ADD/DEL LIST, comment
248
248
  field (COMMENT), GENERATE REPORT select.
249
249
  - View select, sort order select, address mode select and ADV OPTIONS all match the
250
250
  CLI's `U`/`O`/`P`/`X` pickers exactly. FILE OPERATIONS (incl. ADD HOSTS FILE FROM CLIENT/
@@ -254,7 +254,7 @@ Single self-contained page, no external resources.
254
254
  | Entry | Does |
255
255
  |---|---|
256
256
  | ADD HOSTS FILE FROM SERVER | picks one or more `.txt` files or inventory `.csv` files already on the server, adds their hosts; a `.csv` without the inventory header, e.g. an eping log, is refused |
257
- | ADD HOSTS FILE FROM CLIENT | local file picker (`.txt`/`.csv`), adds the hosts; a file can also be dropped anywhere on the page |
257
+ | ADD HOSTS FILE FROM CLIENT | dialog with a file picker (`.txt`/`.csv`; works in Safari), adds the hosts; a file can also be dropped anywhere on the page |
258
258
  | SAVE HOSTS FILE ON SERVER | confirmation, then saves the currently displayed hosts; blank name overwrites the active `-f` hostfile (first one, if eping.py was started with one) or, with none active, the default `eping-hosts.txt`; a typed name always saves as that new file instead, `.txt` appended once, active hostfile left untouched. If the displayed hosts carry inventory values (CSV import) an inventory `.csv` is written instead (`;` delimited, columns as in *Inventory CSV import*); a `.txt` name or hostfile then becomes `.csv`, the default is `eping-hosts.csv`. The options that differ from the default are written first as `opt:` lines (see *Host file format*); for a `.csv` the checkboxes `include options (opt: lines)` (default on) and `save as .txt (hosts only, no inventory)` (default off) are offered |
259
259
  | UPLOAD FILE TO SERVER | picks a local `.txt`/`.csv`, saves as-is server-side, no host parsing; never overwrites, appends `-1`/`-2`/... |
260
260
  | DOWNLOAD ALL HOSTS | full reference list: inventory `.csv` if the hosts carry inventory values, else `.txt`; non-default options first as `opt:` lines (`.csv`: checkboxes `include options`, default on, and `save as .txt`, default off) |
@@ -273,6 +273,13 @@ Single self-contained page, no external resources.
273
273
  `+`/`−` font size (works while typing). No keyboard shortcut for `X`/ADV OPTIONS or
274
274
  GENERATE REPORT - click required.
275
275
  - Host field feeds both ADD/DELETE; ENTER triggers the button used last, ESC clears.
276
+ - **ADD/DEL LIST** (button after DELETE; also Ctrl/Cmd+V on the page while no field has focus): paste a
277
+ host list - one per line or separated by comma/semicolon/blank, `#` comment, CIDR expanded. A live
278
+ preview shows found / new / already in list / ignored words / masks out of range; ADD and DELETE are
279
+ enabled only if they would change something (DELETE over 20 hosts asks to confirm; exact name match
280
+ only). A pasted inventory `.csv` (with the `IP Address`/`Hostname` header) also updates hosts already in
281
+ the list: IP entries (e.g. from a `.txt`) are renamed to the CSV hostname, history kept, and the CSV
282
+ values are attached. Works with `-wvc` too.
276
283
  - Files can also be dropped anywhere on the page (max 16MB, see *Host file format*).
277
284
  - Commands acknowledge immediately; a click aborts the running round.
278
285
  - **Host popup**: hovering a host row (after ~350 ms) shows everything known about it:
@@ -1,4 +1,4 @@
1
- # eping.py 3.85
1
+ # eping.py 3.91
2
2
 
3
3
  Continuous ICMP reachability monitor built on `fping`. Scans a host list in a loop,
4
4
  reports each host UP/DOWN/NO-DNS, counts state changes. CLI (curses) or web GUI.
@@ -225,7 +225,7 @@ Single self-contained page, no external resources.
225
225
  - Row 1: view select (9 views), sort order select, SET REFERENCE, ZERO CHANGES,
226
226
  CLEAR ALL, address mode select, GET NAMES, ADV OPTIONS, RESET LOG, FILE OPERATIONS
227
227
  select, EXIT, font size.
228
- - Row 2: match filter (SET/CLEAR), ADV FILTER, host field (ADD/DELETE), comment
228
+ - Row 2: match filter (SET/CLEAR), ADV FILTER, host field (ADD/DELETE), ADD/DEL LIST, comment
229
229
  field (COMMENT), GENERATE REPORT select.
230
230
  - View select, sort order select, address mode select and ADV OPTIONS all match the
231
231
  CLI's `U`/`O`/`P`/`X` pickers exactly. FILE OPERATIONS (incl. ADD HOSTS FILE FROM CLIENT/
@@ -235,7 +235,7 @@ Single self-contained page, no external resources.
235
235
  | Entry | Does |
236
236
  |---|---|
237
237
  | ADD HOSTS FILE FROM SERVER | picks one or more `.txt` files or inventory `.csv` files already on the server, adds their hosts; a `.csv` without the inventory header, e.g. an eping log, is refused |
238
- | ADD HOSTS FILE FROM CLIENT | local file picker (`.txt`/`.csv`), adds the hosts; a file can also be dropped anywhere on the page |
238
+ | ADD HOSTS FILE FROM CLIENT | dialog with a file picker (`.txt`/`.csv`; works in Safari), adds the hosts; a file can also be dropped anywhere on the page |
239
239
  | SAVE HOSTS FILE ON SERVER | confirmation, then saves the currently displayed hosts; blank name overwrites the active `-f` hostfile (first one, if eping.py was started with one) or, with none active, the default `eping-hosts.txt`; a typed name always saves as that new file instead, `.txt` appended once, active hostfile left untouched. If the displayed hosts carry inventory values (CSV import) an inventory `.csv` is written instead (`;` delimited, columns as in *Inventory CSV import*); a `.txt` name or hostfile then becomes `.csv`, the default is `eping-hosts.csv`. The options that differ from the default are written first as `opt:` lines (see *Host file format*); for a `.csv` the checkboxes `include options (opt: lines)` (default on) and `save as .txt (hosts only, no inventory)` (default off) are offered |
240
240
  | UPLOAD FILE TO SERVER | picks a local `.txt`/`.csv`, saves as-is server-side, no host parsing; never overwrites, appends `-1`/`-2`/... |
241
241
  | DOWNLOAD ALL HOSTS | full reference list: inventory `.csv` if the hosts carry inventory values, else `.txt`; non-default options first as `opt:` lines (`.csv`: checkboxes `include options`, default on, and `save as .txt`, default off) |
@@ -254,6 +254,13 @@ Single self-contained page, no external resources.
254
254
  `+`/`−` font size (works while typing). No keyboard shortcut for `X`/ADV OPTIONS or
255
255
  GENERATE REPORT - click required.
256
256
  - Host field feeds both ADD/DELETE; ENTER triggers the button used last, ESC clears.
257
+ - **ADD/DEL LIST** (button after DELETE; also Ctrl/Cmd+V on the page while no field has focus): paste a
258
+ host list - one per line or separated by comma/semicolon/blank, `#` comment, CIDR expanded. A live
259
+ preview shows found / new / already in list / ignored words / masks out of range; ADD and DELETE are
260
+ enabled only if they would change something (DELETE over 20 hosts asks to confirm; exact name match
261
+ only). A pasted inventory `.csv` (with the `IP Address`/`Hostname` header) also updates hosts already in
262
+ the list: IP entries (e.g. from a `.txt`) are renamed to the CSV hostname, history kept, and the CSV
263
+ values are attached. Works with `-wvc` too.
257
264
  - Files can also be dropped anywhere on the page (max 16MB, see *Host file format*).
258
265
  - Commands acknowledge immediately; a click aborts the running round.
259
266
  - **Host popup**: hovering a host row (after ~350 ms) shows everything known about it:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "eping"
7
- version = "3.85"
7
+ version = "3.91"
8
8
  description = "Continuous ICMP reachability monitor built on fping, with a CSV logfile analyser (epinga)"
9
9
  readme = "README.md"
10
10
  license = { text = "GPL-3.0-or-later" }
@@ -7,7 +7,7 @@
7
7
  # I knew how it worked.
8
8
  # Now, only god knows it!
9
9
  # - - - - - - - - - - - - - - - - - - - - - - - -
10
- VERSION = '3.85'
10
+ VERSION = '3.91'
11
11
  version = VERSION # legacy alias (kept for existing references)
12
12
 
13
13
  # --- scaling limits ---
@@ -710,6 +710,26 @@ def _rename_host_in_place(old, new, original_hosts_list, active_hosts_list,
710
710
  host_csv_ip.setdefault(new, host_csv_ip[old])
711
711
  forget_names([new]) # force a fresh forward lookup for the new identity
712
712
 
713
+ def merge_csv_ips_into_lists(hosts, original_hosts_list, active_hosts_list,
714
+ host_state, up_seen, down_streak):
715
+ """ADD LIST with an inventory CSV: a CSV host whose IP is already in the lists as a
716
+ plain IP entry (e.g. from a .txt file) updates that entry - it is renamed to the
717
+ CSV hostname in place (history kept) instead of being added a second time.
718
+ Returns the number of renamed entries. host_csv_ip holds the IP of each CSV host."""
719
+ cur = set(original_hosts_list) | set(active_hosts_list)
720
+ renamed = 0
721
+ for h in hosts:
722
+ if h in cur:
723
+ continue
724
+ ip = host_csv_ip.get(h)
725
+ if ip and ip != h and ip in cur:
726
+ _rename_host_in_place(ip, h, original_hosts_list, active_hosts_list,
727
+ host_state, up_seen, down_streak)
728
+ cur.discard(ip)
729
+ cur.add(h)
730
+ renamed += 1
731
+ return renamed
732
+
713
733
  def _forward_confirms(name, ip):
714
734
  """True if 'name' has a forward record (A for an IPv4 ip, AAAA for an IPv6 ip)
715
735
  that resolves back to exactly this ip. Guards GET NAMES: a PTR record with no
@@ -1768,7 +1788,7 @@ def parse_hosts_from_text(text, stats=None, use_names=False, name=None):
1768
1788
  recognised by its header; anything else - and every file named *.txt - is parsed
1769
1789
  as a plain host list (_parse_plain_hosts). A NUL byte separates concatenated files."""
1770
1790
  csv_ok = not (name and str(name).lower().endswith('.txt'))
1771
- out, seen, networks, skipped = [], set(), 0, []
1791
+ out, seen, networks, skipped, ignored = [], set(), 0, [], []
1772
1792
  csv_stats = {}
1773
1793
  for seg in (text or '').split('\x00'):
1774
1794
  sub = {}
@@ -1781,6 +1801,7 @@ def parse_hosts_from_text(text, stats=None, use_names=False, name=None):
1781
1801
  csv_stats['csv'] = True
1782
1802
  networks += sub.get('networks', 0)
1783
1803
  skipped.extend(sub.get('skipped', []))
1804
+ ignored.extend(sub.get('ignored', []))
1784
1805
  for h in got:
1785
1806
  if h not in seen:
1786
1807
  seen.add(h)
@@ -1788,6 +1809,7 @@ def parse_hosts_from_text(text, stats=None, use_names=False, name=None):
1788
1809
  if stats is not None:
1789
1810
  stats['networks'] = networks
1790
1811
  stats['skipped'] = skipped
1812
+ stats['ignored'] = ignored
1791
1813
  stats.update(csv_stats)
1792
1814
  return out
1793
1815
 
@@ -1804,6 +1826,7 @@ def _parse_plain_hosts(text, stats=None):
1804
1826
  fqdns = []
1805
1827
  networks = 0
1806
1828
  skipped = []
1829
+ ignored = []
1807
1830
  for line in (text or '').splitlines():
1808
1831
  line = line.split('#')[0] # ignore comments
1809
1832
  for word in line.replace(',', ' ').replace(';', ' ').split():
@@ -1823,6 +1846,8 @@ def _parse_plain_hosts(text, stats=None):
1823
1846
  ips.append(normalize_ip(word.rsplit('/', 1)[0])) # single-host IPv6 CIDR, no expansion
1824
1847
  elif match_re(word, fqdn_re):
1825
1848
  fqdns.append(word)
1849
+ else:
1850
+ ignored.append(word)
1826
1851
  seen = set()
1827
1852
  out = []
1828
1853
  for h in ips + fqdns:
@@ -1832,6 +1857,7 @@ def _parse_plain_hosts(text, stats=None):
1832
1857
  if stats is not None:
1833
1858
  stats['networks'] = networks
1834
1859
  stats['skipped'] = skipped
1860
+ stats['ignored'] = ignored
1835
1861
  return out
1836
1862
 
1837
1863
  def extract_opt_lines(text):
@@ -2848,6 +2874,29 @@ WEB_INDEX_HTML = r"""<!DOCTYPE html>
2848
2874
  </div>
2849
2875
  </div>
2850
2876
  </div>
2877
+ <div id="hostListModal" class="modal-overlay" style="display:none">
2878
+ <div class="modal-box wide">
2879
+ <h3>ADD / DELETE HOST LIST</h3>
2880
+ <p>Paste hosts: IPv4/IPv6, hostname, IPv4 CIDR - separated by new line, comma, semicolon or blank; `#` starts a comment.</p>
2881
+ <textarea id="hostListText" rows="10" spellcheck="false" style="width:100%;box-sizing:border-box;margin-bottom:8px;background:var(--panel);color:var(--fg);border:1px solid var(--ctrl-line);border-radius:4px;padding:6px 8px;font-family:inherit;font-size:12px;resize:vertical"></textarea>
2882
+ <div id="hostListInfo" style="min-height:34px;margin-bottom:12px;font-size:12px;color:var(--dim)">&nbsp;</div>
2883
+ <div class="modal-buttons">
2884
+ <button id="hostListAddBtn" disabled>ADD</button>
2885
+ <button id="hostListDelBtn" class="danger" disabled>DELETE</button>
2886
+ <button id="hostListCancelBtn">CANCEL</button>
2887
+ </div>
2888
+ </div>
2889
+ </div>
2890
+ <div id="addFromClientModal" class="modal-overlay" style="display:none">
2891
+ <div class="modal-box">
2892
+ <h3>ADD FROM CLIENT</h3>
2893
+ <p>Choose a `.txt` or `.csv` hosts file from this computer to add its hosts.</p>
2894
+ <input type="file" id="fileInputVis" accept=".txt,.csv,.list,text/plain" style="margin-bottom:14px;color:var(--fg)">
2895
+ <div class="modal-buttons">
2896
+ <button id="modalBtnAddFromClientCancel">CANCEL</button>
2897
+ </div>
2898
+ </div>
2899
+ </div>
2851
2900
  <div id="chooseReportModal" class="modal-overlay" style="display:none">
2852
2901
  <div class="modal-box wide xwide">
2853
2902
  <h3>CHOOSE LOGFILE</h3>
@@ -2996,6 +3045,7 @@ WEB_INDEX_HTML = r"""<!DOCTYPE html>
2996
3045
  <input type="text" id="addInput" placeholder="IPv4/IPv6, host, IPv4 CIDR, IPv6 /128, ip1-ip2">
2997
3046
  <button id="btnAdd" title="add the given host(s) to the list - same input as DELETE">ADD</button>
2998
3047
  <button id="btnDel" title="remove the given host(s) - same input as ADD">DELETE</button>
3048
+ <button id="btnList" title="paste a list of hosts (one per line, or separated by comma/semicolon/blank) to add or delete - also opened by Ctrl/Cmd+V on the page">ADD/DEL LIST</button>
2999
3049
  </span>
3000
3050
  <span class="sep">&nbsp;|&nbsp;</span>
3001
3051
  <span class="grp">
@@ -4085,11 +4135,85 @@ document.getElementById('commentInput').addEventListener('keydown', function(e){
4085
4135
  document.getElementById('btnClear').onclick = function(){
4086
4136
  if(confirm('Remove ALL hosts and reset their state?')){ post('clear'); } };
4087
4137
 
4138
+ /* ---- ADD/DEL LIST window (paste a host list), preview before applying ---- */
4139
+ var hostListModal = document.getElementById('hostListModal');
4140
+ var hostListText = document.getElementById('hostListText');
4141
+ var hostListInfo = document.getElementById('hostListInfo');
4142
+ var hostListAddBtn = document.getElementById('hostListAddBtn');
4143
+ var hostListDelBtn = document.getElementById('hostListDelBtn');
4144
+ var hostListTimer = null, hostListSeq = 0, hostListInList = 0, hostListNew = 0;
4145
+ function hostListOpen(){ return hostListModal.style.display === 'flex'; }
4146
+ function closeHostList(){ hostListModal.style.display = 'none'; hostListText.value = ''; }
4147
+ function openHostList(text){
4148
+ hostListText.value = text || '';
4149
+ hostListModal.style.display = 'flex';
4150
+ hostListPreviewNow();
4151
+ hostListText.focus();
4152
+ }
4153
+ function hostListPost(action){
4154
+ return fetch('api/hostlist', {method:'POST', headers:{'Content-Type':'application/json'},
4155
+ body: JSON.stringify({text: hostListText.value, action: action})}).then(function(r){ return r.json(); });
4156
+ }
4157
+ function hostListPreviewNow(){
4158
+ var seq = ++hostListSeq;
4159
+ if(!hostListText.value.trim()){
4160
+ hostListInfo.innerHTML = '&nbsp;'; hostListAddBtn.disabled = true; hostListDelBtn.disabled = true; return;
4161
+ }
4162
+ hostListPost('preview').then(function(j){
4163
+ if(seq !== hostListSeq || !j.ok) return;
4164
+ hostListInList = j.in_list; hostListNew = j.new;
4165
+ var t = j.found + ' host(s) found: ' + j.new + ' new (ADD), ' + j.in_list + ' already in list (DELETE)';
4166
+ if(j.csv) t += ' - CSV: existing hosts are updated with its data' + (j.renamed ? ' (' + j.renamed + ' IP entries renamed to hostname)' : '');
4167
+ if(j.networks) t += ' - ' + j.networks + ' network(s) expanded';
4168
+ if(j.ignored) t += ' - ' + j.ignored + ' ignored: ' + j.ignored_sample.join(', ') + (j.ignored > j.ignored_sample.length ? ' ...' : '');
4169
+ if(j.skipped.length) t += ' - mask out of range: ' + j.skipped.join(', ');
4170
+ hostListInfo.textContent = t;
4171
+ hostListAddBtn.disabled = !(j.new || (j.csv && (j.in_list || j.renamed)));
4172
+ hostListDelBtn.disabled = !j.in_list;
4173
+ }).catch(function(){});
4174
+ }
4175
+ hostListText.addEventListener('input', function(){
4176
+ clearTimeout(hostListTimer); hostListTimer = setTimeout(hostListPreviewNow, 300); });
4177
+ function hostListRun(action){
4178
+ note(action === 'add' ? 'adding host list ...' : 'removing host list ...', true);
4179
+ hostListPost(action).then(function(j){ if(!j.ok){ note('list failed: ' + (j.error||''), false); } })
4180
+ .catch(function(){ note('list failed', false); });
4181
+ closeHostList();
4182
+ }
4183
+ hostListAddBtn.onclick = function(){ hostListRun('add'); };
4184
+ hostListDelBtn.onclick = function(){
4185
+ if(hostListInList > 20 && !confirm('Remove ' + hostListInList + ' hosts from the list?')) return;
4186
+ hostListRun('del'); };
4187
+ document.getElementById('hostListCancelBtn').onclick = closeHostList;
4188
+ document.getElementById('btnList').onclick = function(){ openHostList(''); };
4189
+ /* Ctrl/Cmd+V with no field focused and no window open: open the window with the clipboard text */
4190
+ document.addEventListener('paste', function(e){
4191
+ var a = document.activeElement, tn = a && a.tagName;
4192
+ if(isReadOnly || hostListOpen() || tn === 'INPUT' || tn === 'TEXTAREA' || tn === 'SELECT') return;
4193
+ if(document.querySelector('.modal-overlay[style*="flex"]')) return;
4194
+ var t = e.clipboardData && e.clipboardData.getData('text');
4195
+ if(!t || !t.trim()) return;
4196
+ e.preventDefault();
4197
+ openHostList(t);
4198
+ });
4199
+
4088
4200
  /* ---- host file upload ---- */
4089
4201
  var fileInput = document.getElementById('fileInput');
4202
+ var fileInputVis = document.getElementById('fileInputVis');
4203
+ var addFromClientModal = document.getElementById('addFromClientModal');
4204
+ function closeAddFromClient(){ addFromClientModal.style.display = 'none'; fileInputVis.value = ''; }
4205
+ function addFromClientOpen(){ return addFromClientModal.style.display === 'flex'; }
4206
+ document.getElementById('modalBtnAddFromClientCancel').onclick = closeAddFromClient;
4207
+ fileInputVis.addEventListener('change', function(){
4208
+ var f = fileInputVis.files && fileInputVis.files[0];
4209
+ closeAddFromClient();
4210
+ if(f) uploadHostsFile(f, null);
4211
+ });
4090
4212
  fileInput.addEventListener('change', function(){
4091
4213
  var f = fileInput.files && fileInput.files[0];
4092
- if(!f) return;
4214
+ if(f) uploadHostsFile(f, fileInput);
4215
+ });
4216
+ function uploadHostsFile(f, inp){
4093
4217
  var rd = new FileReader();
4094
4218
  rd.onload = function(){
4095
4219
  note('uploading ' + f.name + ' ...', true);
@@ -4097,10 +4221,10 @@ fileInput.addEventListener('change', function(){
4097
4221
  body: rd.result}).then(function(r){ return r.json(); }).then(function(j){
4098
4222
  if(!j.ok){ note('upload failed: ' + (j.error||''), false); }
4099
4223
  }).catch(function(){ note('upload failed', false); });
4100
- fileInput.value = '';
4224
+ if(inp) inp.value = '';
4101
4225
  };
4102
4226
  rd.readAsText(f);
4103
- });
4227
+ }
4104
4228
 
4105
4229
  var uploadServerFileInput = document.getElementById('uploadServerFileInput');
4106
4230
  uploadServerFileInput.addEventListener('change', function(){
@@ -4367,7 +4491,7 @@ document.getElementById('selDownload').onchange = function(){
4367
4491
  this.value = ''; // reset - a select, not a toggle
4368
4492
  if(!what) return;
4369
4493
  if(what === 'add_from_server'){ openAddFromServer(); return; }
4370
- if(what === 'add_from_client'){ fileInput.click(); return; }
4494
+ if(what === 'add_from_client'){ addFromClientModal.style.display = 'flex'; return; } // visible input: Safari blocks click() from select change
4371
4495
  if(what === 'choose_logfile'){ openChooseLog(); return; }
4372
4496
  if(what === 'delete_files'){ openDeleteFiles(); return; }
4373
4497
  if(what === 'view_file'){ openViewFile(); return; }
@@ -4489,6 +4613,14 @@ document.addEventListener('keydown', function(e){
4489
4613
  if(sk === 'y' || e.key === 'Enter'){ runSaveHostsFile(); e.preventDefault(); }
4490
4614
  return;
4491
4615
  }
4616
+ if(hostListOpen()){
4617
+ if(e.key === 'Escape'){ closeHostList(); e.preventDefault(); }
4618
+ return;
4619
+ }
4620
+ if(addFromClientOpen()){
4621
+ if(e.key === 'Escape'){ closeAddFromClient(); e.preventDefault(); }
4622
+ return;
4623
+ }
4492
4624
  if(chooseLogOpen()){
4493
4625
  if(e.key === 'Escape'){ closeChooseLog(); e.preventDefault(); }
4494
4626
  return;
@@ -5776,6 +5908,45 @@ class EpingWebHandler(http.server.BaseHTTPRequestHandler):
5776
5908
  self._respond(200, 'application/json; charset=utf-8', json.dumps({'ok': True}))
5777
5909
  return
5778
5910
 
5911
+ # --- ADD/DEL LIST window: preview (no change) or queue add_list/del_list ---
5912
+ if path in ('/api/hostlist', 'api/hostlist'):
5913
+ raw = self._read_body(WEB_MAX_UPLOAD)
5914
+ if raw is None:
5915
+ self._respond(413, 'application/json; charset=utf-8',
5916
+ json.dumps({'ok': False, 'error': 'text too large'}))
5917
+ return
5918
+ try:
5919
+ req = json.loads(raw.decode('utf-8', 'replace') or '{}')
5920
+ except Exception:
5921
+ req = {}
5922
+ text = str(req.get('text', ''))
5923
+ action = str(req.get('action', 'preview'))
5924
+ if action in ('add', 'del'):
5925
+ if not self._queue_command((action + '_list', text)):
5926
+ return
5927
+ self._respond(200, 'application/json; charset=utf-8', json.dumps({'ok': True}))
5928
+ return
5929
+ pstats = {}
5930
+ _m0, _c0 = set(host_meta), set(host_csv_ip)
5931
+ found = parse_hosts_from_text(text, pstats, True)
5932
+ with web_lock:
5933
+ present = set(web_state.get('host_list_all') or [])
5934
+ n_ren = (sum(1 for h in found if h not in present and host_csv_ip.get(h) in present
5935
+ and host_csv_ip.get(h) != h) if pstats.get('csv') else 0)
5936
+ for _k in set(host_meta) - _m0: # preview only - keep no CSV data of it
5937
+ host_meta.pop(_k, None)
5938
+ for _k in set(host_csv_ip) - _c0:
5939
+ host_csv_ip.pop(_k, None)
5940
+ n_in = sum(1 for h in found if h in present)
5941
+ self._respond(200, 'application/json; charset=utf-8', json.dumps({
5942
+ 'ok': True, 'found': len(found), 'in_list': n_in,
5943
+ 'csv': bool(pstats.get('csv')), 'renamed': n_ren,
5944
+ 'new': len(found) - n_in - n_ren, 'networks': pstats.get('networks', 0),
5945
+ 'ignored': len(pstats.get('ignored', [])),
5946
+ 'ignored_sample': pstats.get('ignored', [])[:8],
5947
+ 'skipped': pstats.get('skipped', [])[:5]}))
5948
+ return
5949
+
5779
5950
  # --- FILE ACTIONS > UPLOAD: save a *.txt/*.csv file as-is, no host
5780
5951
  # parsing (see /api/upload above for that) ---
5781
5952
  if path in ('/api/upload_server_file', 'api/upload_server_file'):
@@ -6632,6 +6803,47 @@ def run_web_mode(original_hosts_list, host_state, args, logfile_file_name,
6632
6803
  message += ' - please wait, pinging ...'
6633
6804
  write_log_info(args.disable_logging, logfile_file_name,
6634
6805
  'ADD -f (uploaded, ' + str(added) + ' host(s))', tz_offset)
6806
+ elif cmd in ('add_list', 'del_list'):
6807
+ lst_stats = {}
6808
+ lst_hosts = parse_hosts_from_text(value, lst_stats, addr_mode not in (2, 3))
6809
+ if not lst_hosts:
6810
+ message = ('list: mask must be /%d../%d: %s'
6811
+ % (CIDR_MIN_MASK, CIDR_MAX_MASK, lst_stats['skipped'][0])
6812
+ if lst_stats.get('skipped') else 'list: no valid host found')
6813
+ elif cmd == 'add_list':
6814
+ renamed = (merge_csv_ips_into_lists(lst_hosts, original_hosts_list,
6815
+ active_hosts_list, host_state,
6816
+ up_seen, down_streak)
6817
+ if lst_stats.get('csv') else 0)
6818
+ before_active = set(active_hosts_list)
6819
+ added, err = add_hosts_to(lst_hosts, active_hosts_list, original_hosts_list)
6820
+ if err:
6821
+ message = 'list ' + err
6822
+ else:
6823
+ message = ('list: ' + str(added) + ' new host(s) of '
6824
+ + str(len(lst_hosts)) + ' found')
6825
+ if lst_stats.get('csv'):
6826
+ message += (', %d existing host(s) updated (%d renamed to hostname)'
6827
+ % (len(lst_hosts) - added, renamed))
6828
+ if lst_stats.get('ignored'):
6829
+ message += ', %d ignored' % len(lst_stats['ignored'])
6830
+ if added:
6831
+ pending_add_hosts |= (set(lst_hosts) - before_active)
6832
+ message += ' - please wait, pinging ...'
6833
+ write_log_info(args.disable_logging, logfile_file_name,
6834
+ 'ADD list (' + str(added) + ' host(s))', tz_offset)
6835
+ else:
6836
+ removed = remove_hosts_from(lst_hosts, active_hosts_list,
6837
+ original_hosts_list, host_state)
6838
+ up_seen.difference_update(lst_hosts)
6839
+ for _h in lst_hosts:
6840
+ down_streak.pop(_h, None)
6841
+ forget_names(lst_hosts)
6842
+ message = ('list: removed ' + str(removed) + ' of '
6843
+ + str(len(lst_hosts)) + ' host(s) found')
6844
+ if removed:
6845
+ write_log_info(args.disable_logging, logfile_file_name,
6846
+ 'DEL list (' + str(removed) + ' host(s))', tz_offset)
6635
6847
  elif cmd == 'set_ref':
6636
6848
  # what is actually shown becomes the new reference list - the view
6637
6849
  # filter (active_hosts_list) AND the [M]/display filter (display_list
@@ -8331,6 +8543,52 @@ if __name__=='__main__':
8331
8543
  write_log_info(args.disable_logging, logfile_file_name,
8332
8544
  host_spec_cli('DEL', bval), tz_offset)
8333
8545
 
8546
+ elif bcmd in ('add_list', 'del_list'):
8547
+ lst_stats = {}
8548
+ lst_hosts = parse_hosts_from_text(bval, lst_stats, not (prefer_ip or ip_only_mode))
8549
+ if not lst_hosts:
8550
+ message = ('list: mask must be /%d..%d: %s'
8551
+ % (CIDR_MIN_MASK, CIDR_MAX_MASK, lst_stats['skipped'][0])
8552
+ if lst_stats.get('skipped') else 'list: no valid host found')
8553
+ notice(message.upper(), 3)
8554
+ elif bcmd == 'add_list':
8555
+ renamed = (merge_csv_ips_into_lists(lst_hosts, original_hosts_list,
8556
+ active_hosts_list, host_state,
8557
+ up_seen, down_streak)
8558
+ if lst_stats.get('csv') else 0)
8559
+ before_active = set(active_hosts_list)
8560
+ added, err = add_hosts_to(lst_hosts, active_hosts_list, original_hosts_list)
8561
+ if err:
8562
+ notice(('LIST ' + err).upper(), 3)
8563
+ message = 'list ' + err
8564
+ else:
8565
+ message = ('list: ' + str(added) + ' new host(s) of '
8566
+ + str(len(lst_hosts)) + ' found')
8567
+ if lst_stats.get('csv'):
8568
+ message += (', %d existing host(s) updated (%d renamed to hostname)'
8569
+ % (len(lst_hosts) - added, renamed))
8570
+ if lst_stats.get('ignored'):
8571
+ message += ', %d ignored' % len(lst_stats['ignored'])
8572
+ notice(message.upper(), 2)
8573
+ if added:
8574
+ pending_add_hosts |= (set(lst_hosts) - before_active)
8575
+ message += ' - please wait, pinging ...'
8576
+ write_log_info(args.disable_logging, logfile_file_name,
8577
+ 'ADD list (' + str(added) + ' host(s))', tz_offset)
8578
+ else:
8579
+ removed = remove_hosts_from(lst_hosts, active_hosts_list,
8580
+ original_hosts_list, host_state)
8581
+ up_seen.difference_update(lst_hosts)
8582
+ for _h in lst_hosts:
8583
+ down_streak.pop(_h, None)
8584
+ forget_names(lst_hosts)
8585
+ message = ('list: removed ' + str(removed) + ' of '
8586
+ + str(len(lst_hosts)) + ' host(s) found')
8587
+ notice(message.upper(), 2 if removed else 3)
8588
+ if removed:
8589
+ write_log_info(args.disable_logging, logfile_file_name,
8590
+ 'DEL list (' + str(removed) + ' host(s))', tz_offset)
8591
+
8334
8592
  elif bcmd == 'upload':
8335
8593
  up_stats = {}
8336
8594
  new_hosts = parse_hosts_from_text(bval, up_stats, not (prefer_ip or ip_only_mode))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eping
3
- Version: 3.85
3
+ Version: 3.91
4
4
  Summary: Continuous ICMP reachability monitor built on fping, with a CSV logfile analyser (epinga)
5
5
  Author-email: Ewald Jeitler <ewald@jeitler.cc>
6
6
  License: GPL-3.0-or-later
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Dynamic: license-file
19
19
 
20
- # eping.py 3.85
20
+ # eping.py 3.91
21
21
 
22
22
  Continuous ICMP reachability monitor built on `fping`. Scans a host list in a loop,
23
23
  reports each host UP/DOWN/NO-DNS, counts state changes. CLI (curses) or web GUI.
@@ -244,7 +244,7 @@ Single self-contained page, no external resources.
244
244
  - Row 1: view select (9 views), sort order select, SET REFERENCE, ZERO CHANGES,
245
245
  CLEAR ALL, address mode select, GET NAMES, ADV OPTIONS, RESET LOG, FILE OPERATIONS
246
246
  select, EXIT, font size.
247
- - Row 2: match filter (SET/CLEAR), ADV FILTER, host field (ADD/DELETE), comment
247
+ - Row 2: match filter (SET/CLEAR), ADV FILTER, host field (ADD/DELETE), ADD/DEL LIST, comment
248
248
  field (COMMENT), GENERATE REPORT select.
249
249
  - View select, sort order select, address mode select and ADV OPTIONS all match the
250
250
  CLI's `U`/`O`/`P`/`X` pickers exactly. FILE OPERATIONS (incl. ADD HOSTS FILE FROM CLIENT/
@@ -254,7 +254,7 @@ Single self-contained page, no external resources.
254
254
  | Entry | Does |
255
255
  |---|---|
256
256
  | ADD HOSTS FILE FROM SERVER | picks one or more `.txt` files or inventory `.csv` files already on the server, adds their hosts; a `.csv` without the inventory header, e.g. an eping log, is refused |
257
- | ADD HOSTS FILE FROM CLIENT | local file picker (`.txt`/`.csv`), adds the hosts; a file can also be dropped anywhere on the page |
257
+ | ADD HOSTS FILE FROM CLIENT | dialog with a file picker (`.txt`/`.csv`; works in Safari), adds the hosts; a file can also be dropped anywhere on the page |
258
258
  | SAVE HOSTS FILE ON SERVER | confirmation, then saves the currently displayed hosts; blank name overwrites the active `-f` hostfile (first one, if eping.py was started with one) or, with none active, the default `eping-hosts.txt`; a typed name always saves as that new file instead, `.txt` appended once, active hostfile left untouched. If the displayed hosts carry inventory values (CSV import) an inventory `.csv` is written instead (`;` delimited, columns as in *Inventory CSV import*); a `.txt` name or hostfile then becomes `.csv`, the default is `eping-hosts.csv`. The options that differ from the default are written first as `opt:` lines (see *Host file format*); for a `.csv` the checkboxes `include options (opt: lines)` (default on) and `save as .txt (hosts only, no inventory)` (default off) are offered |
259
259
  | UPLOAD FILE TO SERVER | picks a local `.txt`/`.csv`, saves as-is server-side, no host parsing; never overwrites, appends `-1`/`-2`/... |
260
260
  | DOWNLOAD ALL HOSTS | full reference list: inventory `.csv` if the hosts carry inventory values, else `.txt`; non-default options first as `opt:` lines (`.csv`: checkboxes `include options`, default on, and `save as .txt`, default off) |
@@ -273,6 +273,13 @@ Single self-contained page, no external resources.
273
273
  `+`/`−` font size (works while typing). No keyboard shortcut for `X`/ADV OPTIONS or
274
274
  GENERATE REPORT - click required.
275
275
  - Host field feeds both ADD/DELETE; ENTER triggers the button used last, ESC clears.
276
+ - **ADD/DEL LIST** (button after DELETE; also Ctrl/Cmd+V on the page while no field has focus): paste a
277
+ host list - one per line or separated by comma/semicolon/blank, `#` comment, CIDR expanded. A live
278
+ preview shows found / new / already in list / ignored words / masks out of range; ADD and DELETE are
279
+ enabled only if they would change something (DELETE over 20 hosts asks to confirm; exact name match
280
+ only). A pasted inventory `.csv` (with the `IP Address`/`Hostname` header) also updates hosts already in
281
+ the list: IP entries (e.g. from a `.txt`) are renamed to the CSV hostname, history kept, and the CSV
282
+ values are attached. Works with `-wvc` too.
276
283
  - Files can also be dropped anywhere on the page (max 16MB, see *Host file format*).
277
284
  - Commands acknowledge immediately; a click aborts the running round.
278
285
  - **Host popup**: hovering a host row (after ~350 ms) shows everything known about it:
File without changes
File without changes
File without changes
File without changes
File without changes