sharing and UX improvements

master
glmdev 5 years ago
parent 1bd6ad1830
commit 2a80e65c35

@ -10,7 +10,7 @@ const Unit = require('libflitter/Unit')
* other units in the stack. * other units in the stack.
*/ */
class MiscUnit extends Unit { class MiscUnit extends Unit {
/* /*
* Initializes the unit class. * Initializes the unit class.
* This is called OUTSIDE of a Flitter context, * This is called OUTSIDE of a Flitter context,
@ -19,10 +19,10 @@ class MiscUnit extends Unit {
*/ */
constructor(){ constructor(){
super() super()
} }
/* /*
* Initialize the actual Unit. * Initialize the actual Unit.
* This is where most of the changes will go. * This is where most of the changes will go.
@ -32,15 +32,75 @@ class MiscUnit extends Unit {
* model(), etc. work. * model(), etc. work.
*/ */
async go(app, context){ async go(app, context){
// do stuff here // do stuff here
global.devbug = {
version: '0.2.0',
code: {
php: `<?php
// ===========================================================
// DEVBUG INLINE DEBUGGING HELPER - FOR USE WITH DEVBUG SERVER
// TODO: REMOVE BEFORE COMMITTING
$dev_outs = [];
function out($key, $what, $group = null){
\tglobal $dev_outs;
\tif ( $group ){
\t\tif ( !array_key_exists($group, $dev_outs) ){
\t\t\t$dev_outs[$group] = [ $key => $what ];
\t\t}
\t\telse {
\t\t\t$dev_outs[$group][$key] = $what;
\t\t}
\t}
\telse {
\t\t$dev_outs[$key] = $what;
\t}
}
function breakpoint($html = false, $name = null){
\tglobal $dev_outs;
\t$devbug = "http://CHANGEME:8000/";
\t$project_api_key = "CHANGEME";
\t$bt = debug_backtrace();
\t$caller = array_shift($bt);
\tif ( !$html ){
\t\tvar_dump([($item ? $item : $caller), 'outs' => $dev_outs]);
\t}
\telse {
\t\techo "<pre><code>";
\t\tvar_dump([($item ? $item : $caller), 'outs' => $dev_outs]);
\t\techo "</code></pre>";
\t}
\t// Send to devbug server
\t$ch = curl_init();
\t$url = $devbug.'api/v1/out/'.$project_api_key;
\tvar_dump($url);
\tcurl_setopt($ch, CURLOPT_URL, $url);
\tcurl_setopt($ch, CURLOPT_POST, 1);
\tcurl_setopt($ch, CURLOPT_POSTFIELDS, [
\t 'data' => json_encode([
\t 'brief' => ($name ? $name : 'Breakpoint').': '.$caller['file'].': '.$caller['line'],
\t 'data' => $dev_outs,
\t ])
\t]);
\tcurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
\t
\t$odata = curl_exec($ch);
\texit();
}
// ===========================================================`
}
}
} }
name(){ name(){
return "misc" return "misc"
} }
} }
module.exports = exports = MiscUnit module.exports = exports = MiscUnit

