apps/feeds/templates/admin.html 7.5 K raw
1
<!doctype html>
2
<html lang="en">
3
  <head>
4
    <meta charset="UTF-8" />
5
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
    <meta name="theme-color" content="#121113" />
7
    <link rel="stylesheet" href="/assets/darkmatter.css" />
8
    <link rel="stylesheet" href="/static/styles.css" />
9
    <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
10
    <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
11
    <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
12
    <link rel="manifest" href="/static/site.webmanifest">
13
    <title>Feeds | Admin</title>
14
  </head>
15
  <body>
16
    <div class="header">
17
      <a href="/" class="logo"><h1>FEEDS</h1></a>
18
      <nav class="links"><a href="/feeds?format=opml">opml</a><a href="/admin/logout">logout</a></nav>
19
    </div>
20
21
    {{if .Success}}<p class="success">{{.Success}}</p>{{end}}
22
    {{if .Error}}<p class="error">{{.Error}}</p>{{end}}
23
24
    <section class="admin-form">
25
      <h3>Discover</h3>
26
      <div class="discover-row">
27
        <input type="url" id="base_url" placeholder="https://example.com" />
28
        <button type="button" id="discover-btn" onclick="discoverFeeds()">Discover</button>
29
      </div>
30
      <div id="discover-status" class="discover-status" style="display:none;"></div>
31
      <div id="discover-results" class="discover-results" style="display:none;"></div>
32
    </section>
33
34
    <form class="admin-form" id="add-feed-form" method="POST" action="/admin/add-feed">
35
      <h3>Add Feed</h3>
36
      <label for="feed_url">Feed URL</label>
37
      <input type="url" id="feed_url" name="feed_url" placeholder="https://example.com/feed.xml" required />
38
      <label for="category_name">Category (optional)</label>
39
      <input type="text" id="category_name" name="category_name" placeholder="Tech" list="categories-list" />
40
      <datalist id="categories-list">{{range .Categories}}<option value="{{.Name}}"></option>{{end}}</datalist>
41
      <button type="submit" id="add-feed-submit"><span id="add-feed-label">Add Feed</span></button>
42
    </form>
43
44
    <form class="admin-form" id="opml-form" method="POST" action="/admin/import-opml" enctype="multipart/form-data">
45
      <h3>Import OPML</h3>
46
      <input type="file" name="file" accept=".opml,.xml,application/xml,text/xml" required />
47
      <button type="submit" id="opml-submit"><span id="opml-submit-label">Import</span></button>
48
    </form>
49
50
    <form class="admin-form" method="POST" action="/admin/settings">
51
      <h3>Settings</h3>
52
      <label for="poll_interval_minutes">Poll interval (minutes)</label>
53
      <input type="number" id="poll_interval_minutes" name="poll_interval_minutes" min="1" max="1440" value="{{.PollIntervalMinutes}}" required />
54
      <p class="hint">Item cap per feed: {{.ItemCap}} (set via ITEM_CAP_PER_FEED)</p>
55
      <p class="hint">API key: {{if .APIKeyConfigured}}configured{{else}}not set{{end}}</p>
56
      <button type="submit">Save</button>
57
    </form>
58
59
    <section class="admin-subs">
60
      <h3>Categories ({{len .Categories}})</h3>
61
      <form class="admin-form inline" method="POST" action="/admin/categories">
62
        <input type="text" name="name" placeholder="New category" required />
63
        <button type="submit">Add</button>
64
      </form>
65
      <ul class="category-list">
66
        {{range .Categories}}
67
        <li>
68
          <span>{{.Name}}</span>
69
          <form method="POST" action="/admin/categories/{{.ID}}/delete" class="inline"><button type="submit" class="danger">Delete</button></form>
70
        </li>
71
        {{end}}
72
      </ul>
73
    </section>
74
75
    <section class="admin-subs">
76
      <h3>Subscriptions ({{len .Subscriptions}})</h3>
