File size: 8,935 Bytes
e28c9e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
{% extends "base.html" %}

{% block title %}管理员后台{% endblock %}

{% block header_actions %}
  <span class="status-pill status-live">{{ admin.role }} · {{ admin.username }}</span>
  <form method="post" action="{{ url_for('admin_logout') }}">
    <button type="submit" class="button button-ghost">退出后台</button>
  </form>
{% endblock %}

{% block content %}
<section class="hero-card">
  <div class="hero-copy-block">
    <div class="eyebrow">管理员后台</div>
    <h1>统一管理用户、并发调度与全局实时日志。</h1>
    <p class="hero-copy">
      用户提交的课程配置会集中展示在这里。管理员可以手动录入用户信息、代为启动任务,并根据 Hugging Face Space 资源调整并行数。
    </p>
  </div>
  <div class="stat-grid admin-stat-grid">
    <div class="stat-card">
      <span>用户数</span>
      <strong id="admin-users">{{ summary.users }}</strong>
    </div>
    <div class="stat-card">
      <span>运行中任务</span>
      <strong id="admin-running">{{ summary.running_tasks }}</strong>
    </div>
    <div class="stat-card">
      <span>排队任务</span>
      <strong id="admin-queued">{{ summary.queued_tasks }}</strong>
    </div>
    <div class="stat-card">
      <span>并行数</span>
      <strong id="admin-parallelism">{{ summary.parallelism }}</strong>
    </div>
  </div>
</section>

<section class="content-grid content-grid-admin">
  <article class="panel">
    <div class="panel-head">
      <div>
        <div class="section-kicker">系统设置</div>
        <h2>并行调度</h2>
      </div>
      <span class="status-pill status-pending">资源敏感</span>
    </div>
    <form method="post" action="{{ url_for('update_parallelism') }}" class="form-grid form-grid-inline">
      <label class="field">
        <span>最大并行任务数</span>
        <input type="number" name="parallelism" min="1" max="8" value="{{ summary.parallelism }}" required>
      </label>
      <button type="submit" class="button button-primary">保存并行数</button>
    </form>
    <p class="muted-note">建议根据 Space 资源从 1 开始逐步增加。每个任务都会独立启动一个无头浏览器实例。</p>
  </article>

  <article class="panel">
    <div class="panel-head">
      <div>
        <div class="section-kicker">用户录入</div>
        <h2>手动添加或更新用户</h2>
      </div>
      <span class="status-pill status-live">管理员可写</span>
    </div>
    <form method="post" action="{{ url_for('create_or_update_user_from_admin') }}" class="form-grid">
      <label class="field">
        <span>学号</span>
        <input type="text" name="student_id" inputmode="numeric" placeholder="学生学号" required>
      </label>
      <label class="field">
        <span>密码</span>
        <input type="password" name="password" placeholder="学生密码" required>
      </label>
      <label class="field">
        <span>初始课程号</span>
        <input type="text" name="course_id" inputmode="numeric" placeholder="可选">
      </label>
      <label class="field">
        <span>初始课序号</span>
        <input type="text" name="course_index" inputmode="numeric" maxlength="2" placeholder="可选">
      </label>
      <button type="submit" class="button button-primary">保存用户</button>
    </form>
  </article>

  {% if admin.role == "superadmin" %}
  <article class="panel">
    <div class="panel-head">
      <div>
        <div class="section-kicker">权限管理</div>
        <h2>创建普通管理员</h2>
      </div>
      <span class="status-pill status-live">仅超级管理员</span>
    </div>
    <form method="post" action="{{ url_for('create_admin_account') }}" class="form-grid form-grid-inline">
      <label class="field">
        <span>管理员账号</span>
        <input type="text" name="username" placeholder="admin-ops" required>
      </label>
      <label class="field">
        <span>管理员密码</span>
        <input type="password" name="password" placeholder="管理员密码" required>
      </label>
      <button type="submit" class="button button-ghost">创建管理员</button>
    </form>
    <div class="chip-wrap">
      {% for item in admins %}
        <span class="feature-pill">{{ item.username }} · {{ item.role }}</span>
      {% endfor %}
    </div>
  </article>
  {% endif %}