@ -2,7 +2,7 @@
@import url('https://fonts.googleapis.com/css?family=Source+Code+Pro'); @import url('https://fonts.googleapis.com/css?family=Source+Code+Pro');
html { html {
font-family: "Source Sans Pro"; font-family: "Source Sans Pro", sans-serif;
} }
table, th, td { table, th, td {
@ -17,7 +17,7 @@ th, td {
} }
pre, code { pre, code {
font-family: "Source Code Pro"; font-family: "Source Code Pro", monospace;
font-size: 10pt; font-size: 10pt;
} }
@ -25,11 +25,44 @@ a {
color: #004d4d; color: #004d4d;
} }
.page-header {
background: #ccdddd;
width: 100%;
margin: 0;
padding: 0;
padding-left: 20px;
padding-right: 20px;
position: fixed;
top: 0;
left: 0;
}
.devbug-header {
background: #509d9d;
width: 100%;
position: relative;
margin: 0;
padding: 5px;
padding-left: 20px;
margin-left: -20px;
color: white;
}
.spacer {
min-height: 165px;
}
.navul { .navul {
list-style-type: none; list-style-type: none;
margin: 0; margin: 0;
padding: 5; padding: 0;
margin-bottom: 20px; padding-left: 20px;
padding-top: 10px;
padding-bottom: 10px;
margin-left: -20px;
position: relative;
width: 100%;
background: #004d4d;
} }
.navli { .navli {
@ -41,7 +74,7 @@ a {
} }
.navli:hover { .navli:hover {
background: #004d4d; background: #509d9d;
} }
.nava { .nava {
@ -52,4 +85,11 @@ a {
.nava:hover { .nava:hover {
color: #eee; color: #eee;
} }
pre {outline: 1px solid #ccc; padding: 5px; margin: 5px; }
.string { color: darkslateblue; }
.number { color: darkorange; }
.boolean { color: blue; }
.null { color: magenta; }
.key { color: green; }

@ -0,0 +1,29 @@
function output(inp) {
document.body.appendChild(document.createElement('pre')).innerHTML = inp;
}
function syntaxHighlight(json) {
json = JSON.stringify(JSON.parse(json.replace(/&quot;/g, '"')), undefined, 4)
json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
var cls = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key';
} else {
cls = 'string';
}
} else if (/true|false/.test(match)) {
cls = 'boolean';
} else if (/null/.test(match)) {
cls = 'null';
}
return '<span class="' + cls + '">' + match + '</span>';
});
}
function from_server(json_string){
console.log(json_string)
json_string = JSON.parse(json_string.replace(/&quot;/g, '"'))
window.devbug = json_string
}

@ -38,9 +38,22 @@ class v1 {
error: 'Missing data.' error: 'Missing data.'
}) })
} }
const data = JSON.parse(req.body.data) let data
try {
data = JSON.parse(req.body.data)
}
catch (e){
return res.status(400).send({
success: false,
error: 'Invalid JSON.'
})
}
if ( !data.brief ){
data.brief = 'Breakpoint. (No Message.)'
}
const out = new Out({ const out = new Out({
brief: data.brief, brief: data.brief,
data: JSON.stringify(data.data), data: JSON.stringify(data.data),
@ -55,4 +68,4 @@ class v1 {
} }
} }
module.exports = exports = v1 module.exports = exports = v1

@ -13,6 +13,16 @@ class v1 {
async main(req, res){ async main(req, res){
const projects = await Project.find({ archived: false, user_id: req.session.auth.uuid }) const projects = await Project.find({ archived: false, user_id: req.session.auth.uuid })
let find = {
shared_user_ids: {
$elemMatch: {
$eq: req.session.auth.uuid
}
}
}
const shared_projects = await Project.find(find)
/* /*
* Return the main view. * Return the main view.
@ -20,7 +30,7 @@ class v1 {
* View parameters can be passed as an optional third * View parameters can be passed as an optional third
* argument to the view() method. * argument to the view() method.
*/ */
return _flitter.view(res, 'dash_v1:main', { projects }) return _flitter.view(res, 'dash_v1:main', { projects, shared_projects, user: req.session.auth.user })
} }
new_project_show(req, res, next){ new_project_show(req, res, next){
@ -30,20 +40,30 @@ class v1 {
async project_edit_show(req, res, next){ async project_edit_show(req, res, next){
const project = await Project.findById(req.params.id) const project = await Project.findById(req.params.id)
if ( !project ){ if ( !project ){
return _flitter.error(res, 404, 'Project not found with the specified ID.') return _flitter.error(res, 404, {reason: 'Project not found with the specified ID.'})
}
// check access perms
if ( !(project.user_id === req.session.auth.uuid) ){
return _flitter.error(res, 401, {reason: 'You do not have permissions to edit this project.'})
} }
return _flitter.view(res, 'dash_v1:project', { show_back: true, title: 'Update Project', project_name: project.name}) return _flitter.view(res, 'dash_v1:project', { show_back: true, title: 'Update Project', project_name: project.name, user: req.session.auth.user })
} }
async project_edit_do(req, res, next){ async project_edit_do(req, res, next){
const project = await Project.findById(req.params.id) const project = await Project.findById(req.params.id)
if ( !project ){ if ( !project ){
return _flitter.error(res, 404, 'Project not found with the specified ID.') return _flitter.error(res, 404, {reason: 'Project not found with the specified ID.'})
} }
if ( !req.body || !req.body.name ){ if ( !req.body || !req.body.name ){
return _flitter.view(res, 'dash_v1:project', {show_back: true, title: 'Update Project', project_name: project.name, errors: ['Project name is required.']}) return _flitter.view(res, 'dash_v1:project', {user: req.session.auth.user, show_back: true, title: 'Update Project', project_name: project.name, errors: ['Project name is required.']})
}
// check access perms
if ( !(project.user_id === req.session.auth.uuid) ){
return _flitter.error(res, 401, {reason: 'Project not found with the specified ID.'})
} }
project.name = req.body.name project.name = req.body.name
@ -54,7 +74,7 @@ class v1 {
async new_project_do(req, res, next){ async new_project_do(req, res, next){
if ( !req.body.name ){ if ( !req.body.name ){
return _flitter.view(res, 'dash_v1:project', {show_back: true, title: 'Create Project', errors: ['Project name is required.']}) return _flitter.view(res, 'dash_v1:project', {user: req.session.auth.user, show_back: true, title: 'Create Project', errors: ['Project name is required.']})
} }
const project = new Project({ const project = new Project({
@ -76,31 +96,78 @@ class v1 {
const project = await Project.findById(req.params.id) const project = await Project.findById(req.params.id)
if ( !project ){ if ( !project ){
_flitter.error(res, 404, 'Project not found.') return _flitter.error(res, 404, {reason: 'Project not found with the specified ID.'})
} }
const outs = await Out.find({ project_id: project.id }).sort('-created') const outs = await Out.find({ project_id: project.id }).sort('-created')
return _flitter.view(res, 'dash_v1:view', { project, outs, show_back: true, title: 'View: '+project.name }) if ( !(project.user_id === req.session.auth.uuid) && !(project.shared_user_ids.includes(req.session.auth.uuid)) ){
return _flitter.error(res, 401, {reason: 'You do not have permission to view this project.'})
}
return _flitter.view(res, 'dash_v1:view', {user: req.session.auth.user, project, outs, show_back: true, title: 'View: '+project.name })
} }
async out_view(req, res, next){ async out_view(req, res, next){
const out = await Out.findById(req.params.id) const out = await Out.findById(req.params.id)
if ( !out ){
return _flitter.error(res, 404, {reason: 'Output not found with the specified ID.'})
}
let pretty
try {
pretty = JSON.stringify(JSON.parse(out.data), null, 4)
}
catch (e){
return _flitter.error(res, 500, {reason: 'Unable to parse output data. Data contains invalid JSON.'})
}
const project = await Project.findById(out.project_id)
console.log(out.data) if ( !project || (!(project.user_id === req.session.auth.uuid) && !(project.shared_user_ids.includes(req.session.auth.uuid))) ){
return _flitter.error(res, 401, {reason: 'You do not have permission to view this project.'})
const pretty = JSON.stringify(JSON.parse(out.data), null, 4) }
// TODO permission access check // TODO permission access check
return _flitter.view(res, 'dash_v1:out', {out, prettyd:pretty, show_back: true, title: out.brief, title_small: true }); return _flitter.view(res, 'dash_v1:out', {user: req.session.auth.user, out, prettyd:pretty, show_back: true, title: out.brief, title_small: true });
} }
async out_delete(req, res, next){
const out = await Out.findById(req.params.id)
project_delete_show(req, res, next){ const project = await Project.findById(req.params.project)
return _flitter.view(res, 'dash_v1:confirm', {show_back: true, title: 'Are you sure?', text: 'Deleting this project will remove all stored breakpoint data. This action cannot be undone.', destination: '/dash/v1/project/delete/'+req.params.id}) if ( !project || ( !(project.user_id === req.session.auth.uuid) ) ){
return _flitter.error(res, 401, {reason: 'You do not have permission to edit this project.'})
}
if ( out ){
await out.delete()
}
return res.redirect('/dash/v1/project/view/'+req.params.project)
}
async project_delete_show(req, res, next){
const project = await Project.findById(req.params.id)
if ( !project ){
return _flitter.error(res, 404, {reason: 'Project not found with the specified ID.'})
}
if ( !(project.user_id === req.session.auth.uuid) ){
return _flitter.error(res, 401, {reason: 'You do not have permission to edit this project.'})
}
return _flitter.view(res, 'dash_v1:confirm', {user: req.session.auth.user, project, show_back: true, title: 'Are you sure?', text: 'Deleting this project will remove all stored breakpoint data. This action cannot be undone.', destination: '/dash/v1/project/delete/'+req.params.id})
} }
async project_delete_do(req, res, next){ async project_delete_do(req, res, next){
const project = await Project.findById(req.params.id) const project = await Project.findById(req.params.id)
if ( project && ( !(project.user_id === req.session.auth.uuid) ) ){
return _flitter.error(res, 401, {reason: 'You do not have permission to edit this project.'})
}
if ( project ){ if ( project ){
const outs = await Out.find({project_id: project.id}) const outs = await Out.find({project_id: project.id})
@ -113,6 +180,90 @@ class v1 {
return res.redirect('/dash/v1') return res.redirect('/dash/v1')
} }
view_code(req, res, next){
return _flitter.view(res, 'dash_v1:code', { user: req.session.auth.user, title: 'Inline Code Snippets' })
}
async project_share_show(req, res, next){
const project = await Project.findById(req.params.id)
if ( !project ) return _flitter.error(res, 404, {reason: 'Project not found with the specified ID.'})
if ( !(project.user_id === req.session.auth.uuid) ) return _flitter.error(res, 401, {reason: 'You do not have permission to edit this project.'})
let find = {
uuid: { $nin: [] }
}
find.uuid.$nin.push(req.session.auth.uuid)
find.uuid.$nin = find.uuid.$nin.concat(project.shared_user_ids)
const to_share = await _flitter.model('User').find(find)
find = {
uuid: { $in: find.uuid.$nin }
}
const shared = await _flitter.model('User').find(find)
return _flitter.view(res, 'dash_v1:share', { user: req.session.auth.user, sharing: { to_share, shared }, project, title: 'Share Project: '+project.name, show_back: true })
}
async project_share_do(req, res, next){
const project = await Project.findById(req.params.id)
if ( !project ) return _flitter.error(res, 404, {reason: 'Project not found with the specified ID.'})
const target_user = await _flitter.model('User').findOne({uuid: req.params.user})
if ( !target_user ) return _flitter.error(res, 404, {reason: 'User not found with the specified ID.'})
if ( !(project.user_id === req.session.auth.uuid) ) return _flitter.error(res, 401, {reason: "You do not have permission to edit this project."})
if ( !(project.user_id === target_user.uuid) && !(project.shared_user_ids.includes(target_user.uuid)) ){
project.shared_user_ids.push(target_user.uuid)
await project.save()
}
return res.redirect('/dash/v1/project/share/'+project.id)
}
async project_share_revoke(req, res, next){
const project = await Project.findById(req.params.id)
if ( !project ) return _flitter.error(res, 404, {reason: 'Project not found with the specified ID.'})
const target_user = await _flitter.model('User').findOne({uuid: req.params.user})
if ( !target_user ) return _flitter.error(res, 404, {reason: 'User not found with the specified ID.'})
if ( !(project.user_id === req.session.auth.uuid || project.shared_user_ids.includes(req.session.auth.uuid)) ) return _flitter.error(res, 401, {reason: "You do not have permission to edit this project."})
const to_dash = project.shared_user_ids.includes(req.session.auth.uuid)
if ( !(target_user.uuid === project.user_id) && (project.shared_user_ids.includes(target_user.uuid)) ){
project.shared_user_ids.splice(project.shared_user_ids.indexOf(target_user.uuid), 1)
await project.save()
}
if ( to_dash ) return res.redirect('/dash/v1')
return res.redirect('/dash/v1/project/share/'+project.id)
}
async project_share_transfer(req, res, next){
const project = await Project.findById(req.params.id)
if ( !project ) return _flitter.error(res, 404, {reason: 'Project not found with the specified ID.'})
const target_user = await _flitter.model('User').findOne({uuid: req.params.user})
if ( !target_user ) return _flitter.error(res, 404, {reason: 'User not found with the specified ID.'})
if ( !project.user_id === req.session.auth.uuid ) return _flitter.error(res, 401, {reason: 'You do not have permission to edit this project.'})
project.user_id = target_user.uuid
project.shared_user_ids.push(req.session.auth.uuid)
await project.save()
return res.redirect('/dash/v1')
}
} }
module.exports = exports = v1 module.exports = exports = v1

@ -12,7 +12,7 @@ const v1 = {
* '/login' becomes '/auth/login' * '/login' becomes '/auth/login'
*/ */
prefix: '/dash/v1', prefix: '/dash/v1',
/* /*
* Define middleware that should be applied to all * Define middleware that should be applied to all
* routes defined in this file. Middleware should be * routes defined in this file. Middleware should be
@ -34,15 +34,21 @@ const v1 = {
* controller() calls get methods in Flitter controllers * controller() calls get methods in Flitter controllers
*/ */
get: { get: {
// '/': [ controller('Controller_Name').handler_name ],
'/': [ _flitter.controller('dash:v1').main ], '/': [ _flitter.controller('dash:v1').main ],
'/project/new': [ _flitter.controller('dash:v1').new_project_show ], '/project/new': [ _flitter.controller('dash:v1').new_project_show ],
'/project/view/:id': [ _flitter.controller('dash:v1').project_view ], '/project/view/:id': [ _flitter.controller('dash:v1').project_view ],
'/project/delete/:id': [ _flitter.controller('dash:v1').project_delete_show ], '/project/delete/:id': [ _flitter.controller('dash:v1').project_delete_show ],
'/project/edit/:id': [ _flitter.controller('dash:v1').project_edit_show ], '/project/edit/:id': [ _flitter.controller('dash:v1').project_edit_show ],
'/project/share/:id': [ _flitter.controller('dash:v1').project_share_show ],
'/project/share/:id/share/:user': [ _flitter.controller('dash:v1').project_share_do ],
'/project/share/:id/revoke/:user': [ _flitter.controller('dash:v1').project_share_revoke ],
'/project/share/:id/transfer/:user': [ _flitter.controller('dash:v1').project_share_transfer ],
'/out/view/:id': [ _flitter.controller('dash:v1').out_view ], '/out/view/:id': [ _flitter.controller('dash:v1').out_view ],
'/out/delete/:id/:project': [ _flitter.controller('dash:v1').out_delete ],
'/code': [ _flitter.controller('dash:v1').view_code ],
}, },
/* /*
@ -61,4 +67,4 @@ const v1 = {
}, },
} }
module.exports = v1 module.exports = v1

@ -0,0 +1,15 @@
extends ./template
block content
p
| These code snippets are designed to be included in-line. You can store outputs using the
code out()
| function. Then, call the
code breakpoint()
| function to send those outputs to DevBug.
p
| You'll need to ensure that the DevBug server URL and Project API Key are correct. These are local variables in the
code breakpoint()
| function.
h2 PHP
pre
code #{devbug.code.php}

@ -17,7 +17,27 @@ block content
ul(style='list-style-type: none; margin: 0; padding: 0;') ul(style='list-style-type: none; margin: 0; padding: 0;')
li li
a.action(href='/dash/v1/project/view/'+project.id) View a.action(href='/dash/v1/project/view/'+project.id) View
li
a.action(href='/dash/v1/project/share/'+project.id) Share
li li
a.action(href='/dash/v1/project/delete/'+project.id) Delete a.action(href='/dash/v1/project/delete/'+project.id) Delete
li li
a.action(href='/dash/v1/project/edit/'+project.id) Edit a.action(href='/dash/v1/project/edit/'+project.id) Edit
if shared_projects
h3 Projects Shared With Me
table
thead
tr
th(scope='col' style='min-width: 250px') Name
th(scope='col') Actions
tbody
each project in shared_projects
tr
td #{project.name}
td
ul(style='list-style-type: none; margin: 0; padding: 0;')
li
a.action(href='/dash/v1/project/view/'+project.id) View
li
a.action(href='/dash/v1/project/share/'+project.id+'/revoke/'+user.uuid) Remove

@ -1,5 +1,4 @@
extends ./template extends ./template
block content block content
pre script(src='/assets/dash_v1.js')
code script output(syntaxHighlight(`#{prettyd}`));
div #{prettyd}

@ -0,0 +1,45 @@
extends ./template
block content
h2 Shared With
table
thead
tr
th(scope='col' style='min-width: 250px') Username
th(scope='col') Actions
tbody
each user in sharing.shared
tr
td #{(user.uuid === project.user_id ? user.username + " (Owner)" : user.username)}
td
ul(style='list-style-type: none; margin: 0; padding: 0;')
if !(user.uuid === project.user_id)
li
a.action(href='/dash/v1/project/share/'+project.id+'/revoke/'+user.uuid) Revoke
li
a.action(href='/dash/v1/project/share/'+project.id+'/transfer/'+user.uuid) Transfer Ownership
else
li
strike Revoke
li
strike Transfer Ownership
br
h2 Share With New User
table
thead
tr
th(scope='col' style='min-width: 250px') Username
th(scope='col') Actions
tbody
each user in sharing.to_share
tr
td #{(user.uuid === project.user_id ? user.username + " (Owner)" : user.username)}
td
ul(style='list-style-type: none; margin: 0; padding: 0;')
if !(user.uuid === project.user_id)
li
a.action(href='/dash/v1/project/share/' + project.id + '/share/'+user.uuid) Share
else
li
strike Share
li
a.action(href='/dash/v1/project/share/' + project.id + '/transfer/' + user.uuid) Transfer Ownership

@ -2,17 +2,23 @@ html
head head
title #{(title ? title+' | DevBug' : 'DevBug Dashboard')} title #{(title ? title+' | DevBug' : 'DevBug Dashboard')}
link(rel='stylesheet' href='/assets/dash_v1.css') link(rel='stylesheet' href='/assets/dash_v1.css')
script(src='/assets/dash_v1.js')
body body
if title_small .page-header
h3 #{(title ? title+' | DevBug' : 'DevBug Dashboard')} .devbug-header DevBug | v#{devbug.version} #{(user ? " | User: "+user.username : "")} #{(project ? " | Project: "+project.name+" | API: "+project.uuid : "")} #{((_flitter.config('server.environment') === 'development') ? " | Development" : "" )}
else if title_small
h1 #{(title ? title+' | DevBug' : 'DevBug Dashboard')} h3 #{(title ? title : 'Dashboard')}
ul.navul else
li.navli h1 #{(title ? title : 'Dashboard')}
a.nava(href='/dash/v1') Home ul.navul
li.navli
a.nava(href='/auth/logout') Logout
if show_back
li.navli li.navli
a.nava(href='javascript:window.history.back()') Back a.nava(href='/dash/v1') Home
block content li.navli
a.nava(href='/dash/v1/code') Code Snippets
li.navli
a.nava(href='/auth/logout') Logout
if show_back
li.navli
a.nava(href='javascript:window.history.back()') Back
.spacer
block content

@ -17,4 +17,4 @@ block content
li li
a.action(href='/dash/v1/out/view/'+out.id) View a.action(href='/dash/v1/out/view/'+out.id) View
li li
a.action(href='/dash/v1/out/delete/'+out.id) Delete a.action(href='/dash/v1/out/delete/'+out.id+'/'+project.id) Delete

@ -0,0 +1,33 @@
html
head
title Access Denied | Flitter
style(type="text/css").
@import url('https://fonts.googleapis.com/css?family=Rajdhani');
html,
body {
height: 100%;
overflow-y: hidden;
background-color: #c7dbdf;
}
.flitter-container {
height: 60%;
display: flex;
align-items: center;
justify-content: center;
}
.flitter-image {
height: 150px;
}
.flitter-name {
font-family: "Rajdhani";
font-size: 36pt;
margin-left: 35px;
color: #00323d;
}
body
.flitter-container
img.flitter-image(src="/assets/flitter.png")
p.flitter-name Access Denied: #{(reason ? reason : "Resource Not Found.")}

@ -23,11 +23,11 @@ html
.flitter-name { .flitter-name {
font-family: "Rajdhani"; font-family: "Rajdhani";
font-size: 50pt; font-size: 36pt;
margin-left: 35px; margin-left: 35px;
color: #00323d; color: #00323d;
} }
body body
.flitter-container .flitter-container
img.flitter-image(src="/assets/flitter.png") img.flitter-image(src="/assets/flitter.png")
p.flitter-name 404: Page Not Found p.flitter-name 404: #{(reason ? reason : "Resource Not Found.")}

Loading…
Cancel
Save