Commit 3cb69228e88227f3c6c7f749bca337c3158e0b9c

Authored by Adhidarma Hadiwinoto
1 parent 5844c8d4d7
Exists in master

relayout with panel

Showing 2 changed files with 10 additions and 0 deletions Inline Diff

1 var express = require('express'); 1 var express = require('express');
2 var app = express(); 2 var app = express();
3 var nunjucks = require('nunjucks'); 3 var nunjucks = require('nunjucks');
4 var passport = require('passport'); 4 var passport = require('passport');
5 var LocalStrategy = require('passport-local').Strategy; 5 var LocalStrategy = require('passport-local').Strategy;
6 var crypto = require('crypto'); 6 var crypto = require('crypto');
7 //var sha256sum = crypto.createHash('sha256'); 7 //var sha256sum = crypto.createHash('sha256');
8 var fsextra = require('fs-extra'); 8 var fsextra = require('fs-extra');
9 var strftime = require('strftime'); 9 var strftime = require('strftime');
10 var fs = require('fs'); 10 var fs = require('fs');
11 var ini = require('ini'); 11 var ini = require('ini');
12 var os = require('os'); 12 var os = require('os');
13 13
14 var config; 14 var config;
15 var aaa; 15 var aaa;
16 var matrix; 16 var matrix;
17 var view_path = __dirname + '/views'; 17 var view_path = __dirname + '/views';
18 18
19 function loggedIn(req, res, next) { 19 function loggedIn(req, res, next) {
20 if (req.user) { 20 if (req.user) {
21 next(); 21 next();
22 } else { 22 } else {
23 res.redirect('/login'); 23 res.redirect('/login');
24 } 24 }
25 } 25 }
26 26
27 function matchedPasswordAndHash(password, hash) { 27 function matchedPasswordAndHash(password, hash) {
28 var sha256sum = crypto.createHash('sha256'); 28 var sha256sum = crypto.createHash('sha256');
29 sha256sum.update(password); 29 sha256sum.update(password);
30 return (hash == sha256sum.digest('hex')) 30 return (hash == sha256sum.digest('hex'))
31 } 31 }
32 32
33 function renderDashboard(req, res) {
34 res.render('dashboard.html', {
35 gateway_name: config.globals.gateway_name,
36 title: 'Dashboard',
37 config: config,
38 });
39 }
40
33 function renderConfigIndex(req, res) { 41 function renderConfigIndex(req, res) {
34 42
35 masked = ['config.expresso.password']; 43 masked = ['config.expresso.password'];
36 44
37 res.render('config.index.html', { 45 res.render('config.index.html', {
38 gateway_name: config.globals.gateway_name, 46 gateway_name: config.globals.gateway_name,
39 title: 'CONFIG', configs: config, 47 title: 'CONFIG', configs: config,
40 config_dirty: matrix.config_dirty, 48 config_dirty: matrix.config_dirty,
41 isMask: function(scope, keyword) { 49 isMask: function(scope, keyword) {
42 return masked.indexOf('config.' + scope + '.' + keyword) >= 0; 50 return masked.indexOf('config.' + scope + '.' + keyword) >= 0;
43 }, 51 },
44 }); 52 });
45 } 53 }
46 54
47 function renderConfigEdit(req, res) { 55 function renderConfigEdit(req, res) {
48 var template = "config.edit.html"; 56 var template = "config.edit.html";
49 57
50 if (req.scope == 'expresso' && req.keyword == 'password') { 58 if (req.scope == 'expresso' && req.keyword == 'password') {
51 template = "config.edit.expresso.password.html"; 59 template = "config.edit.expresso.password.html";
52 } 60 }
53 61
54 res.render(template, { 62 res.render(template, {
55 gateway_name: config.globals.gateway_name, 63 gateway_name: config.globals.gateway_name,
56 title: 'Edit Konfigurasi', 64 title: 'Edit Konfigurasi',
57 scope: req.scope, 65 scope: req.scope,
58 keyword: req.keyword, 66 keyword: req.keyword,
59 old_value: config[req.scope][req.keyword] 67 old_value: config[req.scope][req.keyword]
60 }); 68 });
61 } 69 }
62 70
63 function renderConfigAdd(req, res){ 71 function renderConfigAdd(req, res){
64 72
65 var template = 'config.add.html'; 73 var template = 'config.add.html';
66 if (req.scope == 'products') { 74 if (req.scope == 'products') {
67 template = 'config.add.products.html'; 75 template = 'config.add.products.html';
68 } 76 }
69 77
70 res.render(template, { 78 res.render(template, {
71 gateway_name: config.globals.gateway_name, 79 gateway_name: config.globals.gateway_name,
72 title: 'Tambah Item Konfigurasi', 80 title: 'Tambah Item Konfigurasi',
73 scope: req.scope 81 scope: req.scope
74 }); 82 });
75 } 83 }
76 84
77 function submitPause(req, res) { 85 function submitPause(req, res) {
78 matrix.pause = 1; 86 matrix.pause = 1;
79 res.redirect('/'); 87 res.redirect('/');
80 return; 88 return;
81 } 89 }
82 90
83 function submitResume(req, res) { 91 function submitResume(req, res) {
84 matrix.pause = 0; 92 matrix.pause = 0;
85 res.redirect('/'); 93 res.redirect('/');
86 return; 94 return;
87 } 95 }
88 96
89 function submitTerminate(req, res) { 97 function submitTerminate(req, res) {
90 res.end('Terminating....'); 98 res.end('Terminating....');
91 process.exit(); 99 process.exit();
92 } 100 }
93 101
94 function submitConfigEdit(req, res) { 102 function submitConfigEdit(req, res) {
95 103
96 if (req.scope == 'expresso' && req.keyword == 'password') { 104 if (req.scope == 'expresso' && req.keyword == 'password') {
97 if (req.body.newValue != req.body.newValue2) { 105 if (req.body.newValue != req.body.newValue2) {
98 res.redirect('/config/edit/' + req.scope + '/' + req.keyword); 106 res.redirect('/config/edit/' + req.scope + '/' + req.keyword);
99 return; 107 return;
100 } 108 }
101 109
102 var sha256sum = crypto.createHash('sha256'); 110 var sha256sum = crypto.createHash('sha256');
103 sha256sum.update(req.body.newValue); 111 sha256sum.update(req.body.newValue);
104 req.body.newValue = sha256sum.digest('hex'); 112 req.body.newValue = sha256sum.digest('hex');
105 113
106 } else if (req.scope == 'globals' && req.keyword == 'products' && req.body.newValue.trim()) { 114 } else if (req.scope == 'globals' && req.keyword == 'products' && req.body.newValue.trim()) {
107 115
108 try { 116 try {
109 var unsortedString = req.body.newValue.toUpperCase().replace(/ /g, '').trim(); 117 var unsortedString = req.body.newValue.toUpperCase().replace(/ /g, '').trim();
110 var unsortedProducts = unsortedString.split(','); 118 var unsortedProducts = unsortedString.split(',');
111 var sortedProducts = aaa.sortProductsArray(unsortedProducts); 119 var sortedProducts = aaa.sortProductsArray(unsortedProducts);
112 req.body.newValue = sortedProducts.join(','); 120 req.body.newValue = sortedProducts.join(',');
113 } 121 }
114 catch(e) { 122 catch(e) {
115 console.log('Error sorting products'); 123 console.log('Error sorting products');
116 } 124 }
117 } 125 }
118 126
119 if (config[req.body.scope][req.body.keyword] != req.body.newValue.trim()) { 127 if (config[req.body.scope][req.body.keyword] != req.body.newValue.trim()) {
120 config[req.body.scope][req.body.keyword] = req.body.newValue.trim(); 128 config[req.body.scope][req.body.keyword] = req.body.newValue.trim();
121 matrix.config_dirty = 1; 129 matrix.config_dirty = 1;
122 } 130 }
123 131
124 res.redirect('/config'); 132 res.redirect('/config');
125 } 133 }
126 134
127 function submitConfigAdd(req, res) { 135 function submitConfigAdd(req, res) {
128 136
129 if (!req.body.newKeyword.trim()) { 137 if (!req.body.newKeyword.trim()) {
130 res.redirect('/config'); 138 res.redirect('/config');
131 return; 139 return;
132 } 140 }
133 141
134 if (config[req.body.scope] === undefined) { 142 if (config[req.body.scope] === undefined) {
135 config[req.body.scope] = {}; 143 config[req.body.scope] = {};
136 } 144 }
137 145
138 config[req.body.scope][req.body.newKeyword.trim()] = req.body.newValue.trim(); 146 config[req.body.scope][req.body.newKeyword.trim()] = req.body.newValue.trim();
139 matrix.config_dirty = 1; 147 matrix.config_dirty = 1;
140 148
141 res.redirect('/config#config.' + req.body.scope + '.' + req.body.newKeyword); 149 res.redirect('/config#config.' + req.body.scope + '.' + req.body.newKeyword);
142 } 150 }
143 151
144 function submitConfigDelete(req, res) { 152 function submitConfigDelete(req, res) {
145 153
146 matrix.config_dirty = 1; 154 matrix.config_dirty = 1;
147 delete config[req.scope][req.keyword]; 155 delete config[req.scope][req.keyword];
148 156
149 res.redirect('/config'); 157 res.redirect('/config');
150 } 158 }
151 159
152 function submitConfigSave(req, res) { 160 function submitConfigSave(req, res) {
153 fsextra.copy('config.ini', 'config.ini.backup_' + strftime('%F_%H%M%S', new Date()), function(err) { 161 fsextra.copy('config.ini', 'config.ini.backup_' + strftime('%F_%H%M%S', new Date()), function(err) {
154 fs.writeFileSync('./config.ini', ini.stringify(config)); 162 fs.writeFileSync('./config.ini', ini.stringify(config));
155 matrix.config_dirty = 0; 163 matrix.config_dirty = 0;
156 res.redirect('/config'); 164 res.redirect('/config');
157 }); 165 });
158 } 166 }
159 167
160 function renderLoginPage(req, res) { 168 function renderLoginPage(req, res) {
161 res.render('signin.html', {title: config.globals.gateway_name}); 169 res.render('signin.html', {title: config.globals.gateway_name});
162 } 170 }
163 171
164 function renderConfigAskDelete(req, res) { 172 function renderConfigAskDelete(req, res) {
165 res.render('config.ask.delete.html', { 173 res.render('config.ask.delete.html', {
166 gateway_name: config.globals.gateway_name, 174 gateway_name: config.globals.gateway_name,
167 title: 'Konfirmasi Penghapusan', 175 title: 'Konfirmasi Penghapusan',
168 scope: req.scope, 176 scope: req.scope,
169 keyword: req.keyword, 177 keyword: req.keyword,
170 value: config[req.scope][req.keyword], 178 value: config[req.scope][req.keyword],
171 }); 179 });
172 } 180 }
173 181
174 function renderRuntimeInfo(req, res) { 182 function renderRuntimeInfo(req, res) {
175 var template = "runtime-info.html"; 183 var template = "runtime-info.html";
176 184
177 var os_info = { 185 var os_info = {
178 uptime: os.uptime(), 186 uptime: os.uptime(),
179 loadavg: os.loadavg(), 187 loadavg: os.loadavg(),
180 hostname: os.hostname(), 188 hostname: os.hostname(),
181 type: os.type(), 189 type: os.type(),
182 platform: os.platform(), 190 platform: os.platform(),
183 arch: os.arch(), 191 arch: os.arch(),
184 release: os.release(), 192 release: os.release(),
185 totalmem: os.totalmem(), 193 totalmem: os.totalmem(),
186 } 194 }
187 195
188 var pendings; 196 var pendings;
189 try { 197 try {
190 pendings = JSON.stringify(aaa.getPendingList(), null, 2); 198 pendings = JSON.stringify(aaa.getPendingList(), null, 2);
191 } 199 }
192 catch(err) { 200 catch(err) {
193 pendings = "{}"; 201 pendings = "{}";
194 } 202 }
195 203
196 res.render(template, { 204 res.render(template, {
197 gateway_name: config.globals.gateway_name, 205 gateway_name: config.globals.gateway_name,
198 title: 'Runtime Info', 206 title: 'Runtime Info',
199 matrix: JSON.stringify(matrix, null, 2), 207 matrix: JSON.stringify(matrix, null, 2),
200 config: JSON.stringify(config, null, 2), 208 config: JSON.stringify(config, null, 2),
201 nodejs_versions: JSON.stringify(process.versions, null, 2), 209 nodejs_versions: JSON.stringify(process.versions, null, 2),
202 pendings: pendings, 210 pendings: pendings,
203 memory_usage: JSON.stringify(process.memoryUsage(), null, 2), 211 memory_usage: JSON.stringify(process.memoryUsage(), null, 2),
204 uptime: process.uptime(), 212 uptime: process.uptime(),
205 os_info: JSON.stringify(os_info, null, 2), 213 os_info: JSON.stringify(os_info, null, 2),
206 net_ifaces: JSON.stringify(os.networkInterfaces(), null, 2), 214 net_ifaces: JSON.stringify(os.networkInterfaces(), null, 2),
207 }); 215 });
208 } 216 }
209 217
210 function isNoTemplateCache() { 218 function isNoTemplateCache() {
211 var retval = false; 219 var retval = false;
212 try { 220 try {
213 retval = config.expresso.no_template_cache.toUpperCase() == 'YES'; 221 retval = config.expresso.no_template_cache.toUpperCase() == 'YES';
214 } 222 }
215 catch(err) { 223 catch(err) {
216 return retval; 224 return retval;
217 } 225 }
218 226
219 return retval; 227 return retval;
220 } 228 }
221 229
222 function createServer() { 230 function createServer() {
223 if (!config.expresso || !config.expresso.listen_port) { 231 if (!config.expresso || !config.expresso.listen_port) {
224 console.log('Not starting expresso admin UI'); 232 console.log('Not starting expresso admin UI');
225 return; 233 return;
226 } 234 }
227 235
228 nunjucks.configure(view_path, { 236 nunjucks.configure(view_path, {
229 autoescape: true, 237 autoescape: true,
230 noCache: isNoTemplateCache(), 238 noCache: isNoTemplateCache(),
231 express: app 239 express: app
232 }); 240 });
233 241
234 app.use(express.static(__dirname + '/public')); 242 app.use(express.static(__dirname + '/public'));
235 app.use(require('cookie-parser')()); 243 app.use(require('cookie-parser')());
236 app.use(require('body-parser').urlencoded({ extended: true })); 244 app.use(require('body-parser').urlencoded({ extended: true }));
237 245
238 var express_session_opts = { 246 var express_session_opts = {
239 secret: 'keyboard cat', 247 secret: 'keyboard cat',
240 resave: false, 248 resave: false,
241 saveUninitialized: false 249 saveUninitialized: false
242 }; 250 };
243 251
244 if (config.expresso && config.expresso.session_name) { 252 if (config.expresso && config.expresso.session_name) {
245 express_session_opts.name = config.expresso.session_name; 253 express_session_opts.name = config.expresso.session_name;
246 } 254 }
247 255
248 app.use(require('express-session')(express_session_opts)); 256 app.use(require('express-session')(express_session_opts));
249 app.use(passport.initialize()); 257 app.use(passport.initialize());
250 app.use(passport.session()); 258 app.use(passport.session());
251 259
252 passport.use(new LocalStrategy( 260 passport.use(new LocalStrategy(
253 function(username, password, done) { 261 function(username, password, done) {
254 262
255 if (username == 'admin' && matchedPasswordAndHash(password, config.expresso.password)) { 263 if (username == 'admin' && matchedPasswordAndHash(password, config.expresso.password)) {
256 var user = { 264 var user = {
257 username: username, 265 username: username,
258 last_login: Date.now() / 1000 | 0 266 last_login: Date.now() / 1000 | 0
259 } 267 }
260 268
261 return done(null, user); 269 return done(null, user);
262 } 270 }
263 271
264 return done(null, false, { message: 'Incorrect password.' }); 272 return done(null, false, { message: 'Incorrect password.' });
265 } 273 }
266 )); 274 ));
267 275
268 passport.serializeUser(function(user, done) { 276 passport.serializeUser(function(user, done) {
269 done(null, user.username); 277 done(null, user.username);
270 }); 278 });
271 279
272 passport.deserializeUser(function(username, done) { 280 passport.deserializeUser(function(username, done) {
273 var user = { 281 var user = {
274 username: username 282 username: username
275 }; 283 };
276 done(null, user); 284 done(null, user);
277 }); 285 });
278 286
279 app.get('/', loggedIn, function(req, res) { res.redirect('/runtime-info'); }); 287 app.get('/', loggedIn, function(req, res) { res.redirect('/runtime-info'); });
288 app.get('/dashboard', loggedIn, renderDashboard);
280 app.get('/config', loggedIn, renderConfigIndex); 289 app.get('/config', loggedIn, renderConfigIndex);
281 app.get('/login', renderLoginPage); 290 app.get('/login', renderLoginPage);
282 app.post('/login', passport.authenticate('local', { successRedirect: '/', failureRedirect: '/login' })); 291 app.post('/login', passport.authenticate('local', { successRedirect: '/', failureRedirect: '/login' }));
283 app.get('/logout', function(req, res){ 292 app.get('/logout', function(req, res){
284 req.logout(); 293 req.logout();
285 res.redirect('/login'); 294 res.redirect('/login');
286 }); 295 });
287 296
288 app.param('scope', function(req, res, next, value) { 297 app.param('scope', function(req, res, next, value) {
289 req.scope = value; 298 req.scope = value;
290 next(); 299 next();
291 }); 300 });
292 301
293 app.param('keyword', function(req, res, next, value) { 302 app.param('keyword', function(req, res, next, value) {
294 req.keyword = value; 303 req.keyword = value;
295 next(); 304 next();
296 }); 305 });
297 306
298 app.get('/config/edit/:scope/:keyword', loggedIn, renderConfigEdit); 307 app.get('/config/edit/:scope/:keyword', loggedIn, renderConfigEdit);
299 app.post('/config/edit/:scope/:keyword', loggedIn, submitConfigEdit); 308 app.post('/config/edit/:scope/:keyword', loggedIn, submitConfigEdit);
300 309
301 app.get('/config/ask-delete/:scope/:keyword', loggedIn, renderConfigAskDelete); 310 app.get('/config/ask-delete/:scope/:keyword', loggedIn, renderConfigAskDelete);
302 app.get('/config/delete/:scope/:keyword', loggedIn, submitConfigDelete); 311 app.get('/config/delete/:scope/:keyword', loggedIn, submitConfigDelete);
303 312
304 app.get('/config/add/:scope', loggedIn, renderConfigAdd); 313 app.get('/config/add/:scope', loggedIn, renderConfigAdd);
305 app.post('/config/add/:scope', loggedIn, submitConfigAdd); 314 app.post('/config/add/:scope', loggedIn, submitConfigAdd);
306 315
307 app.get('/config/save', loggedIn, submitConfigSave); 316 app.get('/config/save', loggedIn, submitConfigSave);
308 317
309 app.get('/pause', loggedIn, submitPause); 318 app.get('/pause', loggedIn, submitPause);
310 app.get('/resume', loggedIn, submitResume); 319 app.get('/resume', loggedIn, submitResume);
311 app.get('/terminate', loggedIn, submitTerminate); 320 app.get('/terminate', loggedIn, submitTerminate);
312 321
313 app.get('/runtime-info', loggedIn, renderRuntimeInfo); 322 app.get('/runtime-info', loggedIn, renderRuntimeInfo);
314 323
315 var server = app.listen(config.expresso.listen_port, function () { 324 var server = app.listen(config.expresso.listen_port, function () {
316 var host = server.address().address; 325 var host = server.address().address;
317 var port = server.address().port; 326 var port = server.address().port;
318 327
319 console.log('Expresso admin UI listening at http://%s:%s', host, port); 328 console.log('Expresso admin UI listening at http://%s:%s', host, port);
320 }); 329 });
321 } 330 }
322 331
323 function sanitizeMatrix(matrix) { 332 function sanitizeMatrix(matrix) {
324 if (matrix === undefined) { 333 if (matrix === undefined) {
325 matrix = {}; 334 matrix = {};
326 } 335 }
327 if (matrix.config_dirty === undefined) { 336 if (matrix.config_dirty === undefined) {
328 matrix.config_dirty = 0; 337 matrix.config_dirty = 0;
329 } 338 }
330 } 339 }
331 340
332 function start(options) { 341 function start(options) {
333 if (options['config']) { 342 if (options['config']) {
334 config = options['config']; 343 config = options['config'];
335 } 344 }
336 345
337 if (options['matrix']) { 346 if (options['matrix']) {
338 matrix = options['matrix']; 347 matrix = options['matrix'];
339 } 348 }
340 sanitizeMatrix(matrix); 349 sanitizeMatrix(matrix);
341 350
342 if (options['aaa']) { 351 if (options['aaa']) {
343 aaa = options['aaa']; 352 aaa = options['aaa'];
344 } 353 }
345 354
346 if (options['expresso_views']) { 355 if (options['expresso_views']) {
347 view_path = options['expresso_views']; 356 view_path = options['expresso_views'];
348 } 357 }
349 358
350 createServer(); 359 createServer();
351 } 360 }
352 361
353 exports.start = start; 362 exports.start = start;
354 363
views/layout-with-topbar.html
1 <!DOCTYPE html> 1 <!DOCTYPE html>
2 <html lang="en"> 2 <html lang="en">
3 <head> 3 <head>
4 <meta charset="utf-8"> 4 <meta charset="utf-8">
5 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge">
6 <meta name="viewport" content="width=device-width, initial-scale=1"> 6 <meta name="viewport" content="width=device-width, initial-scale=1">
7 <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags --> 7 <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
8 <meta name="description" content=""> 8 <meta name="description" content="">
9 <meta name="author" content=""> 9 <meta name="author" content="">
10 <link rel="icon" href="/favicon.ico"> 10 <link rel="icon" href="/favicon.ico">
11 11
12 <title>{{ gateway_name }} - {{ title }}</title> 12 <title>{{ gateway_name }} - {{ title }}</title>
13 13
14 <!-- Bootstrap core CSS --> 14 <!-- Bootstrap core CSS -->
15 <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> 15 <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
16 16
17 <!-- Custom styles for this template --> 17 <!-- Custom styles for this template -->
18 <link href="/css/starter-template.css" rel="stylesheet"> 18 <link href="/css/starter-template.css" rel="stylesheet">
19 19
20 <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries --> 20 <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
21 <!--[if lt IE 9]> 21 <!--[if lt IE 9]>
22 <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script> 22 <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
23 <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script> 23 <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
24 <![endif]--> 24 <![endif]-->
25 </head> 25 </head>
26 26
27 <body> 27 <body>
28 28
29 <nav class="navbar navbar-inverse navbar-fixed-top"> 29 <nav class="navbar navbar-inverse navbar-fixed-top">
30 <div class="container"> 30 <div class="container">
31 <div class="navbar-header"> 31 <div class="navbar-header">
32 <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"> 32 <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
33 <span class="sr-only">Toggle navigation</span> 33 <span class="sr-only">Toggle navigation</span>
34 <span class="icon-bar"></span> 34 <span class="icon-bar"></span>
35 <span class="icon-bar"></span> 35 <span class="icon-bar"></span>
36 <span class="icon-bar"></span> 36 <span class="icon-bar"></span>
37 </button> 37 </button>
38 <a class="navbar-brand" href="/">{{ gateway_name}}</a> 38 <a class="navbar-brand" href="/">{{ gateway_name}}</a>
39 </div> 39 </div>
40 <div id="navbar" class="collapse navbar-collapse"> 40 <div id="navbar" class="collapse navbar-collapse">
41 <ul class="nav navbar-nav"> 41 <ul class="nav navbar-nav">
42 <li><a href="/">Home</a></li> 42 <li><a href="/">Home</a></li>
43 <li><a href="/dashboard">Dashboard</a></li>
43 <li><a href="/config">Config</a></li> 44 <li><a href="/config">Config</a></li>
44 <li><a href="/runtime-info">Runtime</a></li> 45 <li><a href="/runtime-info">Runtime</a></li>
45 <li><a href="/logout">Logout</a></li> 46 <li><a href="/logout">Logout</a></li>
46 </ul> 47 </ul>
47 </div><!--/.nav-collapse --> 48 </div><!--/.nav-collapse -->
48 </div> 49 </div>
49 </nav> 50 </nav>
50 51
51 <div class="container"> 52 <div class="container">
52 53
53 <div class="starter-template"> 54 <div class="starter-template">
54 <h1>{{ title }}</h1> 55 <h1>{{ title }}</h1>
55 56
56 {% block content %} 57 {% block content %}
57 58
58 <p class="lead">Use this document as a way to quickly start any new project.<br> All you get is this text and a mostly barebones HTML document.</p> 59 <p class="lead">Use this document as a way to quickly start any new project.<br> All you get is this text and a mostly barebones HTML document.</p>
59 60
60 {% endblock %} 61 {% endblock %}
61 62
62 </div> 63 </div>
63 64
64 </div><!-- /.container --> 65 </div><!-- /.container -->
65 66
66 67
67 <!-- Bootstrap core JavaScript 68 <!-- Bootstrap core JavaScript
68 ================================================== --> 69 ================================================== -->
69 <!-- Placed at the end of the document so the pages load faster --> 70 <!-- Placed at the end of the document so the pages load faster -->
70 <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> 71 <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
71 <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> 72 <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
72 </body> 73 </body>
73 </html> 74 </html>
74 75