</section>

<section class="panel">
  <div class="panel-head">
    <div>
      <div class="section-kicker">用户列表</div>
      <h2>多用户管理</h2>
    </div>
    <span class="status-pill status-live">管理员全量可见</span>
  </div>

  <div class="user-grid">
    {% if users %}
      {% for item in users %}
        <article class="user-card">
          <div class="user-card-head">
            <div>
              <div class="user-code">{{ item.student_id }}</div>
              <div class="user-meta-text">已选 {{ item.selected_count }}/{{ item.course_count }} · 最近登录 {{ item.last_login_at or "尚未登录" }}</div>
            </div>
            <span class="status-pill status-{{ item.active_task_status or 'idle' }}">{{ item.active_task_status or "idle" }}</span>
          </div>

          <div class="button-row compact">
            <form method="post" action="{{ url_for('start_user_task_from_admin', user_id=item.id) }}">
              <button type="submit" class="button button-primary button-small">启动</button>
            </form>
            <form method="post" action="{{ url_for('stop_user_task_from_admin', user_id=item.id) }}">
              <button type="submit" class="button button-secondary button-small">停止</button>
            </form>
          </div>

          <form method="post" action="{{ url_for('update_user_password_from_admin', user_id=item.id) }}" class="form-grid">
            <label class="field">
              <span>重置密码</span>
              <input type="password" name="password" placeholder="输入新密码" required>
            </label>
            <button type="submit" class="button button-ghost button-small">更新</button>
          </form>

          <form method="post" action="{{ url_for('add_user_course_from_admin', user_id=item.id) }}" class="form-grid form-grid-inline">
            <label class="field">
              <span>课程号</span>
              <input type="text" name="course_id" inputmode="numeric" placeholder="课程号" required>
            </label>
            <label class="field">
              <span>课序号</span>
              <input type="text" name="course_index" inputmode="numeric" maxlength="2" placeholder="01" required>
            </label>
            <button type="submit" class="button button-primary button-small">新增课程</button>
          </form>

          <div class="course-chip-grid">
            {% if item.courses %}
              {% for course in item.courses %}
                <div class="course-chip course-chip-{{ course.status }}">
                  <div>
                    <strong>{{ course.course_id }}_{{ course.course_index }}</strong>
                    <p>{{ course.last_result or "等待执行" }}</p>
                  </div>
                  <form method="post" action="{{ url_for('delete_user_course_from_admin', user_id=item.id, course_record_id=course.id) }}">
                    <button type="submit" class="button button-danger button-small"></button>
                  </form>
                </div>
              {% endfor %}
            {% else %}
              <div class="empty-state small">这个用户还没有课程记录。</div>
            {% endif %}
          </div>
        </article>
      {% endfor %}
    {% else %}
      <div class="empty-state">当前还没有任何用户,先在上方录入用户信息。</div>
    {% endif %}
  </div>
</section>

<section class="panel console-panel">
  <div class="panel-head">
    <div>
      <div class="section-kicker">全局日志</div>
      <h2>管理员实时控制台</h2>
    </div>
    <span class="status-pill status-live">Live</span>
  </div>
  <div
    class="log-console"
    id="admin-log-stream"
    data-last-id="{{ logs[-1].id if logs else 0 }}"
    data-empty="当前还没有系统日志。"
  >
    {% if logs %}
      {% for log in logs %}
        <div class="log-line log-{{ log.level|lower }}">
          <span class="log-time">{{ log.created_at }}</span>
          <span class="log-actor">{{ log.student_id or log.actor }}</span>
          <span class="log-message">{{ log.message }}</span>
        </div>
      {% endfor %}
    {% else %}
      <div class="log-placeholder">当前还没有系统日志。</div>
    {% endif %}
  </div>
</section>

<script>
  window.addEventListener("DOMContentLoaded", () => {
    SCUApp.initLogStream("admin-log-stream", "{{ url_for('admin_log_stream') }}");
    SCUApp.initAdminStatus("{{ url_for('admin_status') }}");
  });
</script>
{% endblock %}