77
      <div class="feeds-list">
78
        {{range .Subscriptions}}
79
        <div class="feed-item">
80
          <h3 class="feed-title"><a href="{{.SiteURL}}" target="_blank" rel="noopener noreferrer">{{.Title}}</a></h3>
81
          {{if .LastFetchedAt}}<p class="feed-meta"><span class="feed-date">last: {{.LastFetchedAt}}</span>{{if .LastError}} <span class="error">· {{.LastError}}</span>{{end}}</p>{{end}}
82
          <form method="POST" action="/admin/feeds/{{.ID}}/category" class="inline">
83
            <input type="text" name="category_name" placeholder="category" list="categories-list" value="{{.CategoryName}}" />
84
            <button type="submit">Save</button>
85
          </form>
86
          <form method="POST" action="/admin/feeds/{{.ID}}/delete" class="inline"><button type="submit" class="danger">Delete</button></form>
87
        </div>
88
        {{end}}
89
      </div>
90
    </section>
91
92
    <script>
93
      (function() {
94
        const form = document.getElementById('add-feed-form');
95
        if (!form) return;
96
        form.addEventListener('submit', function() {
97
          const btn = document.getElementById('add-feed-submit');
98
          const label = document.getElementById('add-feed-label');
99
          btn.disabled = true;
100
          btn.classList.add('loading');
101
          label.innerHTML = 'Adding <span class="spinner"></span>';
102
        });
103
      })();
104
105
      (function() {
106
        const form = document.getElementById('opml-form');
107
        if (!form) return;
108
        form.addEventListener('submit', function() {
109
          const btn = document.getElementById('opml-submit');
110
          const label = document.getElementById('opml-submit-label');
111
          btn.disabled = true;
112
          btn.classList.add('loading');
113
          label.innerHTML = 'Importing <span class="spinner"></span>';
114
        });
115
      })();
116
117
      async function discoverFeeds() {
118
        const baseUrl = document.getElementById('base_url').value.trim();
119
        if (!baseUrl) return;
120
        const btn = document.getElementById('discover-btn');
121
        const status = document.getElementById('discover-status');
122
        const results = document.getElementById('discover-results');
123
        const feedInput = document.getElementById('feed_url');
124
        btn.disabled = true;
125
        btn.textContent = 'Searching...';
126
        status.style.display = 'none';
127
        results.style.display = 'none';
128
        results.innerHTML = '';
129
        try {
130
          const body = new URLSearchParams({ base_url: baseUrl });
131
          const resp = await fetch('/admin/discover-feeds', { method: 'POST', body });
132
          const data = await resp.json();
133
          if (!resp.ok) {
134
            status.textContent = data.error || 'No feeds found';
135
            status.className = 'discover-status error';
136
            status.style.display = 'block';
137
            return;
138
          }
139
          feedInput.value = data[0];
140
          status.textContent = data.length + ' feed(s) found';
141
          status.className = 'discover-status success';
142
          status.style.display = 'block';
143
          if (data.length > 1) {
144
            results.style.display = 'flex';
145
            data.forEach(function(url) {
146
              const item = document.createElement('button');
147
              item.type = 'button';
148
              item.className = 'discover-result-item' + (url === data[0] ? ' active' : '');
149
              item.textContent = url;
150
              item.onclick = function() {
151
                feedInput.value = url;
152
                results.querySelectorAll('.discover-result-item').forEach(function(el) { el.classList.remove('active'); });
153
                item.classList.add('active');
154
              };
155
              results.appendChild(item);
156
            });
157
          }
158
        } catch (e) {
159
          status.textContent = 'Request failed';
160
          status.className = 'discover-status error';
161
          status.style.display = 'block';
162
        } finally {
163
          btn.disabled = false;
164
          btn.textContent = 'Discover';
165
        }
166
      }
167
    </script>
168
  </body>
169
</html>