docs: public void interactManually(int timeoutMinutes) throws Exception {
          LOG.info("Start UI to interact manually");
          String info = wsc.getHttpHost().toString();
          long end = System.currentTimeMillis() + timeoutMinutes * 60000L;
  
          AtomicBoolean visible = new AtomicBoolean(true);
          AtomicBoolean pass = new AtomicBoolean(false);
          String tName = Thread.currentThread().getName() + "m";
          SwingUtilities.invokeLater(() -> {
              JDialog jd = new JDialog((Frame) null, "Manual Web Service Interaction");
              jd.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
  
              JPanel jpContent = new JPanel(new BorderLayout());
              jd.setContentPane(jpContent);
              jpContent.setPreferredSize(new Dimension(1088, 828));
              jpContent.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
  
              JComboBox<HTTP_METHOD> jcbMethods = new JComboBox<>(HTTP_METHOD.values());
              JTextField jtfEndpoint = new JTextField();
              JTextField jtfParameters = new JTextField();
  
              JTextArea jtaRequest = new JTextArea();
              Font font = jtaRequest.getFont();
              jtaRequest.setFont(new Font("Courier New", font.getStyle(), font.getSize()));
  
              JTextArea jtaResponse = new JTextArea();
              font = jtaResponse.getFont();
              jtaResponse.setFont(new Font("Courier New", font.getStyle(), font.getSize()));
  
              DefaultListModel<JSONObject> historyModel = this.loadHistoryModel();
              JList<JSONObject> jlHistory = new JList<>(historyModel);
              jlHistory.setCellRenderer(new DefaultListCellRenderer() {
                  @Override
                  public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,
                      boolean cellHasFocus) {
                      setBackground(list.getBackground());
                      JSONObject json = (JSONObject) value;
                      setText(json.getString("endpoint"));
                      setToolTipText(json.getString("endpoint"));
                      if (isSelected) {
                          setFont(getFont().deriveFont(Font.BOLD));
                      } else {
                          setFont(getFont().deriveFont(Font.PLAIN));
                      }
                      return this;
                  }
              });
              jlHistory.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
              jlHistory.clearSelection();
              jlHistory.getSelectionModel().addListSelectionListener(event -> {
                  ListSelectionModel lsm = (ListSelectionModel) event.getSource();
                  if (event.getValueIsAdjusting()) {
                      return;
                  }
                  JSONObject json = historyModel.get(lsm.getMinSelectionIndex());
                  jcbMethods.setSelectedItem(HTTP_METHOD.valueOf(json.getString("method")));
                  jtfEndpoint.setText(json.getString("endpoint"));
                  jtfParameters.setText(json.getString("parameters"));
                  jtaRequest.setText(json.getString("req-body"));
                  jtaResponse.setText(json.getString("res-body"));
              });
  
              JPanel jpInfo = new JPanel();
              jpContent.add(jpInfo, BorderLayout.PAGE_START);
              jpInfo.setLayout(new BorderLayout());
              {
                  JButton jb = new JButton("PASS");
                  jb.setForeground(Color.green.darker());
                  jb.setFont(jb.getFont().deriveFont(Font.BOLD));
                  jpInfo.add(jb, BorderLayout.LINE_START);
                  jb.addActionListener(event -> {
                      pass.set(true);
                      jd.dispose();
                      visible.set(false);
                  });
              }
              {
                  JButton jb = new JButton("FAIL");
                  jb.setForeground(Color.red);
                  jb.setFont(jb.getFont().deriveFont(Font.BOLD));
                  jpInfo.add(jb, BorderLayout.LINE_END);
                  jb.addActionListener(event -> {
                      pass.set(false);
                      jd.dispose();
                      visible.set(false);
                  });
              }
              JLabel jlTimeout = new JLabel("1200 seconds left", SwingConstants.CENTER);
              jpInfo.add(jlTimeout, BorderLayout.CENTER);
              new SwingWorker<Long, Long>() {
                  @Override
                  protected Long doInBackground() throws Exception {
                      while (System.currentTimeMillis() < end) {
                          Thread.sleep(1000);
                          long left = (end - System.currentTimeMillis()) / 1000;
                          this.publish(left);
                      }
                      return 0L;
                  }
  
                  @Override
                  protected void process(List<Long> chunks) {
                      Long l = chunks.get(chunks.size() - 1);
                      jlTimeout.setText(l + " seconds left");
                      if (l < 850) {
                          jlTimeout.setForeground(Color.red);
                      }
                  }
              }.execute();
  
              JPanel jpRequest = new JPanel(new BorderLayout());
              {
                  JPanel jpHttp = new JPanel();
                  jpHttp.setLayout(new BoxLayout(jpHttp, BoxLayout.PAGE_AXIS));
                  jpRequest.add(jpHttp, BorderLayout.PAGE_START);
  
                  JPanel jpHttp1 = new JPanel();
                  jpHttp1.setLayout(new BoxLayout(jpHttp1, BoxLayout.LINE_AXIS));
                  jpHttp.add(jpHttp1);
  
                  jpHttp1.add(jcbMethods);
                  jpHttp1.add(Box.createHorizontalStrut(18));
                  jpHttp1.add(new JLabel(info, SwingConstants.CENTER), BorderLayout.CENTER);
                  jpHttp1.add(Box.createHorizontalStrut(8));
                  jpHttp1.add(new JLabel("endpoint: "));
                  jpHttp1.add(Box.createHorizontalStrut(5));
                  jpHttp1.add(jtfEndpoint);
                  jpHttp1.add(Box.createHorizontalStrut(8));
                  JButton jbHeaders = new JButton("Headers");
                  jpHttp1.add(jbHeaders);
                  jbHeaders.addActionListener((ActionEvent event) -> {
                      Thread t = new Thread(tName) {
                          @Override
                          public void run() {
                              JPanel jpHeaders = new JPanel(new BorderLayout());
                              jpHeaders.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
  
                              DefaultTableModel tm = new DefaultTableModel(1, 2);
                              tm.setColumnIdentifiers(new String[]{"Name", "Value"});
                              wsc.getHeaders().entrySet().forEach(es -> {
                                  tm.insertRow(0, new String[]{es.getKey(), es.getValue()});
                              });
                              tm.addRow(new String[]{"", ""});
  
                              JTable jt = new JTable(tm);
                              jpHeaders.add(new JScrollPane(jt), BorderLayout.CENTER);
  
                              JPanel jp = new JPanel();
                              jpHeaders.add(jp, BorderLayout.PAGE_END);
                              jp.setLayout(new BoxLayout(jp, BoxLayout.LINE_AXIS));
                              JButton apply = new JButton("Apply");
                              jp.add(Box.createHorizontalGlue());
                              jp.add(apply);
                              apply.setEnabled(false);
                              apply.setToolTipText("disabled for now, coming soon...");
  
                              JDialog jd0 = new JDialog(jd, "HTTP Headers");
                              jd0.setContentPane(jpHeaders);
                              jd0.setModal(true);
                              jd0.pack();
                              jd0.setLocationRelativeTo(jd);
                              jd0.setVisible(true);
                          }
                      };
                      t.start();
                  });
  
                  JPanel jpHttp2 = new JPanel();
                  jpHttp2.setLayout(new BoxLayout(jpHttp2, BoxLayout.LINE_AXIS));
                  jpHttp.add(jpHttp2);
  
                  jpHttp2.add(new JLabel("parameters: "));
                  jpHttp2.add(Box.createHorizontalStrut(8));
                  jpHttp2.add(jtfParameters);
                  jpHttp.add(Box.createHorizontalStrut(18));
                  JButton jbSend = new JButton("Send Request");
                  jpHttp2.add(jbSend);
  
                  String error = "Cannot send request";
                  jbSend.addActionListener((ActionEvent event) -> {
                      Thread t = new Thread(tName) {
                          @Override
                          public void run() {
                              LOG.debug("\n\n");
                              String ep = jtfEndpoint.getText();
                              if (StringUtils.isBlank(ep)) {
                                  return;
                              }
  
                              String pm = jtfParameters.getText();
                              String ct = jtaRequest.getText();
                              String requestId = UUID.randomUUID().toString();
                              HTTP_METHOD method = (HTTP_METHOD) jcbMethods.getSelectedItem();
                              String res;
                              JSONObject json;
  
                              jtaResponse.setText("");
                              try {
                                  switch (method) {
                                      case GET:
                                          res = wsc.get(ep, pm, requestId);
                                          jtaResponse.setText(res);
                                          break;
                                      case GET_JSONObject:
                                          json = wsc.getJsonObject(ep, pm, requestId);
                                          jtaResponse.setText(json.toString(2));
                                          break;
                                      case GET_JSONArray:
                                          JSONArray jarr = wsc.getJsonArray(ep, pm, requestId);
                                          jtaResponse.setText(jarr.toString(2));
                                          break;
                                      case POST:
                                          res = wsc.post(ep, pm, ct, requestId);
                                          jtaResponse.setText(res);
                                          break;
                                      case POST_JSONObject:
                                          res = wsc.postJson(ep, pm, new JSONObject(ct), requestId);
                                          jtaResponse.setText(res);
                                          break;
                                      case PUT:
                                          res = wsc.put(ep, pm, ct, requestId);
                                          jtaResponse.setText(res);
                                          break;
                                      case PUT_JSONObject:
                                          res = wsc.putJson(ep, pm, new JSONObject(ct), requestId);
                                          jtaResponse.setText(res);
                                          break;
                                      case DELETE:
                                          if (StringUtils.isBlank(ct)) {
                                              res = wsc.delete(ep, pm, requestId);
                                          } else {
                                              res = wsc.delete(ep, pm, ct, requestId);
                                          }
                                          jtaResponse.setText(res);
                                          break;
                                      case DELETE_JSONObject:
                                          res = wsc.deleteJson(ep, pm, new JSONObject(ct), requestId);
                                          jtaResponse.setText(res);
                                          break;
                                      case HEAD:
                                          for (Header h : wsc.head(ep, pm, requestId)) {
                                              jtaResponse.append(h.getName() + ": " + h.getValue() + "\n");
                                          }
                                          break;
                                  }
                              } catch (IOException | JSONException ex) {
                                  LOG.error(error, ex);
                                  StringWriter sw = new StringWriter();
                                  ex.printStackTrace(new PrintWriter(sw));
                                  jtaResponse.append(sw.toString());
                                  jtaResponse.append("\n");
                              }
                              long time = wsc.getResponseTime(requestId);
                              jtaResponse.append("\n\nresponse time (ms) " + time);
                              LOG.debug("\n\n");
  
                              JSONObject j = new JSONObject()
                                  .put("method", method.name())
                                  .put("endpoint", ep)
                                  .put("parameters", pm)
                                  .put("req-body", ct)
                                  .put("res-body", jtaResponse.getText());
                              historyModel.insertElementAt(j, 0);
                              try {
                                  FileUtils.writeStringToFile(new File(historyDir, "ws-" + System.currentTimeMillis()
                                      + ".json"), j.toString(2), Charset.defaultCharset());
                              } catch (IOException ex) {
                                  LOG.warn("Cannot save history {}", ex.getMessage());
                              }
                          }
                      };
                      t.start();
  
                      try {
                          t.join();
                      } catch (InterruptedException ex) {
                          LOG.error(error, ex);
                      }
                  });
  
                  jtaRequest.setTabSize(4);
                  JScrollPane jsp = new JScrollPane(jtaRequest);
                  jsp.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(5, 10, 5, 5),
                      UIManager.getBorder("TextField.border")));
                  jpRequest.add(new JLabel("<html>request<br>body: "), BorderLayout.LINE_START);
                  jpRequest.add(jsp, BorderLayout.CENTER);
              }
  
              JPanel jpResponse = new JPanel(new BorderLayout());
              {
                  JScrollPane jsp = new JScrollPane(jtaResponse);
                  jsp.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(5, 10, 5, 5),
                      UIManager.getBorder("TextField.border")));
                  jpResponse.add(jsp, BorderLayout.CENTER);
                  jpResponse.add(new JLabel("response: "), BorderLayout.LINE_START);
              }
  
              JPanel jpLog = new JPanel();
              jpLog.setLayout(new BoxLayout(jpLog, BoxLayout.LINE_AXIS));
              jpResponse.add(jpLog, BorderLayout.PAGE_END);
              {
                  JTextField jtMsg = new JTextField(10);
                  jpLog.add(jtMsg);
                  JButton jbLogMsg = new JButton("Log Message");
                  jpLog.add(jbLogMsg);
                  jtMsg.addFocusListener(new FocusListener() {
                      @Override
                      public void focusLost(final FocusEvent pE) {
                      }
  
                      @Override
                      public void focusGained(final FocusEvent pE) {
                          jtMsg.selectAll();
                      }
                  });
                  jbLogMsg.addActionListener(event -> {
                      Thread t = new Thread(tName) {
                          @Override
                          public void run() {
                              String msg = jtMsg.getText();
                              if (StringUtils.isNotBlank(msg)) {
                                  LOG.info("{}", msg);
                              }
                          }
                      };
                      t.start();
                      try {
                          t.join();
                      } catch (InterruptedException ex) {
                          LOG.error("", ex);
                      }
                      jtMsg.requestFocus();
                  });
              }
              JSplitPane jSplitPaneReqRes = new JSplitPane(JSplitPane.VERTICAL_SPLIT, jpRequest, jpResponse);
              jSplitPaneReqRes.setResizeWeight(0.28);
              jSplitPaneReqRes.setBorder(BorderFactory.createEtchedBorder());
  
              JPanel jpHistory = new JPanel(new BorderLayout());
              {
                  jpHistory.setBorder(BorderFactory.createEtchedBorder());
                  jpHistory.add(new JScrollPane(jlHistory), BorderLayout.CENTER);
              }
  
              JSplitPane jSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, jpHistory, jSplitPaneReqRes);
              jSplitPane.setResizeWeight(0.18);
              jSplitPane.setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 0));
  
              jpContent.add(jSplitPane, BorderLayout.CENTER);
              jd.pack();
              jd.setLocationRelativeTo(null);
              jd.setVisible(true);
              jSplitPane.setDividerLocation(0.12);
          });
  
          while (visible.get()) {
              if (System.currentTimeMillis() > end) {
                  LOG.error("Manual interaction timeout");
                  break;
              }
              Thread.sleep(500);
          }
  
          if (pass.get()) {
              LOG.info("Manual Interaction returns PASS");
          } else {
              Assert.fail("Manual Interaction returns FAIL");
          }
      }